Vue 依赖收集原理分析

作者: wuww | 来源:发表于2017-06-14 10:19 被阅读1242次

    Vue 依赖收集原理分析

    Vue实例在初始化时,可以接受以下几类数据:

    • 模板
    • 初始化数据
    • 传递给组件的属性值
    • computed
    • watch
    • methods

    Vue 根据实例化时接受的数据,在将数据和模板转化成DOM节点的同时,分析其依赖的数据。在特定数据改变时,自动在下一个周期重新渲染DOM节点

    本文主要分析Vue是如何进行依赖收集的。

    Vue中,与依赖收集相关的类有:

    Dep : 一个订阅者的列表类,可以增加或删除订阅者,可以向订阅者发送消息

    Watcher : 订阅者类。它在初始化时可以接受getter, callback两个函数作为参数。getter用来计算Watcher对象的值。当Watcher被触发时,会重新通过getter计算当前Watcher的值,如果值改变,则会执行callback.

    对初始化数据的处理

    对于一个Vue组件,需要一个初始化数据的生成函数。如下:

    export default {
        data () {
            return {
                text: 'some texts',
                arr: [],
                obj: {}
            }
        }
    }
    

    Vue为数据中的每一个key维护一个订阅者列表。对于生成的数据,通过Object.defineProperty对其中的每一个key进行处理,主要是为每一个key设置get, set方法,以此来为对应的key收集订阅者,并在值改变时通知对应的订阅者。部分代码如下:

      const dep = new Dep()
    
      const property = Object.getOwnPropertyDescriptor(obj, key)
      if (property && property.configurable === false) {
        return
      }
    
      // cater for pre-defined getter/setters
      const getter = property && property.get
      const setter = property && property.set
    
      let childOb = observe(val)
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
          const value = getter ? getter.call(obj) : val
          if (Dep.target) {
            dep.depend()
            if (childOb) {
              childOb.dep.depend()
            }
            if (Array.isArray(value)) {
              dependArray(value)
            }
          }
          return value
        },
        set: function reactiveSetter (newVal) {
          const value = getter ? getter.call(obj) : val
          /* eslint-disable no-self-compare */
          if (newVal === value || (newVal !== newVal && value !== value)) {
            return
          }
          /* eslint-enable no-self-compare */
          if (process.env.NODE_ENV !== 'production' && customSetter) {
            customSetter()
          }
          if (setter) {
            setter.call(obj, newVal)
          } else {
            val = newVal
          }
          childOb = observe(newVal)
          dep.notify()
        }
      })
    

    每一key都有一个订阅者列表
    const dep = new Dep()

    在为key进行赋值时,如果值发生了改变,则会通知所有的订阅者
    dep.notify()

    在对key进行取值时,如果Dep.target有值,除正常的取值操作外会进行一些额外的操作来添加订阅者。大多数时间里,Dep.target的值都为null,只有订阅者在进行订阅操作时,Dep.target才有值,为正在进行订阅的订阅者。此时进行取值操作,会将订阅者加入到对应的订阅者列表中。

    订阅者在进行订阅操作时,主要包含以下3个步骤:

    • 将自己放在Dep.target
    • 对自己依赖的key进行取值
    • 将自己从Dep.target移除

    在执行订阅操作后,订阅者会被加入到相关key的订阅者列表中。

    针对对象和数组的处理

    如果为key赋的值为对象:

    • 会递归地对这个对象中的每一key进行处理

    如果为key赋的值为数组:

    • 递归地对这个数组中的每一个对象进行处理
    • 重新定义数组的push,pop,shift,unshift,splice,sort,reverse方法,调用以上方法时key的订阅者列表会通知订阅者们“值已改变”。如果调用的是push,unshift,splice方法,递归处理新增加的项

    对模板的处理

    Vue将模板处理成一个render函数。需要重新渲染DOM时,render函数结合Vue实例中的数据生成一个虚拟节点。新的虚拟节点和原虚拟节点进行对比,对需要修改的DOM节点进行修改。

    订阅者

    订阅者在初始化时主要接受2个参数getter, callbackgetter用来计算订阅者的值,所以其在执行时会对订阅者所有需要订阅的key进行取值。订阅者的订阅操作主要是通过getter来实现。

    部分代码如下:

      /**
       * Evaluate the getter, and re-collect dependencies.
       */
      get () {
        pushTarget(this)
        let value
        const vm = this.vm
        if (this.user) {
          try {
            value = this.getter.call(vm, vm)
          } catch (e) {
            handleError(e, vm, `getter for watcher "${this.expression}"`)
          }
        } else {
          value = this.getter.call(vm, vm)
        }
        // "touch" every property so they are all tracked as
        // dependencies for deep watching
        if (this.deep) {
          traverse(value)
        }
        popTarget()
        this.cleanupDeps()
        return value
      }
    

    主要步骤:

    • 将自己放在Dep.target上(pushTarget(this))
    • 执行getter(this.getter.call(vm, vm))
    • 将自己从Dep.target移除(popTarget())
    • 清理之前的订阅(this.cleanupDeps())

    此后,订阅者在依赖的key的值发生变化会得到通知。获得通知的订阅者并不会立即被触发,而是会被加入到一个待触发的数组中,在下一个周期统一被触发。

    订阅者在被触发时,会执行getter来计算订阅者的值,如果值改变,则会执行callback.

    负责渲染DOM的订阅者

    Vue实例化后都会生成一个用于渲染DOM的订阅者。此订阅者在实例化时传入的getter方法为渲染DOM的方法。

    部分代码如下:

    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
        
    vm._watcher = new Watcher(vm, updateComponent, noop) 
    

    vm._render()结合模板和数据,计算出虚拟DOM
    vm._update()根据虚拟DOM渲染真实的DOM节点

    此订阅者在初始化时就会进行订阅操作。实例化时传入的getterupdateComponent。其中的vm._render()在执行时一定会对所有依赖的key进行取值,能完成对依赖的key的订阅。同时vm._update()完成了第一次DOM渲染。当前依赖的key的值发生变化,订阅者被触发时,作为getterupdateComponent会重新执行,重新渲染DOM。因为getter返回的值一直为undefined,所以此订阅者中的callback并没有被用到,于是传入了一个空函数noop作为callback

    对computed的处理

    通过computed可以定义一组计算属性,通过计算属性可以将一些复杂的计算过程抽离出来,保持模板的简单和清晰。

    代码示例:

    export default {
        data () {
            return {
                text: 'some texts',
                arr: [],
                obj: {}
            }
        },
        computed: {
            key1: function () {
                return this.text + this.arr.length
            }
        }
    }
    

    在定义一个计算属性时,需要定义一个key和一个计算方法。

    Vue在对computed进行处理时,会为每一个计算属性生成一个lazy状态的订阅者。普通的订阅者在实例化和触发时会执行getter来计算自身的值和进行订阅操作。而lazy状态的订阅者在上述情况下只会将自身置为dirty状态,不进行其它操作。在订阅者执行自身的evaluate方法时,会清除自身的dirty状态并执行getter来计算自身的值和进行订阅。

    Vue在为计算属性生成订阅者时的示例代码如下:

    const computedWatcherOptions = { lazy: true }
    
    // create internal watcher for the computed property.
    watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)
    

    传入的getter为自定义的计算方法,callback为空函数。(lazy状态的订阅者永远都没有机会执行callback)

    Vue 在自身实例上为指定key定义get方法,使可以通过Vue实例获取计算属性的值。

    部分代码如下:

    function createComputedGetter (key) {
      return function computedGetter () {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
          if (watcher.dirty) {
            watcher.evaluate()
          }
          if (Dep.target) {
            watcher.depend()
          }
          return watcher.value
        }
      }
    }
    

    在对计算属性定义的key进行取值时,会首先获取之前生成好的订阅者。只有订阅者处于dirty状态时,才会执行evaluate计算订阅者的值。所以为计算属性定义的计算方法只有在对计算属性的key进行取值并且计算属性依赖的key曾经改变时才会执行。

    假如对上文定义的计算属性key1进行取值

    vm.key1; //第一次取值,自定义计算方法执行
    vm.key1; //第二次取值,依赖的key的值没有变化,自定义计算方法不会执行
    vm.text = '' //改变计算属性依赖的key的值,计算属性对应的订阅者会进入dirty状态,自定义计算方法不会执行
    vm.key1; //第三次取值,计算属性依赖的key的值发生了变化并且对计算属性进行取值,自定义的计算方法执行
    
    订阅计算属性值的变化

    计算属性的key不会维护一个订阅者列表,也不能通过计算属性的set方法在触发所有订阅者。(计算属性不能被赋值)。一个订阅者执行订阅操作来订阅计算属性值的变化其实是订阅了计算属性依赖的key的值的变化。
    在计算属性的get方法中

    if (Dep.target) {
        watcher.depend()
    }
    

    如果有订阅者来订阅计算属性的变化,计算属性会将自己的订阅复制到正在进行订阅的订阅者上。watcher.depend()的作用就是如此。

    例如:

    //初始化订阅者watcher, 依赖计算属性key1
    var watcher = new Watcher(function () {
        return vm.key1
    }, noop)
    
    vm.text = '' //计算属性key1依赖的text的值发生变化,watcher会被触发
    

    对watch的处理

    Vue实例化时可以传入watch对象,来监听某些值的变化。
    例如:

    export default {
        watch: {
            'a.b.c': function (val, oldVal) {
                console.log(val)
                console.log(oldVal)
            }
        }
    }
    

    Vue 会为watch中的每一项生成一个订阅者。订阅者的getter通过处理字符串得到。如'a.b.c'会被处理成

    function (vm) {
        var a = vm.a
        var b = a.b
        var c = b.c
        return c
    }
    

    处理字符串的源码如下:

    /**
     * Parse simple path.
     */
    const bailRE = /[^\w.$]/
    export function parsePath (path: string): any {
      if (bailRE.test(path)) {
        return
      }
      const segments = path.split('.')
      return function (obj) {
        for (let i = 0; i < segments.length; i++) {
          if (!obj) return
          obj = obj[segments[i]]
        }
        return obj
      }
    }
    

    订阅者的callback为定义watch时传入的监听函数。当订阅者被触发时,如果订阅者的值发生变化,则会执行callbackcallback执行时会传入变化后的值,变化前的值作为参数。


    相关文章

      网友评论

        本文标题:Vue 依赖收集原理分析

        本文链接:https://www.haomeiwen.com/subject/lcsfyttx.html