美文网首页Vue源码分析
彻底理解vue里面的各种watcher及其作用

彻底理解vue里面的各种watcher及其作用

作者: 0月 | 来源:发表于2021-10-25 23:46 被阅读0次

    vue中的watcher的分类

    在文章vue异步更新流程梳理中提到了vue中的watcher的分类,分别是:

    1. 渲染watcher, 负责更新视图变化的,即一个vue实例对应一个渲染watcher
    2. 用户自定义watcher,用户通过watch:{value(val, oldVal){}}选项定义的,或者this.$watch()方法生成的。
    3. computed选项里面的计算属性也是watcher, 和第2点中的watcher的区别是它的watcher实例有dirty属性控制着watcher.value值的变化

    先看一下 Watcher 是怎么定义的:

    export default class Watcher {
      vm: Component;
      expression: string;
      cb: Function;
      id: number;
      deep: boolean;
      user: boolean;
      lazy: boolean;
      sync: boolean;
      dirty: boolean;
      active: boolean;
      deps: Array<Dep>;
      newDeps: Array<Dep>;
      depIds: SimpleSet;
      newDepIds: SimpleSet;
      before: ?Function;
      getter: Function;
      value: any;
    
      constructor (
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
      ) {
        this.vm = vm
        if (isRenderWatcher) {
          vm._watcher = this
        }
        vm._watchers.push(this)
        // options
        if (options) {
          this.deep = !!options.deep
          this.user = !!options.user
          this.lazy = !!options.lazy
          this.sync = !!options.sync
          this.before = options.before
        } else {
          this.deep = this.user = this.lazy = this.sync = false
        }
        this.cb = cb
        this.id = ++uid // uid for batching
        this.active = true
        this.dirty = this.lazy // for lazy watchers
        this.deps = []
        this.newDeps = []
        this.depIds = new Set()
        this.newDepIds = new Set()
        this.expression = process.env.NODE_ENV !== 'production'
          ? expOrFn.toString()
          : ''
        // parse expression for getter
        if (typeof expOrFn === 'function') {
          this.getter = expOrFn
        } else {
          this.getter = parsePath(expOrFn)
          if (!this.getter) {
            this.getter = noop
            process.env.NODE_ENV !== 'production' && warn(
              `Failed watching path: "${expOrFn}" ` +
              'Watcher only accepts simple dot-delimited paths. ' +
              'For full control, use a function instead.',
              vm
            )
          }
        }
        this.value = this.lazy
          ? undefined
          : this.get()
      }
    
      /**
       * Evaluate the getter, and re-collect dependencies.
       */
      get () {
        pushTarget(this)
        let value
        const vm = this.vm
        try {
          value = this.getter.call(vm, vm)
        } catch (e) {
          if (this.user) {
            handleError(e, vm, `getter for watcher "${this.expression}"`)
          } else {
            throw e
          }
        } finally {
          // "touch" every property so they are all tracked as
          // dependencies for deep watching
          if (this.deep) {
            traverse(value)
          }
          popTarget()
          this.cleanupDeps()
        }
        return value
      }
    
      /**
       * Add a dependency to this directive.
       */
      addDep (dep: Dep) {
        const id = dep.id
        if (!this.newDepIds.has(id)) {
          this.newDepIds.add(id)
          this.newDeps.push(dep)
          if (!this.depIds.has(id)) {
            dep.addSub(this)
          }
        }
      }
    
      /**
       * Clean up for dependency collection.
       */
      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
      }
    
      /**
       * Subscriber interface.
       * Will be called when a dependency changes.
       */
      update () {
        /* istanbul ignore else */
        if (this.lazy) {
          this.dirty = true
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
      }
    
      /**
       * Scheduler job interface.
       * Will be called by the scheduler.
       */
      run () {
        if (this.active) {
          const value = this.get()
          if (
            value !== this.value ||
            // Deep watchers and watchers on Object/Arrays should fire even
            // when the value is the same, because the value may
            // have mutated.
            isObject(value) ||
            this.deep
          ) {
            // set new value
            const oldValue = this.value
            this.value = value
            if (this.user) {
              const info = `callback for watcher "${this.expression}"`
              invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
            } else {
              this.cb.call(this.vm, value, oldValue)
            }
          }
        }
      }
    
      /**
       * Evaluate the value of the watcher.
       * This only gets called for lazy watchers.
       */
      evaluate () {
        this.value = this.get()
        this.dirty = false
      }
    
      /**
       * Depend on all deps collected by this watcher.
       */
      depend () {
        let i = this.deps.length
        while (i--) {
          this.deps[i].depend()
        }
      }
    
      /**
       * Remove self from all dependencies' subscriber list.
       */
      teardown () {
        if (this.active) {
          // remove self from vm's watcher list
          // this is a somewhat expensive operation so we skip it
          // if the vm is being destroyed.
          if (!this.vm._isBeingDestroyed) {
            remove(this.vm._watchers, this)
          }
          let i = this.deps.length
          while (i--) {
            this.deps[i].removeSub(this)
          }
          this.active = false
        }
      }
    }
    
    

    看不懂没关系,先分析,用到了再看。

    渲染watcher

    在vue实例初始化的时候会走的流程中,会生成一个渲染watcher

    function mountComponent(){
    // 省略其他代码
    
    new Watcher(vm, updateComponent, noop, {
        before () {
          if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate')
          }
        }
      }, true /* isRenderWatcher */)
    }
    
    // 省略其他代码
    
    

    可以看到第5个参数 为 true /* isRenderWatcher */; 说明是渲染watcher;它的作用其实也非常明确,就是组件内的响应式数据变更了,就会触发setter, setter里面调用了dep.nofity(), dep.subs循环拿到每一个watcher, 执行watcher.update();当这个watcher是渲染watcher时,其实就是走update 方法

    update () {
        /* istanbul ignore else */
        if (this.lazy) {
          this.dirty = true
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
      }
    

    的 queueWatcher(this)方法,这里最终会走到patch流程里面去。具体可以看我另外一篇文章彻底理解vue的patch流程和diff算法

    computed watcher

    其实就是计算属性,内部也是采用wacther实现,我们经常听说computed 计算属性求值可以缓存,有利于性能提升,现在看一下怎么提升性能的吧

    首先是初始化 if (opts.computed) initComputed(vm, opts.computed)

    image.png

    看initComputed做了啥


    image.png

    其实分三步:

    1. 创建一个vm._computedWatchers对象
    2. 遍历computed 中的key记录所有computed属性的watcher
    3. definComputed(vm, key, userDef) 这里就是vm实例代理了计算属性


      image.png

    我们仔细看这个createComputedGetter 方法

    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
        }
      }
    }
    

    这里就是计算属性的关键位置: return function computedGetter () {}作为计算属性的getter方法
    例子来说明

    data() {
       return {a: 1}
    },
    computed: {
       double(){ return this.a * 2}
    }
    

    经过上面的初始化流程后会有这么一个样子:

    vm: {
      _computedWatchers: {
        double: {
          value: undefined, // 一开始并没有求值
          dirty: true // 初始化会设置为true,代表脏了,脏了的意思是要重新求值
        } as  computedWatcher // 这里是一个watcher实例
      },
      
      double: { // double是通过Object.defineProperty(target, key, sharedPropertyDefinition)代理挂上去的
         getter: computedGetter (){
           const watcher = this._computedWatchers &&this._computedWatchers['double']
           if (watcher) {
             if (watcher.dirty) {
               watcher.evaluate()
              }
             if (Dep.target) {
                watcher.depend()
             }
             return watcher.value
           }
         }
      }
    }
    

    重点来了,一开始initComputed这套流程下来只是设置好上面这种关系。其中double这里并没有求值,所以vm._computedWatchers.double.value是undefined, 因为computed在new Watcher时是传的lazy: true进去的。

    image.png

    什么时候开始求值呢?答案是在执行组件render函数的时候,因为走渲染流程会生成组件vnode, 里面会对所有用到的变量进行读取,就会触发变量的getter! 当模板中有这么一句话:

    <div>{{double}}</div>
    

    时,在render函数时会读取double的值,触发double.getter方法:

    double读取值的流程.png

    此时看getter方法里面dirty: true 会走evaluate方法,看看evaluate做了啥?


    image.png
    image.png

    其实就是this.getter去读取值,此时更新了watcher.value为最新值了,然后就可以把dirty设置为false了。
    this.getter是干啥呢?返回到文章上面初始化时的new Watcher看看 ,就是userDef 或者是 userDef.get; 当我们以对象形式配置计算属性的get(){}, set(){}时,就是取userDef.get 如果我们只是配置了function直接当作get函数了,此处例子的userDef就是computed定义的

     function () { return this.a * 2}
    

    绕了一大圈,最后求值时还是执行了我们自定义的computed get函数!

    那么问题来了:具体是如何体现double的值是缓存呢?如何确定计算属性中的对应依赖this.a更新了,double值才会跟着更新呢?

    其实还是在我们的定义get函数这里做了衔接才能形成闭环。

    当执行

    function () { return this.a * 2}
    

    的时候,this.a会触发a的getter函数, 那么此时因为computed watcher在执行this.get()的时候把有这么一句:pushTarget(this)

    get () {
        pushTarget(this)
    // 省略其他代码
    }
    

    这里就是把Dep.target = 该computed watcher了,this.a的getter函数里面的dep的subs会把(Dep.target = 该computed watcher) push进去。这样子,this.a 重新赋值就会触发setter ,就会走watcher.update方法:

    image.png
    此时方法内部会走if (this.lazy) { this.dirty = true}逻辑,因为computed初始化传参进来时lazy就是true; 现在好了,this.dirty又变为true, watcher.value又脏了!下次再读取double的时候,还会重复上面的流程
    double读取值的流程.png

    至此,computed的计算属性值是缓存的解释就走通了。当this.a不变的时候,this.dirty不会变,每次读取double都直接return watcher.value;不用走evaluate方法重新求值,this.a变化时也是只改变watcher.dirty = true, 并没有重新求double值, 只在读double的时候才会去重新evaluate求值,这样子真正做到了按需更新计算属性的值

    用户定义的watch

    用户可以通过this.$watch() 或者配置watch:{}选项来观察变量变化做出处理;统称user watcher


    user watcher.png createWatcher.png

    通过源码可以知道,watche:{}选项最后也是调用了$watch()来实现的; 而且可以配置成数组形式:

    watch: {
      a: [
        function(val, oldVal){},
        function(val, oldVal){}
      ]
    }
    

    看看$watch()方法的定义


    image.png

    所以用户最终定义的watch是通过这一行代码生成watcher实例的

    const watcher = new Watcher(vm, expOrFn, cb, options)
    

    vm: 当前vue组件实例
    expOrFn: 其实就是表达式, 就是watch选项的key字符串,如下例子

    data(){
      return {
        a: 1,
        b: {
          c: 2
        }
      }
    }
    watch: {
      a: function(val, oldVal){},
      'b.c':  function(val, oldVal){}
    }
    

    expOrFn就是 'a' 或者 'b.c';

    cb: 用户定义的回调处理函数,watcher执行get()方法之后会执行
    options: 一些配置,一般可以是 {deep: true, immedate: true, user: true}, 其中deep 和 immedate看用户是否需要配置,user: true是vue代码主动注入的,代表是user watcher

    至此,watch选项的每个key都要生成一个watcher, 追踪new Watcher过程,发现

    this.getter = parsePath(expOrFn)
    // ...
    this.value = this.lazy
          ? undefined
          : this.get()
    
    image.png
    image.png
    image.png

    同样,在读取vm.b.c的过程,触发vm.b的getter,vm.b.c的getter,这两个key的内部的dep.subs会对这个watcher进行收集,到时候,vm.b, vm.b.c重新setter的时候,会走watcher.update()流程。

    说了这么多,那么整个流程是怎么样的呢?假如b.c 重新赋值是怎么触发watch回调的呢?
    this.b.c = 3 触发c这个key的setter,然后 dep.notify() , wacher.update() , queueWatcher(this),这里是把watcher放到一个queue数组里面去,在nextTick之后会把数组中的watcher拿出来,执行watcher.run();如下图1 2 3解释了流程


    1.png 2.png
    3.png

    watcher.run() 干啥了?如下图


    watcher.run().png watcher.cb().png

    总结

    渲染watcher: 数据变动,重新patch
    computed watcher: 计算属性的依赖变动,watcher.dirty会置为true
    user watcher:观察的key变动,会随着 渲染watcher一起在nextTick里面走watcher.run(), 然后重新取值,再执行cb回调函数;

    他们之间有什么区别呢?
    其实从流程分析,它们的流程都是一样的
    都是要走这么一个流程

    try{
      watcher.value = watcher.get()
    } finally{
      watcher.cb()
    }
    
    

    不同点:

    • 渲染watcher的作用是watcher.get()里面的getter执行的就是patch流程,它的cb是一个noop函数,就是一个空函数 function(){}
    • computed watcher的watcher.get()里面的getter执行的就是用户定义的计算函数; 它的cb也是一个noop函数
    • user watcher的watcher.get()里面的getter只是拿被观察的key的值而已,用户定义的回调cb在拿到值之后再执行

    相关文章

      网友评论

        本文标题:彻底理解vue里面的各种watcher及其作用

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