Vue-Watcher观察者源码详解

源码调试地址

https://github.com/KingComedy/vue-debugger

什么是Watcher

  • Watcher是Vue中的观察者类,主要任务是:观察Vue组件中的属性,当属性更新时作相应的操作,即实例化时传入的回调函数
  • 在Vue的对属性做响应式处理时,会收集每个属性的依赖,即每个属性所依赖的watcher,当属性更新时,通知watcher执行更新dom操作。
  • Watcher有三种类型的,一个是计算属性computed创建的computedWatcher,一个是侦听器watch创建的userWatcher,还有一个是用于渲染更新dom的renderWatcher,一个组件只有一个renderWatcher,有多个computedWatcher和userWatcher
  • 通过Vue初始化过程可以知道,组件执行initState处理数据时,处理顺序是props => methods => data => computed => watch,然后最后在$mount阶段才实例化了渲染watcher,所以组件内watcher创建顺序是:computed Watcher => user Watcher(即监听器watcher) => renderWatcher
  • 组件所对应的watcher会存在vm._watchers数组属性中,组件对应的渲染renderWatcher存在vm._watchers

watcher的主要源码详解

Watcher主要属性初始化(并不包括所有属性)

// 主要属性初始化
new Watcher(vm, expOrFn, cb, options)
/* 
  id 是自增属性,在执行更新时会对watcher根据id进行排序,
  因为数据处理watcher的创建顺序是 computedWatcher => userWatcher => renderWatcher 
  所以组件内watcher的执行顺序也是 computedWatcher => userWatcher => renderWatcher
  这样能保证renderWatcher执行dom更新时,computed属性值是最新的
*/
watcher.id 
/* 
  expOrFn参数:key或者函数
    computedWatcher: 传的是函数, 用于获取computed属性的值。如果传的是对象,则是对象的 get
    userWatcher: 传的是属性名, 通过属性名获取所对应的函数
    renderWatcher: 传的是updateComponent即组件更新函数
  最后都会转为函数,保存在watcher.getter
 */
 watcher.expOrFn
 watcher.getter
 /* 
  value: 当前watcher的值,为执行getter函数 的结果值,
    computedWatcher: getter函数执行的值
    userWatcher:所对应属性的值
    renderWatcher: undefined
 */
 watcher.value
/* 
  cb参数:
    computedWatcher和renderWatcher: 传的是空函数
    userWatcher: 传的是回调函数
 */
 watcher.cb 
 /* 
  lazy是在options参数里
  只有computedWatcher的lazy和dirty属性是true
    lazy为true,watcher实例化时,不执行get函数
    dirty是对观察的属性做标记,可以理解为观察的该属性是否是脏数据,即做过改变,如果为true,则需要重新获取属性的值,如果为false,则读取缓存的值
  */
 watcher.lazy
 watcher.dirty
 /* 
  只有userWatcher的user属性是true
   表示是用户手动传入的回调函数,因此在执行cb回调函数时,要try、catch捕获异常
  */
  watcher.user

总结:

  • 1、组件内部watcher的创建顺序和执行顺序都为 computedWatcher => userWatcher => renderWatcher
  • 2、computedWatcher的lazy和dirty属性都为true,lazy用于标识是否在创建watcher时执行watcher.get函数,dirty用于标识数据是否需要重新获取,true为重新获取,false则读取缓存
  • 3、userWatcher的user属性为true,执行cb回调函数时会用try/catch执行捕获异常

Watcher主要函数

// 主要代码
/* 
  get函数触发时机:除了computedWatcher外,userWatcher和renderWatcher实例化时都会执行一次get函数
  get函数执行流程:
    1、将Dep类的静态属性target设置为当前watcher,并推入存储watcher的栈中
    2、然后执行getter函数。
      computedWatcher:执行getter函数,其实就是执行computed属性的对应的函数或者get,如果执行的函数里,有依赖到其他属性,这时就会建立其他属性和当前computedWatcher的依赖关系
      userWatcher:执行getter方法,其实就是获取当前属性的值,并设置当前userWatcher.value
      renderWatcher: 执行updateComponent函数,即执行render和patch,在render阶段时,如果读取到组件里的属性,依旧会触发属性的get,即同时与renderWatcher建立依赖关系
    3、将Dep类的静态属性target设置为当前watcher,并退出存储watcher的栈
 */
get () {
  // 设置Dep.target = this
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    // 如果是监听器 或者调用$watch,需要捕获异常
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    popTarget()
  }
  return value
}
/* 
update函数触发时机:watcher所观察的属性 触发更新
update函数执行流程:
  1、如果this.lazy为true,即当前watcher属于computedWatcher,只是设置dirty属性
  2、如果this.sync, 执行run函数
  3、否则将当前watcher入队,后面在异步更新时,会遍历执行watcher的run方法
*/
update () {
  /* istanbul ignore else */
  // computed
  
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    // 同步执行
    this.run()
  } else {
    // 进入异步刷新队列
    queueWatcher(this)
  }
}
/* 
run函数触发时机是 vue执行异步更新时,会遍历触发watcher的run函数
run函数执行流程:
  1、执行get函数
  2、如果是userWatcher,要执行cb 回调函数
*/
run () {
  const value = this.get()
  if (value !== this.value || isObject(value) || this.deep) {
    // set new value
    const oldValue = this.value
    this.value = value
    // this.user 表示是用户手动调用的watcher,如组件的watch, 需要加 捕获异常
    if (this.user) {
      try {
        this.cb.call(this.vm, value, oldValue)
      } catch (e) {
        handleError(e, this.vm, `callback for watcher "${this.expression}"`)
      }
    } else {
      this.cb.call(this.vm, value, oldValue)
    }
  }
}

总结:

  • watcher.get函数作用一是获取对应属性的值,二是与观察的属性建立关系的过程
  • 不同watcher执行的get:computedWatcher执行get会执行属性所对应的函数,同时与依赖属性的dep建立关系,并返回值。userWatcher即会通过属性名,获取到组件的属性的值。renderWatcher则是执行updateComponent函数,首次执行是初始化和挂载组件,后面则是执行组件更新
  • 当属性改变时,执行的是watcher的update。除了computedWatcher外,userWatcher和renderWatcher都会进入异步刷新队列,即执行queueWatcher(this)
  • userWatcher和renderWatcher执行更新,最终会执行watcher.run函数
  • watcher.run函数主要执行 get函数 和 cb回调函数。因为renderWatcher的cb传的是空函数,所以renderWatcher的run主要还是执行get函数,即更新函数updateComponent。userWatcher则需要先执行get函数获取到新的值,并传入cb回调函数

computed源码详解

computed源码详解(主要源码在src\core\instance\state.js里)

  • 如果computed属性存在,执行initComputed(在src\core\instance\state.js里定义)
  • 创建watcher为一个空对象,用于存储每个computed属性的watcher,存于组件的_computedWatchers属性
  • 遍历computed的每个属性
    • 为每个属性创建一个对应的watcher,并且lazy设置为true,即不立即执行属性的get, 并且watcher.dirty 也为true。dirty相当于标记当前属性脏数据,即是否已发生变化,是的话要重新读取,不是的话读取缓存
    • 检验每个属性 是否与 props 或者 data里的属性重复
    • 如果没有重复,对computed里的每个属性做响应式处理,如果属性是一个函数,则属性的set为空函数(所以手动设置computed属性的值是无效的),属性的get为createComputedGetter(key)返回的函数computedGetter(在src\core\instance\state.js定义)。如果属性是对象,即手动设置的set,get,则属性set可以生效,get则会判断对象的cache的属性,如果没有关闭缓存,则还是createComputedGetter(key)返回的函数computedGetter
  • 在组件render阶段时,读取到computed里的属性时,执行computedGetter
    • 通过属性名,在组件的_computedWatchers属性,获取当前属性对应的watcher
    • 如果watcher.dirty属性为true,调用watcher.evaluate(),因为首次读取属性时,dirty是true,所以首次获取值时会调用watcher.evaluate
    • evaluate会触发当前属性的get,即会执行传入的回调函数,并将dirty设置为false。
    • 先执行computed的回调函数,如果回调函数里有依赖了其他属性如this.a和this.b,则又会触发a和b属性的get,则会将当前computed属性的computedWatcher收集到a,b属性的依赖收集dep里,当a,b属性发生更新时,它们的dep会执行computedWatcher的update,将computedWatcher的dirty设置为true,表示数据已改变,需要重新获取,而不是读取缓存。
    • a,b发生更新,页面重新render时,又需要读取computed的属性值时,即再次执行computedGetter,此时dirty为true,则触发watcher的get,即重新执行下回调函数,返回最新的值,如果dirty为false,则返回旧的watcher的值

 举个栗子

computed: {
  keyValue () {
    return this.count + this.key
  }
},
  • 当computed初始化时,会为keyValue创建一个computedWatcher,并将函数作为computedWatcher的回调函数,并设置computedWatcher的lazy和dirty为true,并为keyValue重写set、get方法。set为空函数,get则会执行computedGetter
  • 当render阶段时,读取到keyValue值时,触发属性的get,即执行computedGetter
  • 因为此时dirty为true,所以会执行computedWatcher.evaluate()函数
    • evaluate中会执行computedWatcher.get(),即执行computedWatcher的回调函数,在执行keyValue函数时,又会触发count和key属性的get,会将当前computedWatcher实例收集到 count和key属性的依赖收集器dep里
    • 执行完computedWatcher.get后,会将computedWatcher.dirty设置为false,如果下次组件重新render时,dirty还是false,则读取缓存的值,即原来computedWatcher.value
  • 在keyValue的computedWatcher 与 count和key属性的dep建立关系后,如果count或者key发生更新,会执行computedWatcher.update,主要是将dirty设置会true,然后再读取keyValue时,就又会执行computedWatcher.evaluate()函数了

 

watch源码详解

watch属性初始化源码详解

  • initWatch(vm, opts.watch)(在src\core\instance\state.js文件下)
  • 遍历watch的所有属性,根据key取出属性对应的值handler
    • 如果handler是数组,则遍历数组,执行createWatcher(如下面这种用法,vue2文档并没有, 一般也没这么使用)
    • watch: {
         'count': [(n) => {
           console.log(n)
         }, () => {
           console.log(n)
         }]
       }

       

    • handler如果不是,则直接执行createWatcher(vm, key, handler)
    • createWatcher函数会对handler进行处理为函数,因为handler可能为函数、对象、字符串。
    • createWatcher主要源码:
    • function createWatcher (
        vm: Component,
        expOrFn: string | Function,
        handler: any,
        options?: Object
      ) {
        // watch有三种形式:对象、方法、字符串
        /*  如:
          watch: {
            count: {
              handler: (newVal, oldVal) {},
              deep: true,
              immediate: true
            },
            'key'(n) {},
            'key': 'method'
          } 
        */
        if (isPlainObject(handler)) {
          options = handler
          handler = handler.handler
        }
        if (typeof handler === 'string') {
          handler = vm[handler]
        }
        // 最终会调用vm.$watch(expOrFn, handler, options)。expOrFn为key,handler为已经被处理的函数,deep和immediate属性会存在options中
        return vm.$watch(expOrFn, handler, options)
        }

       

$watch源码详解

主要源码:

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    // 如果cb参数是对象,还是调用createWatcher
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    // 标记为userWatcher
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    // 如果immediate为true,立即执行一次回调函数
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

 

总结

3中watcher区别

如何区分不同的watcher:

  • lazy 为true 则为computedWatcher
  • user 为true 则为userWatcher
  • getter 为updateComponent 则为renderWatcher

 

初始化过程

  • computedWatcher和userWatcher是每个属性都会创建对应的watcher,而renderWatcher一个组件只会创建一个
  • computedWatcher实例化时不会执行get函数,即还没有收集依赖。userWatcher实例化时,会执行get函数收集依赖,如果immediate为true会立即执行cb回调函数。renderWatcher实例化时,即执行updateComponent,开始组件的render和挂载

dep和3种watcher是怎么建立关系的

  • 执行watcher.get函数
  • pushTarget(watcher):将Dep.target设置为当前watcher
  • 执行watcher.getter,watcher.getter执行期间,只要有读取到属性(已经做过响应式处理的属性),就会建立属性与watcher依赖收集
  • 依赖收集过程:
    • 属性的get函数 会执行dep.depend()
    • dep.depend() 会将执行当前Dep.target即watcher的addDep(dep)函数,并传入当前dep
    • watcher.addDep:会将dep存入newDeps实例属性上,并调用dep.addSub(watcher),并传入watcher
    • dep.addSub:会将传入的watcher存入当前dep的subs实例属性上,然后dep和watcher就互相建立好联系
  • watcher.getter执行结束后,会调用popTarget,将Dep.target设置为空

 

 

组件更新时watcher怎么执行更新

  • 属性更新时,会遍历执行dep中watcher的update函数
  • computedWatcher:computedWatcher执行update会将dirty设置为true,表示需要重新获取属性的值。当其他地方需要读取当前属性的值时,就会重新执行watcher.get,获取最新的值
  • userWatcher:执行update会将当前watcher存入异步更新队列,最后在刷新队列的时候会执行watcher.run函数,即先执行get函数 获取属性最新的值,然后将新旧值传入cb,执行cb回调函数
  • renderWatcher:执行update会将当前watcher存入异步更新队列,在刷新队列前会将watcher队列进行排序,renderWatcher会在最后执行,所以是所有的computedWatcher和userWatcher执行完后,才会执行renderWatcher,即主要是执行get,updateComponent函数,执行组件的更新

相关联文章

renderWatcher执行组件的挂载和更新的过程,可以参考:https://blog.csdn.net/comedyking/article/details/115670343

watcher队列执行异步更新的过程。可以参考:https://blog.csdn.net/comedyking/article/details/117702133

 

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值