Vue 给我们提供了侦听器方法vm.$watch和选项watch,用来观察 Vue 实例变化,然后执行一些操作。
下面看下它们是怎么实现的,其实它们都是基于观察者watcher的封装。
vm.$watch#
先看Vue.prototype.$watch方法的代码,在core/instance/state.js文件中。
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
// ...
};首先看下$watch方法的参数,有三个参数,分别是expOrFn、cb和options,我们发现这三个参数对应着Watcher构造函数的第二个到第四个参数。
继续看下面的代码。
const vm: Component = this;
// ! cb 是对象,调用 createWatcher 方法
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options);
}
// ...声明常量vm存储当前的实例对象,即this。然后判断cb是不是一个普通对象,如果是的话,调用createWatcher函数,传入vm以及$watch原来的三个参数。
createWatcher#
先看下createWatcher函数的代码。
// ! 创建 watcher 的工厂函数
function createWatcher(
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
// ! 传入的 handler 是对象时
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler; // ! 获取对象里的 handler 方法作为 cb
}
// ! 传入的 handler 是字符串时 => watch: { name: 'cbName' }
if (typeof handler === "string") {
handler = vm[handler]; // ! 获取相同名字的函数作为 cb
}
return vm.$watch(expOrFn, handler, options); // ! 最后调用实例的 $watch 方法
}这个函数主要处理在$watch方法中传入的cb参数不是函数的情况,它的第三个参数handler对应$watch的第三个参数cb。
这里再次判断handler是不是一个普通对象,如果是的话,函数的第四个参数options缓存第三个参数handler的值,而第三个参数handler的值为它的handler属性,即回调函数。
然后判断handler是不是字符串,如果是的话,handler的值为实例对象的handler属性,即去实例对象获取名为handler的方法作为回调函数。
最后再次调用vm.$watch方法,并把handler作为函数的第二个参数即回调函数cb传入。
小结,createWatcher函数就是通过不是函数类型的cb得到函数类型的handler,然后再次调用vm.$watch方法。经过这个方法处理后,vm.$watch方法的第三个参数cb就一定是函数。
然后又回到wm.$watch方法中,继续看下面的代码。
options = options || {};
options.user = true; // ! 设置 user 为 true,说明是用户自定义的观察者
const watcher = new Watcher(vm, expOrFn, cb, options); // ! 创建观察者实例,并传入设置的参数
// ! 如果设置了 immediate
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(); // ! 移除 watcher
};重新定义options为传入的options参数或者是一个空对象(没传入参数)。
设置options.user属性为true,说明下面创建的观察者对象是用户定义的观察者,然后通过new创建一个观察者实例对象,把当前的组件实例vm和$mount方法的三个参数依次传入。
判断options选项中immediate属性中是否为真,如果是的话,立即执行一次回调函数cb,使用call执行,函数指向当前的实例对象,并传入参数watcher.value,即观察者的新值。
$watch方法最后返回一个函数,这个函数是用来取消观察,删除观察者的。函数调用观察者的teardown方法。
看下teardown方法的代码,在core/observer/watcher.js文件中。
teardown() {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false // ! 设置观察者实例是非激活状态
}
}判断active是否为真,即判断观察者是否是激活状态,如果是的话,再判断观察者所属实例对象是否已被销毁,如果没有销毁的话,把这个观察者从实例对象中的_watchers属性中删除,即销毁它。
然后通过递减观察者deps的长度的方式遍历deps,在deps里面存在观察者的dep里删除观察者,最后把观察者的active属性设为false,表示观察者是非激活状态,这样观察者的回调函数就不会执行了。
示例对象的$watch方法示例代码。
// cb 是函数时
vm.$watch(
"name",
() => {
console.log("change");
},
{ immediate: true }
);
// cb 是对象时
vm.$watch("name", {
handler: () => {
console.log("change");
},
immediate: true,
});watch#
我们可以使用vm.$watch观察 Vue 实例变化,也可以使用watch选项做相同的事情。watch选项是一个对象,对象的键是需要观察的数据,值是对应的回调函数或者处理器对象,也可以是已经声明的方法的名称。
在 Vue 的实例对象初始化时,通过遍历watch选项中,然后创建观察者。
查看initWatch函数的代码,在core/instance/state.js文件中。
// ! 初始化 watch 选项
function initWatch(vm: Component, watch: Object) {
// ! 遍历 watch
for (const key in watch) {
const handler = watch[key]; // ! 获取 handler
// ! 如果 handler 是数组(多个观察者),遍历数组后分别创建 Watcher 实例
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
createWatcher(vm, key, handler);
}
}
}遍历watch选项,通过key获取到它的值handler。
然后判断handler是不是数组,如果是数组的话,再遍历一次,在每次循环的过程中,调用createWatcher函数创建观察者。如果不是数组的话,直接调用createWatcher函数创建观察者。
通过createWatcher函数处理各种类型的handler,包括函数、对象、字符串,最后创建观察者。
组件中watch选项的示例代码,这里使用官方文档的例子。
var vm = new Vue({
data: {
a: 1,
b: 2,
c: 3,
d: 4,
e: {
f: {
g: 5,
},
},
},
watch: {
a: function (val, oldVal) {
console.log("new: %s, old: %s", val, oldVal);
},
// 方法名
b: "someMethod",
// 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
c: {
handler: function (val, oldVal) {
/* ... */
},
deep: true,
},
// 该回调将会在侦听开始之后被立即调用
d: {
handler: "someMethod",
immediate: true,
},
// 多个 watcher
e: [
"handle1",
function handle2(val, oldVal) {
/* ... */
},
{
handler: function handle3(val, oldVal) {
/* ... */
},
/* ... */
},
],
// watch vm.e.f's value: {g: 5}
"e.f": function (val, oldVal) {
/* ... */
},
},
});
vm.a = 2; // => new: 2, old: 1深度监听#
在观察者的get方法中,如果this.deep的值为true会进行深度监听,深度监听的目标是对象或者数组。
get() {
try {
// ...
} catch (e) {
//...
} finally {
// ! 如果是深度观察 => 用于 watch 选项
if (this.deep) {
traverse(value) // ! 递归去访问 value,触发它所有子项的 getter
}
// ...
}
}通过traverse函数去递归访问value的值,触发它所有子项的getter。
看下traverse函数的代码。
const seenObjects = new Set(); // ! 存储 id 值的集合
export function traverse(val: any) {
_traverse(val, seenObjects);
seenObjects.clear();
}调用_traverse方法,把val和seenObjects集合传入,最后调用seenObjects对象的clear方法清空seenObjects集合的元素。
再看下_traverse函数的代码。
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;
}
// ...
}先声明变量i和keys。再声明常量isA,isA是用来判断传入的参数val是否是数组。
判断val的值的类型,如果val是非数组非对象类型,或者是不可扩展的,或者是一个VNode,直接返回。
继续看下面的代码。
if (val.__ob__) {
const depId = val.__ob__.dep.id;
// ! 已经遍历的对象,不会再遍历,避免循环引用
if (seen.has(depId)) {
return;
}
seen.add(depId);
}判断val是否存在__ob__属性,即它是一个响应式对象,如果是的话,声明常量depId存储从val.__ob__属性获取到的dep的id,这个id是dep的唯一标识。
判断集合seen中是否存在depId,如果存在,直接返回,这说明dep已经收集过依赖,不需要再重复收集。如果seen中不存在depId,把depId添加到集合seen中。
继续看下面的代码。
// ! 数组
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,收集依赖。