Vue源码学习8-响应式原理-watch

  • 内容
  • 评论
  • 相关

前言

前面的文章已经讲过了 render-watcher 和 computed-watcher,如果能轻易的理解前两种 watcher ,本篇介绍的 user-watcher 真的是非常的简单,代码不多,逻辑简单,跟着源码走一边就清楚了

--以下所有代码来自Vue 2.6.11 版本

--以下的vm都代指vue实例

initWatch

在Vue中我们有几种不同使用 watch 的方式,具体怎么写的看文档

不管用那种方式,它们底层的逻辑都是一样的

我们先来看看第一种方式,配置项声明的方式 ,它的初始化逻辑是在 initWatch 中发生的,它的初始化发生在 initComputed 之后,所有它能监听 data/props 也能监听 computed   示例代码:

{
  data:{
    name1:'Tifa',
    name2:'Cloud',
    person:{
      name:'Aerith'
    }
  },
  watch:{
    name1:function(newVal){
      console.log(newVal);
    },
    name2:[.....],
    person:{
      deep:true,
      callback
    }
  }
}

1:进入initWatch 方法中(src/core/instance/state.js),就是一个简单的遍历,然后循环执行 createWatcher 方法(handler 可以是一个 function 也可以是一个 Array,还可以是一个对象)

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

2:进入 createWatcher(src/core/instance/state.js),走到最后我们发现,内部其实调用了 vm.$watch,也就是Vue的检测器提供的第二种写法了,那接下来只需要梳理 $watch 的逻辑就OK了

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

3:找到 $watch 定义的地方(还是在这里面 src/core/instance/state.js),代码不多,逻辑也很简单

options.user = true  //这里标识 Watcher 为user-watcher

接着就是熟悉的 new Watcher 的过程了,这次的入参也是有些许的不同

4:进入构造函数中,可以看到这一次的 this.cb = cb ,cb不再是空函数了,就是我们自己写的监听函数

走到 if(typeof expOrFn === 'function') 判断逻辑,expOrFn 不在是function而是一个字符串,parsePath 是用来解析这个字符串(代码贴在了下方),具体怎么解析的就自己细看了

5:走到构造函数的最后,还是熟悉的 get 方法,压栈-依赖收集-出栈 一系列操作,user-watcher 的 依赖收集在此完成,最后,watcher.value 会赋值为被监听的字段的值,以上面的示例代码为例,name1 的侦听器对应的 user-watcher,它的 watcher.value = 'Tifa'

6:走完 new Watcher ,回到 $watch ,可以看到一个很熟悉的字段:immediate,在Vue文档中它代表着初始化完成后会立即执行侦听器的回调函数

7:最后返回一个方法,这个方法是用来销毁当前 user-watcher 的,至于teardown 方法的具体逻辑就自行查看就行了,它执行完之后,这个user-watcher的所有依赖都会清除掉,包括 user-watcher 本身最后也会被浏览器的 垃圾回收 给清除掉

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  options.user = true
  const watcher = new Watcher(vm, expOrFn, cb, options)
  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()
  }
}

const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
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
  }
}

派发更新

这里的派发更新指的是 user-watcher 触发侦听函数的过程,它和 render-watcher 类似,当收到 Dep 的 notify 后都会执行 watcher.update 方法,然后被加入到下一个事件循环中,也就是说 侦听器也是异步触发的,它们最后都会走到 watcher.run 方法
进入 Watcher的run查看

1:get 取值,顺便开启新一轮 依赖收集,完事儿之后进入 下面的 if 逻辑

2:前面在讲 render-watcher 的时候,我们这里的 if 判断是进不去的,因为这里是给 user-watcher 使用的

3:接着就是触发回调函数的逻辑,也就是用户自己定义的 侦听回调函数,入参为 newVal 和 oldVal

至此,一个简单的 user-watcher 的逻辑就走完了,真的挺简单的。

deep

watch 的 deep 这个字段平时开发可能用的比较少,但难免会遇到有需求的时候,他是干嘛的呢?复制一段 Vue 的文档,再贴一段示例代码

为了发现对象内部值的变化,可以在选项参数中指定 deep: true。注意监听数组的变更不需要这么做。

我们监听的 person 对象,但是当它内部的name发生变化的时候,callback也被触发了,这就是deep的功能

vm.$watch('person', callback, {
  deep: true
});
vm.person.name = 'Aerith';

接下来看看源码是怎么做的

打开 Watcher 的源码看到 get 方法,可以很明显的看到一段 this.deep 的判断逻辑,然后执行 traverse 方法

1:进入这个方法中(源码如下,src/core/observer/traverse.js)

2:seenObjects 是一个类型为 Set 的空集合,_traverse 是一个用来递归操作的方法

3:我们以上面那段 深度监听的代码为例,此时的val就是那个 person 对象({name:'Aerith'}),第一个终止程序的逻辑可以跳过了

4:此时 val 是一个带有 __ob__ 标记的响应式对象,所有第二个 if 逻辑是可以进去的,就是一个简单的排除重复的操作,

5:接着就是第三个 if 逻辑(这里是重点),这里 val 不管是Array还是Object类型,都是差不多的一个遍历递归逻辑,然后注意了,这里会有个获取值的操作  val[i] 或者 val[keys[i]],取值就意味着会触发 Object.defineProperty 的 get 逻辑进行依赖收集,正好我们的 user-watcher 在 get 方法的时候已经压栈了,所以当我们递归获取 person 对象的所有值的时候,person 对象下的每一个 dep 都会被 user-watcher 收集建立依赖。至此,深度侦听的原理就已经搞清楚了,当 person 对象下的每一个字段被改变时,都会对这个 user-watcher 发出通知。

export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}

总结

data、props、computed、watch,render-watcher,computed-watcher,user-watcher 这些东西已经说讲完了,Vue的响应式原理的内容到这里基本也就完了

评论

0条评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注