手撕源码-vue2响应式原理

前言

首先借用一张图来看下vue响应式原理的大致流程:

理解响应式原理需要了解三个类: Observe、Dep、Watcher, Observe类为每个属性创建一个dep类,同时data中的属性进行劫持,属性get的时候往dep.sub数组里面添加watcher, set属性的时候执行dep函数中的notify通知所有的watcher触发更新。

上图说明了,组件在执行render函数,构建虚拟dom的过程中:构建虚拟dom过程中会触发属性的get方法,将watcher收集为依赖,然后将页面挂载到真实dom上,当属性发生改变时,触发set函数,set函数触发属性dep的notify函数通知watcher,生成新的dom树,然后与原有dom树进行对比,更新真实dom。

思考:怎么直接访问data里面的数据呢?

实例化过程执行_init()方法,往实例上添加属性、初始化data ,将data绑定到实例vm上,创建observer实例,全局监听data,为每个属性创建一个dep实例,Object.defineProperty 添加get 方法, set方法(研究响应式原理会重点分析)

上面是之前文章提到过的,实例化的时候会对data进行初始化,我们先看下做了那些操作:

function initData (vm) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'  // vm._data
    ? data()
    : data || {}
  const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    proxy(vm, `_data`, keys[i])
  }
  // 正式监听全局 data 了
  observe(data, true) // data 是引用的关系,引用的vm._data
}

function proxy (target, sourceKey, key) { // vm  _data  msg
  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      return this[sourceKey][key] // this为 target   即 vm._data.msg
    },
    set: function (val) {
      this[sourceKey][key] = val
    }
  })
}
复制代码

对data进行遍历,将data上的属性直接绑到vue实例上,this.xxx 就是 this.data.xxx 

接下来看下,这三个类是如何实现的?相互之间有何关系?也就是响应式原理

响应式原理

1. Observer类

render的时候收集依赖,修改数据的时候进行更新,这里显然用到的是观察者模式,修改data数据属性的时候都会触发页面更新,也就是说每个属性都会对应一个观察者模式的实例,因此我们用Observer类来管理data属性的所有的观察者模式,Dep类作为目标对象,watcher作为观察者。

Observer类循环对data中的属性,每个属性创建一个Dep实例,通过Object.defineProperty 进行劫持,get的时候收集观察者watcher, set的时候通知watcher更新

class Observer {
  constructor(value) {
    this.walk(value)
  }
  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      // data 的每个属性都执行这个函数
      defineReactive(obj, keys[i])
    }
  }
}

function defineReactive (obj, key) {
  // 每个属性都对应一个 dep 实例
  const dep = new Dep()
  let val = obj[key] // 属性值

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      if (Dep.target) { // Dep.target 即为 watcher
        // 将 watcher 加入数组中,
        // dep.depend() 等价于 dep.addSub(Dep.target)
        dep.depend()
      }
      // 返回属性值
      return val
    },
    set: function (newVal) {
      // data中所有的变量赋值都会走set,只不过render中没用到的,那么它的dep.subs是空的,不会触发watcher的update
      if (newVal !== val) {
        val = newVal // val是闭包,会一直存在
        dep.notify() // 通知 watcher 更新
      }
    }
  })
}
复制代码

我们看到了收集依赖的时候执行了dep.depend(),而不是直接执行dep.addSub(Dep.target),这是为什么呢?先留个疑问,下面分析

2. Dep 类

dep类

id: dep的唯一标识,每个属性对应一个dep

subs: [watcher],用于记录该属性的所有观察者

let depId = 0
class Dep {
  constructor () {
    this.id = ++depId
    this.subs = []
  }
  addSub (sub) {
    this.subs.push(sub)
  }
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()  // sub[i] 就是一个watcher,每个watch都有个update方法,相当于执行回调函数
    }
  }
  // addSub 是'Dep主动'添加watcher
  // depend 是让 watcher 主动‘被添加’到Dep中
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
}
Dep.target = null
复制代码

3. Watcher

Watcher类

deps: [dep] 维护一个depId的数组,所有依赖的数据,如果老的dep id 在新get收集后的ids中不存在,那么会删除老的dep中对应的id

getter: 表达式或者是函数

value: 每次求值得到的结果

watcher在源码中有三种,分别是全局watcher, 自定义$watcher,还有就是conputed watcher,这里先不考虑computed watcher

全局watcher

全局watcher是是什么时候定义的呢?

注意:mountComponent 函数中的 new Watcher(vm, updateComponent, noop, {})

在vue实例挂载阶段也就是执行$mount函数的时候,调用了mountComponent 函数, 执行了的new Watcher(vm, updateComponent, noop, {}),实例化watcher的时候constructor中执行了this.get方法,触发了回调updateComponent,也就是 vm._update(vm._render()) ,执行render方法,在render方法执行中访问data里面的属性,被Object.defineProperty的get方法劫持(初始化数据阶段做了数据劫持),触发依赖收集,将全局watcher加入到subs数组中

也就是说,vue只会对render里面用到的数据进行监听

自定义watcher

自定义watcher是怎么被属性加到自己的subs数组里的呢?

实例化获取value时触发get方法,进一步触发属性的get,从而被收集起来

// 定义 $mount 挂载方法
Vue.prototype.$mount = function (el) {
    const vm = this
    vm.$el = typeof el === 'string' ? document.querySelector(el) : document.body
    const options = vm.$options;
    if (!options.render) {
        // 如果不存在 render ,那么就需要 compileToFunctions 编译 render了,这里我们刚好跳过了。。。
        // vm.options.render = compileToFunctions(vm.options.template)
    }
    // 继续
    mountComponent(vm) // 在 lifecycle.js 中定义的
}

// lifecycle.js
function mountComponent (vm) {
    // callHook(vm, 'beforeMount') // 生命周期

    let updateComponent = function () {
        vm._update(vm._render())    // 执行 _render()获得 vnode,再执行 _update 渲染到 dom 中
    }
    // 定义全局Watcher
    new Watcher(vm, updateComponent, noop, {})
    // noop 是一个空函数,本来应该传cb回调函数,但是全局这个Watcher,run的时候执行了const val = this.get()
    // 相当于执行了updateComponent(),即updateComponent就是作为回调函数了,因此cb传个空函数
}

class Watcher {
    constructor(vm, expOrFn, cb, options) {

      // expOrFn 有两种
      // 1. vm.$watch('msg', cb)   expOrFn === msg 为想要监听的属性
      // 2. 全局 Watcher 中,expOrFn === updateComponent 函数
      this.vm = vm
      this.cb = cb
      this.expOrFn = expOrFn
      this.depIds = new Set()  // 用于dep去重
      this.val = this.get() // 触发依赖收集
    }
    // 让key对应的dep,收集自己
    get () {
      let value
      const vm = this.vm
      Dep.target = this // 变为自己
      if (typeof this.expOrFn === 'function') { // 如果全局watcher 传进来的是 updateComponent 函数
        this.getter = this.expOrFn
        value = this.getter.call(vm) // 执行 updateComponent ,触发了 _render
      } else { // 如果是 vm.$watcher('msg') 这种的
        let key = this.expOrFn // key === msg
        console.log(8989,key)
        value = vm._data[key] // 这一步触发了 属性的 get ,defineReactive中,此时 Dep.target 是有值的哈
      }

      Dep.target = null // 清空 watcher

      this.cleanupDeps() // 源码是要清理已经不在render中的属性了,因为可能v-if把一个元素给去掉了

      return value
    }
    // 执行监听回调
    update () {
      this.run()
    }

    run () {
      const vm = this.vm
      const val = this.get() // 每次修改一个属性,都会再次执行this.get(),触发新的render收集依赖;;;新老依赖的dep会通过cleanupDeps做清理

      if (val !== this.val) {
        const oldVal = this.val
        this.val = val
        this.cb.call(vm, val, oldVal) // 新、老 val 传给回调函数并执行
      }
    }

    addDep (dep) {
      const id = dep.id
      if (!this.depIds.has(id)) { // 去重,防止多次render时多次收集依赖
        this.depIds.add(id)
        dep.addSub(this) // dep 将 watcher 实例添加进去
        console.log(12, this.depIds, dep)
      }
    }
    // 我们这里没实现它,但是它很有用,如果老的dep id 在新get收集后的ids中不存在,那么会删除老的dep中对应的id
    cleanupDeps () {
      // let i = this.deps.length
      // while (i--) {
      //   const dep = this.deps[i]
      //   if (!this.newDepIds.has(dep.id)) {
      //     dep.removeSub(this)
      //   }
      // }
      // let tmp = this.depIds
      // this.depIds = this.newDepIds
      // this.newDepIds = tmp
      // this.newDepIds.clear()
      // tmp = this.deps
      // this.deps = this.newDeps
      // this.newDeps = tmp
      // this.newDeps.length = 0
    }
  }
复制代码

以上只说了watcher是怎么被收集起来的,那怎么触发更新呢?

当修改属性值时,执行属性的set方法,通知watcher更新,但是属性需要通知的watcher不止一个呀,有全局watcher和自定义watcher,都执行了run函数,但是这里有区别:

全局watcher执行润函数时,this.value = get()会触发render函数,重新收集依赖,并将本次更新显示到页面上,全局watcher的value都是undefined,所以不会往下执行

自定义watcher,this.get()获取到最新值后会和之前的值作对比,不一致就会执行自己的回调函数

疑问❓

至此,三个类就结束了,当然现在是简化的,现在可以解释一下为什么在watcher类里面执行了dep.add(watcher)收集依赖,而不是属性get的时候直接往dep里面添加依赖。

我们以为的:

this.xxx -> get -> dep.addSub(watcher) dep.target为watcher

实际:

this.xxx -> get -> dep.depend -> dep.target.addDep(dep) -> dep.addSub(watcher)

每次触发get都收集依赖,怎么去重呢?

本来get的时候就可以收集watcher了,实际上get到dep里面转了一圈,进到watcher,在watcher里面去添加依赖,这是因为,属性可以再render中出现很多次,每次都触发get都收集依赖,那么得去重呀,怎么去重呢

  1. 在dep里面维护watcherId数组,收集过的就不再收集了,看起来没毛病,但是没这么做

2. watcher里面维护一个depId数组,收集依赖的时候先去dep里面转一圈,拿到dep ,传递给watcher,这样每次收集watcher 都能获取到当前dep id , depId数组里面不存在当前dep id,说明该dep没收集过,dep.addSub(watcher),并将当前dep Id push到depId数组里面, 比较绕。。。

为什么选择方式二呢?为什么要在watcher里面去收集依赖呢?

为什么要在watcher里面去收集依赖呢?

// 想想v-if 
// v-if 由 true -> false
// v-if 为 true 的时候, this.name才会出现在render函数里面
// v-if 为 false 的时候, this.name不会出现在render函数里面

<p v-if="false">{{ this.name }}</p>

复制代码

因为开始的时候为true,this.name属性中收集了watcher,当this.name变化的时候会触发更新,那false了呢?,不清理的话,是不是修改之后还会触发更新,这不就浪费性能了嘛。。。。。。


要清理dep里面的watcher,那得知道哪个dep由true变false了,那不就得维护一个depId数组(这里每个data里面的属性对应一个dep,每个dep有一个唯一的depId),对比一下, 上一次render的时候和本次render的时候,少了哪些dep,dep数组在哪里维护?当然是watcher啊,因为render中访问的属性都会收集watcher,而触发页面渲染的回调函数也是在watcher中执行的,所以在watcher中维护depId数组,一来是用于去重,二来是清理上一次render的时候和本次render的时候,少了的dep里面的watcher


想想在dep里面能不能做呢,给dep加个标识是显示还是没显示,是否清空watcher?

不能吧, this.name不会出现在render函数里面,那就不会触发get,更没法改this.name对应的dep的标识了呀~~

总结:

理解为,属性在render函数中有时存在有时不存在,为了避免不存在的时候,修改属性值会触发页面更新,影响性能,因此在watcher中维护depId数组,对比前后两次的depId数组,清空本次不存在的dep里面的watcher,顺便在watcher里面调用dep的addSub(this),把自己添加到dep的数组里,进一步想想,都维护了depId数组了,存在的depId,再次过来的时候就不向dep里面添加自己(watcher)了呗,当然就能对watcher去重了

简单代码

class Observer {
  constructor(value) {
    this.walk(value)
  }
  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      // data 的每个属性都执行这个函数
      defineReactive(obj, keys[i])
    }
  }
}

function defineReactive (obj, key) {
  // 每个属性都对应一个 dep 实例
  const dep = new Dep()
  let val = obj[key] // 属性值

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      if (Dep.target) { // Dep.target 即为 watcher
        // 将 watcher 加入数组中,
        // dep.depend() 等价于 dep.addSub(Dep.target)
        dep.depend()
      }
      // 返回属性值
      return val
    },
    set: function (newVal) {
      // data中所有的变量赋值都会走set,只不过render中没用到的,那么它的dep.subs是空的,不会触发watcher的update
      if (newVal !== val) {
        val = newVal // val是闭包,会一直存在
        dep.notify() // 通知 watcher 更新
      }
    }
  })
}

let depId = 0
class Dep {
  constructor () {
    this.id = ++depId
    this.subs = []
  }
  addSub (sub) {
    this.subs.push(sub)
  }
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()  // sub[i] 就是一个watcher,每个watch都有个update方法,相当于执行回调函数
    }
  }
  // addSub 是'Dep主动'添加watcher
  // depend 是让 watcher 主动‘被添加’到Dep中
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
}
Dep.target = null

// 定义 $mount 挂载方法
Vue.prototype.$mount = function (el) {
    const vm = this
    vm.$el = typeof el === 'string' ? document.querySelector(el) : document.body
    const options = vm.$options;
    if (!options.render) {
        // 如果不存在 render ,那么就需要 compileToFunctions 编译 render了,这里我们刚好跳过了。。。
        // vm.options.render = compileToFunctions(vm.options.template)
    }
    // 继续
    mountComponent(vm) // 在 lifecycle.js 中定义的
}

// lifecycle.js
function mountComponent (vm) {
    // callHook(vm, 'beforeMount') // 生命周期

    let updateComponent = function () {
        vm._update(vm._render())    // 执行 _render()获得 vnode,再执行 _update 渲染到 dom 中
    }
    // 定义全局Watcher
    new Watcher(vm, updateComponent, noop, {})
    // noop 是一个空函数,本来应该传cb回调函数,但是全局这个Watcher,run的时候执行了const val = this.get()
    // 相当于执行了updateComponent(),即updateComponent就是作为回调函数了,因此cb传个空函数
}

class Watcher {
  constructor(vm, expOrFn, cb, options) {

    // expOrFn 有两种
    // 1. vm.$watch('msg', cb)   expOrFn === msg 为想要监听的属性
    // 2. 全局 Watcher 中,expOrFn === updateComponent 函数
    this.vm = vm
    this.cb = cb
    this.expOrFn = expOrFn
    this.depIds = new Set()  // 用于dep去重
    this.val = this.get() // 触发依赖收集
  }
  // 让key对应的dep,收集自己
  get () {
    let value
    const vm = this.vm
    Dep.target = this // 变为自己
    if (typeof this.expOrFn === 'function') { // 如果全局watcher 传进来的是 updateComponent 函数
      this.getter = this.expOrFn
      value = this.getter.call(vm) // 执行 updateComponent ,触发了 _render
    } else { // 如果是 vm.$watcher('msg') 这种的
      let key = this.expOrFn // key === msg
      console.log(8989,key)
      value = vm._data[key] // 这一步触发了 属性的 get ,defineReactive中,此时 Dep.target 是有值的哈
    }

    Dep.target = null // 清空 watcher

    this.cleanupDeps() // 源码是要清理已经不在render中的属性了,因为可能v-if把一个元素给去掉了

    return value
  }
  // 执行监听回调
  update () {
    this.run()
  }

  run () {
    const vm = this.vm
    const val = this.get() // 每次修改一个属性,都会再次执行this.get(),触发新的render收集依赖;;;新老依赖的dep会通过cleanupDeps做清理

    if (val !== this.val) {
      const oldVal = this.val
      this.val = val
      this.cb.call(vm, val, oldVal) // 新、老 val 传给回调函数并执行
    }
  }

  addDep (dep) {
    const id = dep.id
    if (!this.depIds.has(id)) { // 去重,防止多次render时多次收集依赖
      this.depIds.add(id)
      dep.addSub(this) // dep 将 watcher 实例添加进去
      console.log(12, this.depIds, dep)
    }
  }
  // 我们这里没实现它,但是它很有用,如果老的dep id 在新get收集后的ids中不存在,那么会删除老的dep中对应的id
  cleanupDeps () {
    // let i = this.deps.length
    // while (i--) {
    //   const dep = this.deps[i]
    //   if (!this.newDepIds.has(dep.id)) {
    //     dep.removeSub(this)
    //   }
    // }
    // let tmp = this.depIds
    // this.depIds = this.newDepIds
    // this.newDepIds = tmp
    // this.newDepIds.clear()
    // tmp = this.deps
    // this.deps = this.newDeps
    // this.newDeps = tmp
    // this.newDeps.length = 0
  }
}
复制代码

猜你喜欢

转载自juejin.im/post/7031890120181547015