Vue源码分析

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

2021-10-25  本文已影响0人  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()
}

不同点:

上一篇下一篇

猜你喜欢

热点阅读