初始化过程
侦听属性的初始化也是发生在 Vue 的实例初始化阶段的 initState
函数中,在 computed 初始化之后
,执行了:
1 | if (opts.watch && opts.watch !== nativeWatch) { |
看一下 initWatch
的实现,它的定义在 src/core/instance/state.js
中:
1 | function initWatch(vm, watch) { |
这里就是对 watch 对象做遍历,拿到每一个 handler
,因为 Vue 是支持 watch 的同一个key 对应多个 handler
,所以如果 handler
是一个数组,则遍历这个数组,调用 createWatcher
方法,否则直接调用 createWatcher
:
1 | function createWatcher ( |
对 hanlder
的类型做判断,拿到它最终的回调函数,最后调用 vm.$watch(keyOrFn, handler, options)
函数
1 | Vue.prototype.$watch = function ( |
最后调用 vm.$watch(keyOrFn, handler, options)
函数,$watch
是 Vue原型
上的方法,它是在执行 stateMixin
的时候定义的:
1 | function stateMixin(Vue) { |
侦听属性 watch 最终会调用 $watch
方法:
- 判断
cb
如果是一个对象,则调用createWatcher 方法
,这是因为$watch
方法是用户可以直接调用的,它可以传递一个对象,也可以传递函数。 - 接着执行
const watcher = new Watcher(vm, expOrFn, cb, options)
实例化了一个 user watcher,其中会进行依赖收集
,稍后再展开分析。 - 通过实例化
watcher
的方式,一旦我们 watch 的数据发生变化,它最终会执行watcher
的run 方法
,执行回调函数 cb
- 如果我们设置了
immediate
为true
,则直接会执行回调函数 cb
。 - 最后返回了一个
unwatchFn
方法,它会调用teardown
方法去移除这个watcher
依赖收集
1 | var vm = new Vue({ |
当执行到 $watch
中的 const watcher = new Watcher(vm, expOrFn, cb, options)
逻辑时:
1 | var Watcher = function Watcher( |
重点关注一下对 expOrFn
的处理:
在本例中我们的 expOrFn
是 'obj.msg'
,因此会调用 parsePath
方法并将返回值作为 this.getter
。
1 | var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/; |
- 首先对
path
进行合法性校验
,如果不合法
则返回undefined
- 将
path
按.
分割,比如本例中obj.msg
将得到['obj', 'msg']
- 返回一个函数,这个函数会保存为
this.getter
在实例化 user watcher
的最后调用 this.get()
进行求值时,Dep.target
是当前的 user watcher
,然后又执行了 this.getter.call(vm, vm)
,在这里函数里将遍历['obj', 'msg']
,依次访问:
vm.obj
,这会触发obj.__ob__.dep
和obj dep
两个dep
的依赖收集。obj.msg
,这会触发msg dep
的依赖收集
因此对于 user watcher
来说,其 deps
中也保存了三者的 dep
deep options
如果我们想对一下对象做深度观测的时候,需要设置 deep
这个属性为 true
。
这样就创建了一个 deep watcher
了,在 watcher
执行 get
求值的过程中有一段逻辑:
1 | get() { |
在对 watch 的表达式求值后,会调用 traverse
函数,它的定义在 src/core/observer/traverse.js
中:
1 | const seenObjects = new Set() |
traverse
的逻辑也很简单,它实际上就是对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程
,这样就可以收集到依赖
,也就是订阅它们变化的 watcher
,这个函数实现还有一个小的优化,遍历过程中会把子响应式对象通过它们的 dep id
记录到 seenObjects
,避免以后重复访问。
那么在执行了 traverse 后,我们再对 watch 的对象内部任何一个值做修改,也会调用 watcher 的回调函数了。
派发更新
在这个例子中,当我们改变 vm.watcher
或者 vm.watcher.msg
的时候,都会触发相应的 setter
,最后会执行 watcher.run
1 |
Dep.prototype.notify
-> subs[i].update()
-> queueWatcher(this)
-> nextTick(flushSchedulerQueue)
-> watcher.run()
;
1 | Watcher.prototype.run = function run() { |
总结:通过这两章的分析我们对计算属性和侦听属性的实现有了深入的了解,计算属性本质上是 computed watcher
,而侦听属性本质上是 user watcher
。
就应用场景而言,计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。