继上篇我们分析了computed的实现原理后,计算属性实现原理请猛戳这里,本篇文章我们来分析一下watch侦听属性的实现原理。
watch
应用场景: 适用于观测某个值的变化去完成一段复杂的业务逻辑。
特点:
- 本质上是一个
user watcher。 - 使用方式:
// 1.使用watch选项
watch: {
name(nv, ov) { // 传递函数
console.log(nv, ov)
},
age: { // 传递对象
handler(nv, ov) {
console.log(nv, ov)
},
deep: true,
immediate: true
},
sex: [ // 传递数组
function handle (nv, ov) {/* ... */},
function handle2 (nv, ov) {/* ... */}
],
job: 'someMethod' // 传递方法名
hobby: {
handler: 'someMethod',
immediate: true
},
'province.city': function (nv, ov) { /* ... */ }
},
// 2. vm.$watch(expOrFn, callback, [options])
vm.$watch('a', (newVal, oldVal) => { // 键路径
console.log(nv, ov)
}, { // 传入配置项
deep: true,
immediate: true
})
vm.$watch(() => this.a, (nv, ov) => { // this.a变化后,会执行监听函数
console.log(nv, ov)
}, {
deep: true,
immediate: true
})
3.vm.$watch 返回一个取消观察函数,用来停止触发回调:
var unwatch = vm.$watch('a', cb) unwatch() // unwatch执行后取消观察
初始化流程
watch的初始化也是在initState函数中,在initComputed后,会执行initWatch函数。
function initState (vm: Component) {
...,
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initWatch函数接收到用户定义的配置项,根据用户传入的回调函数的类型(数组、对象、函数等),内部会调用createWatcher函数。
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key] // 拿到用户定义的回调函数
if (Array.isArray(handler)) { // handler为数组
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else { // handler为对象或者函数
createWatcher(vm, key, handler)
}
}
}
createWatcher主要做了以下工作:
1.参数序列化: 如果handler是对象,先用options保存住handler,通过handler.handler拿到用户定义的回调函数
如果handler为字符串,会通过vm.字符串获取到定义在vm实例上的methods方法,并赋值给handler。
2.调用vm.$watch方法。
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') { // handler是一个字符串的方法
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
$watch方法是定义在Vue原型上的方法,我们来看一下它的实现:
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) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info) // cb.apply(context, args)
popTarget()
}
return function unwatchFn () {
watcher.teardown()
}
}
由于用户会主动调用vm.$watch,第二个参数cb可能会传递对象,如果是对象,会再次调用createWatcher序列化参数,然后再次调用vm.$watch。拿到options选项,新增了user为true,实例化 user watcher。如果用户传递了immediate选项,会立刻执行一次回调函数,最后返回了一个卸载watcher的函数,用户执行这个函数,会把user watcher从subs集合中移除,从而停止侦听操作。
让我们通过一个demo去看一下user watcher的执行逻辑:
<div id="app">
<button @click="change">{{ a }}</button> // 点击按钮a的值由{b: 2} -> {b: 3}
</div>
<script>
const vm = new Vue({
el: '#app',
data() {
return {
a: {
b: 2
}
}
},
watch: {
a: {
handler(newVal, oldVal) {
console.log(newVal, oldVal)
},
deep: true
}
},
methods: {
change() {
this.a.b = 3
}
}
})
</script>
由于要观测的a的配置项为对象,调用createWatcher时,options保留了这个对象,调用$watch时,为options新增user为true的属性,然后调用new Watcher去实例化 user watcher,我们看一下实例化user watcher时传入的参数:
new Watcher(vm, 'a', handler(newVal, oldVal) {console.log(newVal, oldVal)}, {
handler(newVal, oldVal) {
console.log(newVal, oldVal)
},
deep: true,
user: true
})
// watcher内部执行逻辑
class Watcher {
constructor (
vm: Component,
expOrFn: string | Function, // 'a'
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (options) {
this.deep = !!options.deep // true
this.user = !!options.user // true
}
this.cb = cb
this.active = true
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else { // 由于expOrFn为字符串'a',所以走else逻辑
this.getter = parsePath(expOrFn)
/**
this.getter = function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]] // 实际访问了vm.a
}
return obj
}
*/
}
this.value = this.lazy ? undefined : this.get()
}
get () {
pushTarget(this) // targetStack.push(target) Dep.target = user watcher
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm) // 让updateComponent函数执行
} catch() {} finally {
if (this.deep) { // true
traverse(value)
}
popTarget()
...
}
return value
}
}
function parsePath (path: string): any { // 'a'
const segments = path.split('.') // ['a']
return function (obj) { // this.getter会调用该函数,并传入参数vm
for (let i = 0; i < segments.length; i++) { // 闭包的经典用用
if (!obj) return
obj = obj[segments[i]] // 实际访问了vm.a
}
return obj
}
}
parsePath首先会把传入的key通过split拆分为一个数组,然后返回了一个匿名函数,并将其赋值给this.getter,当我们调用watcher.get函数时,实际上会做2件事:
- 先把
Dep.target指向user watcher,并将其添加到targetStack中。 - 执行
this.getter.call(vm, vm), 把vm作为参数传入。此时会执行这个匿名函数,通过闭包的形式拿到先前切割好的数组(此例中segments实际是['a']),遍历数组,通过vm去访问数组里每一项(vm.a),由于a是响应式数据,会触发a的依赖收集,此时a对应的subs集合中就收集当前的user watcher作为依赖,并把vm.a的求值结果{b: 2}返回。
由于我们传递了deep属性,执行完this.getter函数后,会触发traverse({b: 2})函数。我们来看一下traverse函数做了那些工作:
function traverse (val: any) { // {b: 2}
_traverse(val, seenObjects)
}
function _traverse (val: any, seen: SimpleSet) {
...,
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val))) {
return
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val) // ['b']
i = keys.length // 1
while (i--) _traverse(val[keys[i]], seen) // 访问'b',触发子对象的依赖收集
}
}
traverse 的逻辑也很简单,它实际上就是对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher。在执行了 traverse 后,我们再对 watch 的对象内部任何一个值做修改,也会调用 watcher 的回调函数了。
执行完traverse逻辑后,会执行popTarget函数。该函数会执行targetStack.pop()操作,由于targetStack目前只有一项,执行pop操作后就变为了空数组,然后重置了dep.target的指向(Dep.target = targetStack[targetStack.length - 1])为undefined。
初始化完成后会执行挂载($mount)逻辑,此时会实例化render watcher,然后执行render watcher的getter方法,也就是我们之前一直介绍的updateComponent方法。先执行pushTarget(targetStack.push(render watcher) Dep.target = render watcher),执行vm_render时,由于render函数中包含有对响应式数据a的访问,此时会触发a的依赖收集,此时a对应的subs列表中会在新增一项为render watcher。
// 模板中访问了a
<button @click="change">{{ a }}</button>
// 渲染watcher的getter函数
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
对 deep watcher 的理解非常重要,如果大家观测了一个复杂对象,并且会改变对象内部深层某个值的时候也希望触发回调,一定要设置 deep 为 true,但是因为设置了 deep 后会执行 traverse 函数,会有一定的性能开销,所以一定要根据应用场景权衡是否要开启这个配置。
更新流程
当我们对a做修改后,首先会先触发set拦截,调用dep.notify,拿到收集的subs集合遍历,拿到每个watcher,依次执行watcher.update,queueWatcher,nextTick(flushSchedulerQueue)后,最终会执行到watcher.run方法。我们先看一下此时user watcher的执行逻辑:
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value || isObject(value) || this.deep
) {
const oldValue = this.value
this.value = value
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方法(再次执行this.getter函数),重新去求一个最新的值,如果新旧值不一致,会执行用户传入的cb,并把新旧值传给cb函数。这就是我们观测一个数据,能拿到最新值和上一个值的原因。
执行完user watcher的更新后,继续执行render watcher的更新流程,此时会在次调用updateComponent函数,生成最新的Vnode,patch生成最新的dom。
如果我们传递了immediate参数,会立即执行一次cb函数,并把第一次计算的值传入。
最后返回了一个 unwatchFn 方法,它会调用 watcher.teardown 方法去移除这个 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) { // 立即执行一次cb函数
cb.call(vm, watcher.value)
}
return function unwatchFn () {
watcher.teardown()
}
}
class Watcher {
...,
teardown () {
if (this.active) {
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this) // 从subs中移除当前watcher
}
this.active = false
}
}
}
手绘流程图如下:
总结: 侦听属性本质是一个user watcher,通过支持传入不同的配置项(deep、immediate),使用起来会更加灵活,至此侦听属性分析完毕。