响应式原理-侦听器
侦听器的初始化也是定义在 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 技术揭秘