携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
灵魂三问
你是否有以下疑问:
data里的变量改变之后会执行watch内同名变量内的方法, 他们是怎么绑定的?watch内的配置项:immediate及deep是怎么实现的?watch函数回调value及oldValue是怎么获取的?
本文将根据源码逐行解析watcher实现的原理, 找到答案~
简介
官网介绍如下:
通过 watch 选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。
一般来说某个业务需要在某个数据变化时去执行, 这种场景可以使用watch
watch具有两个配置项: immediate和deep immediate表示声明时立即执行一次. deep表示追踪深层次的更新: 由于对象的值是引用值, 若内部某个属性变化,引用值不会发生改变, deep的设计就是为了解决这个问题
声明方法: 选项声明以及命令式的 api
从源码中发现, watch的声明可以是一个字符串映射到method内的方法, 也可以是数组来绑定多个方法, 当数据变化的时候按顺序执行. 使用 Vue 内的 watch选项:
watch: {
// 如果声明的是一个数组, 将数据更新时按顺序执行里面的方法
firstName: [
function (val) {
console.log(val, "默认写法");
},
// 可以传入 methods 内定义的同名方法
"firstNameChange",
// 对象式的写法, 可以添加配置项
{
handler: function (val) {
console.log(val, "对象写法");
},
deep: true,
immediate: true,
},
],
},
使用命令式 api:
this.$watch('firstName', console.log, {deep: true, immediate: true})
两者比较:
- 项式的写法可以传入一个数组, 数据更新执行多个方法
- 命令式的写法则比较灵活, 可以动态的去创建
实现原理
由于选项式的声明方式本质上就是使用命令式api声明, 因此以选项式声明为例.
简单的介绍一下选项式watch的实现原理: initState => initWatch => createWatcher => Vue.$watch => new Watcher() => this.get()进行依赖收集 => 数据更新(watcher.update) => invokeWithErrorHandling执行声明时的方法 详细步骤如下:
initWatch 初始化 watch 选项
initState时若发现watch的选项式写法会执行initWatch(options.watch)方法对其进行初始化 watch声明时可以是一个数组, 若传入的是一个数组, 则遍历数组的每一项进行处理. 否则只需要处理一次
// src\core\instance\state.js
function initWatch(vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key];
/** watch 可以使用数组的写法, 触发多个函数, 因此需要为每一项都进行处理 */
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
createWatcher(vm, key, handler);
}
}
}
createWatcher 统一输出
createWatcher方法主要有三个作用:
- 处理对象声明的写法, 统一
handler执行函数的值 - 处理字符串声明的写法, 将
methods中同名方法赋给handler - 统一数据之后传给
vm.$watch
// src\core\instance\state.js
/** 该方法主要目的是为watch的回调创建一个userWatcher */
function createWatcher(
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
/** 处理对象的写法, 如 watch: {handler: function() {}, deep: true, immediate: true} */
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
/** 可以传入methods中定义的方法 */
if (typeof handler === "string") {
handler = vm[handler];
}
/**
* 本质上是调用 Vue 实例上的 $watch. 跟 this.$watch 一样
* 这里的 expOrFn 是 initWatch 中传入的 key, 其实就是声明时取的名称
* handler 是声明时定义的方法, 即改变后要执行的函数
* options 为一些配置项, 如 deep, immediate
* */
return vm.$watch(expOrFn, handler, options);
}
可以看出最后调用vm.$watch, 本质上选项式写法的内部实现也是使用命令式的api方式
vm.$watch 创建 userWatcher
这一步至关重要, 主要干了这几件事:
- 使用传入的参数创建
userWatcher - 执行依赖收集系统, 将创建的
userWatcher向依赖的数据进行订阅 - 实现了
immediate立即执行功能,invokeWithErrorHandling包装函数执行声明时的方法 - 实现了
deep功能
// src\core\instance\state.js
/** 给 Vue 实例挂载 $watch 方法, 可以通过 this.$watch 访问 */
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this;
/** 若在this.$watch 中使用对象的写法, 将重新解析再执行 */
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options);
}
options = options || {};
/** 标识符 userWatcher, 表示用户定义的 */
options.user = true;
/** 本质上也是创建一个 Watcher, 此时expOrFn是声明时传入的依赖名, cb是声明时的方法, options是声明时的参数 */
const watcher = new Watcher(vm, expOrFn, cb, options);
/** 如果配置了 immediate 属性则立即执行一次声明时的方法 */
if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`;
pushTarget();
/** 1.invokeWithErrorHandling 是一个通用异常包装函数, 执行cd函数, 若有异常则捕获并向上通知到全局
* 2.可处理生命周期, 事件等回调函数的异常捕获
*/
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info);
popTarget();
}
/** 卸载当前watch */
return function unwatchFn() {
watcher.teardown();
};
};
}
如果配置了immediate = true, 会立即执行, invokeWithErrorHandling是 vue一个通用异常包装函数, 在这里可以简单理解为执行一次声明watch时传入的方法. 这一步是实现immediate的原理
上面说到这一步进行了依赖收集(依赖收集有不懂的可以看这篇文章), 核心在于 new Watcher内部:
Watcher内部有这样一段代码:
// src\core\observer\watcher.js
/** 判断传入的 expOrFn 是不是函数, 若是函数直接赋值给 this.getter */
if (typeof expOrFn === "function") {
this.getter = expOrFn;
} else {
/**
* 如果不是函数, 则为userWatcher传入的字符串, 则 this.getter 为 parsePath 返回的方法
* parsePath 主要用来解析, 声明是有可能传入 'data.obj.name' 类似的值, 则最终的结果应该是 name 的值
* 在执行这一步的过程中, 因为访问到了data内部的响应式数据, 因此会进行依赖收集, 此时的Dep.target为该userWatch,收集者为当前响应式数据
*/
this.getter = parsePath(expOrFn);
// ...
}
// 通过this.get来触发this.getter
this.value = this.lazy ? undefined : this.get();
// get方法
get() {
pushTarget(this);
let value;
const vm = this.vm;
try {
/** 执行上面定义的this.getter, 其实就是parsePath(expOrFn)返回的方法 */
value = this.getter.call(vm, vm);
} catch (e) {
// ...
} finally {
/** 对数组和对象进行深层次的追踪, 本质上就是对每一个属性进行订阅 */
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value;
}
当 new Watcher时传入的expOrFn不是方法时, 将触发 parsePath(expOrFn)方法, 该方法其实就是获取data[expOrFn]的值. 当给this.value赋值时会执行此方法从而触发依赖收集(因为触发了响应式数据的getter)
可以看到代码中有判断this.deep, 这里是实现deep的原理, 核心在于traverse方法:
// src\core\observer\traverse.js
export function traverse (val: any) {
/** 其实是执行_traverse方法 */
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
// ...
/** 对数组进行深度追踪, 遍历后分别执行 */
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
/** 若果不是数组, 则递归访问val中的每一个对象, 触发依赖收集, 实现deep的深层次追踪. 本质上就是对当前watcher递归的向val中每个属性进行订阅 */
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
不难看出, deep的实现就是将传入的value下的每个属性都访问一遍, 从而触发依赖收集系统, 将当前watcher收集到被访问数据的订阅者列表中. 当数据被更新时, 进行发布.
数据更新触发watch声明时的方法
当依赖的数据更新时, 执行dep.notify, 所有的订阅者执行watcher.update方法, 本质上就是执行watcher.run方法, 这里只介绍userWatcher.run
// src\core\observer\watcher.js
run() {
/** 是否活跃, 卸载之后该值为false, 将不执行 */
if (this.active) {
/** this.get 其实就是执行this.getter也就是上面定义的parsePath(expOrFn), 作用为获取data[expOrFn]的值, 这里就是获取最新的值 */
const value = this.get();
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
/** 先拿到上一次的值 */
const oldValue = this.value;
/** 更新最新的值 */
this.value = value;
/** 执行用户定义watch时声明的方法 */
if (this.user) {
const info = `callback for watcher "${this.expression}"`;
invokeWithErrorHandling(
/** 声明时传入的方法 */
this.cb,
this.vm,
/** 声明时传入的值, 这里也是为什么新值在前, 旧值在后的原因 */
[value, oldValue],
this.vm,
info
);
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
}
当数据更新后, 使用this.get获取最新的值, 由于还没有给this.value赋最新值, 因此可以通过this.value获取更新前的值.然后再执行之前介绍的包装函数invokeWithErrorHandling触发声明时的方法
总结
-
在
createWatcher中对于不同的声明方式中进行统一处理, 执行vm.$watch(expOrFn, handler, options) -
在
vm.$watch中创建userWatcher,- 通过
this.get获取data[expOrFn]的值, 从而触发响应式系统, 进行依赖收集, 此时的Dep.target为当前userWatcher - 若声明时有
immediate配置项为真, 则执行包装函数invokeWithErrorHandling触发声明时的方法 - 若声明时有
deep配置项为真, 则执行traverse(value)方法, 对value内部的每个属性进行访问, 触发响应式系统进行依赖收集, 此时的Dep.target为当前userWatcher
- 通过
-
数据更新后, 触发订阅发布, 触发订阅者的更新方法, 对于
userWatcher其实就是执行invokeWithErrorHandling包装函数- 通过
this.value获取更新前的值 - 通过
this.get获取最新的值. 本质上就是调用parsePath(expOrFn) - 更新
this.value为最新的值
- 通过
至此所有问题已经得到答案~