响应式原理-侦听器
侦听器的初始化也是定义在 initState
函数中,定义在 src/core/instance/state.js
中:
function initWatch(vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key];
// 支持 watch 的同一个 key 对应多个 handler
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
createWatcher(vm, key, handler);
}
}
}
复制代码
initWatch
主要对 watcher
对象做遍历,拿到每一个 handler
调用 createWatcher
:
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);
}
复制代码
createWatcher
首先对 handler
做类型判断,然后调用 vm.$watch
,$watch
是 Vue
原型上的方法,它是在执行 stateMixin
的时候定义的:
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this;
// $watch 方法是用户可以直接调用的,它可以传递一个对象,也可以传递函数
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options);
}
options = options || {};
options.user = true;
// 这是一个 user watcher
const watcher = new Watcher(vm, expOrFn, cb, options);
if (options.immediate) {
cb.call(vm, watcher.value);
}
return function unwatchFn() {
watcher.teardown();
};
};
复制代码
也就是说,侦听器 watch
最终会调用 $watch
方法,这个方法首先判断 cb
如果是一个对象,则调用 createWatcher
方法,这是因为 $watch
方法是用户可以直接调用的,它可以传递一个对象,也可以传递函数。接着执行 const watcher = new Watcher(vm, expOrFn, cb, options)
实例化了一个 watcher
,这里需要注意一点这是一个 user watcher
。通过实例化 watcher
的方式,一旦我们 watch
的数据发送变化,它最终会执行 watcher
的 run
方法,执行回调函数 cb
,并且如果我们设置了 immediate
为 true
,则直接会执行回调函数 cb
。
所以本质上侦听器也是基于 Watcher
实现的,它是一个 user watcher
。其实 Watcher
支持了不同的类型,下面我们梳理一下它有哪些类型以及它们的作用。
deep watcher
在 watcher
执行 get
求值的过程中有一段逻辑:
get() {
let value = this.getter.call(vm, vm)
// ...
if (this.deep) {
traverse(value)
}
}
复制代码
如果为 deep watcher
则执行 traverse(value)
,traverse
定义在 src/core/observer/traverse.js
中:
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);
}
}
复制代码
它的本质就是对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter
过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher
,这个函数实现还有一个小的优化,遍历过程中会把子响应式对象通过它们的 dep id
记录到 seenObjects
,避免以后重复访问。
那么在执行了 traverse
后,我们再对 watch
的对象内部任何一个值做修改,也会调用 watcher
的回调函数了。
user watcher
在对 watcher
求值以及在执行回调函数的时候,会处理一下错误。
get() {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
},
getAndInvoke() {
// ...
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)
}
}
复制代码
computed watcher
computed watcher
为17.响应式原理-计算属性量身定制的。
sync watcher
当响应式数据发送变化后,触发了 watcher.update()
,只是把这个 watcher
推送到一个队列中,在 nextTick
后才会真正执行 watcher
的回调函数。而一旦我们设置了 sync
,就可以在当前 Tick
中同步执行 watcher
的回调函数。
update () {
if (this.computed) {
// ...
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
复制代码
总结
通过本节分析,侦听器本质上是 user watcher
,适用于观测某个值的变化去完成一段复杂的业务逻辑。同时我们又了解了 watcher
的 4
个 options
,通常我们会在创建 user watcher
的时候配置 deep
和 sync
,可以根据不同的场景做相应的配置。
参考:Vue.js 技术揭秘