准备
vue版本号2.6.12,为方便分析,选择了runtime+compiler版本。
回顾
如果有感兴趣的同学可以看看我之前的源码分析文章,这里呈上链接:《Vue源码分析系列:目录》
写在前面
相信记过前几章节对源码的学习,我们都对Dep、Watcher的运作有了深入的了解。特别是上一章节《源码解析:computed》,对这两个类的理解更加的深入了。这次我们要学习的是watch属性的运作过程,理解了computed属性的运作过程后,watch属性的运作原理理解起来就非常简单了。
watch初始化
watch初始化在_init中:initState(vm)。initState中又有这样一段代码:
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
进入initWatch
initWatch
function initWatch(vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key];
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
createWatcher(vm, key, handler);
}
}
}
遍历vm.$options.watch,如果watch[key]是个数组,就遍历数组,调用createWatcher;否则就直接调用createWatcher。
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);
}
这个方法主要是对watch的一个规范化。毕竟watch在定义的时候既可以是一个函数,也可以是一个对象option,option.handler才是真正的回调。
经过一些标准化处理后最后调用的是原型上的一个API:$watch
$watch
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) {
try {
cb.call(vm, watcher.value);
} catch (error) {
handleError(
error,
vm,
`callback for immediate watcher "${watcher.expression}"`
);
}
}
return function unwatchFn() {
watcher.teardown();
};
};
由于$watch是一个Vue的API,所以既可以是Vue使用者调用的,也可能是Vue内部调用的,所以这里又对cb和option做了一次规范化。
然后option.user标注为true,表示这是一个用户定义的watch。
实例化了一个Watcher,传入expOrFn(这里是要监控的变量名称,如:要对data中的foo监控,这里的expOrFn就是"foo"),cb(监控值变化后要执行的回调函数),options(Watcher的配置)。
先不急进入Watcher的实例化查看逻辑,先继续往下看。
如果options.immediate为true,也就是立即执行这个watch,直接就调用一次回调,并传入最新值。
最后返回了一个函数,主要是用于销毁watch。
好了,现在我们进入user watcher的实例化。
watcher.contructor
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// options
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.lazy = !!options.lazy;
this.sync = !!options.sync;
this.before = options.before;
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
this.cb = cb;
this.id = ++uid; // uid for batching
this.active = true;
this.dirty = this.lazy; // for lazy watchers
this.deps = [];
this.newDeps = [];
this.depIds = new Set();
this.newDepIds = new Set();
this.expression =
process.env.NODE_ENV !== "production" ? expOrFn.toString() : "";
// parse expression for getter
if (typeof expOrFn === "function") {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
process.env.NODE_ENV !== "production" &&
warn(
`Failed watching path: "${expOrFn}" ` +
"Watcher only accepts simple dot-delimited paths. " +
"For full control, use a function instead.",
vm
);
}
}
//computed不会立刻求值
this.value = this.lazy ? undefined : this.get();
}
这边要注意的有两行代码:
parsePath这个方法可以用于解析对象的.语法,如:expOrFn传入的是"a.b.c"这样一个字符串,parsePath就返回一个函数赋值给this.getter,用于寻找this.a.b.c的值。
以及:
直接调用this.get()。
进入this.get。
watcher.get
get() {
//改变Dep.target为当前的Wathcer实例
pushTarget(this);
let value;
const vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`);
} else {
throw e;
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
//归还上一次的Watcher实例
popTarget();
//清除无用的watcher
this.cleanupDeps();
}
return value;
}
首先一上来直接调用pushTarget改变了Dep.target的值为当前的user watcher。
之后调用了this.getter,也就是刚刚使用parsePath解析出来的函数去寻找this上的值,此时这个值有可能是computed也有可能是data。
还记得在defineReactive中设置了数据的响应式吗?
这时的user watcher去获取实例上的值,就会触发defineReactive中设置的get函数,实例上那个值的依赖收集器——Dep实例立刻就会将当前的Dep.target(也就是user watcher)收集到他的列表中!当这个值一发生改变,立刻调用defineReactive中的set,立刻通知user watcher去触发this.update。
我们等等再看this.update,我们先继续向下看this.get。
如果option.deep为true,使用traverse。option.deep这个配置是在监控对象、数组是使用的,可以深度监听对象、数组中所有值的变化。
进入traverse。
traverse
const seenObjects = new Set()
export function traverse (val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
}
创建了一个集合seenObjects,调用_traverse。
_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
}
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)
}
}
先判断传入的是不是一个数组。然后是防止重复调用_traverse的操作。(这里我有一个疑问,明明Set就是一个集合,本身就可以防止重复,为什么这里还需要判断if (seen.has(depId)) return?)最后是一个分支:
- 如果是一个数组,就遍历数组,递归调用
_traverse,传入数组中的每个元素(触发一次get)。 - 如果是个对象,就遍历对象的键,递归调用
_traverse传如对象的每个键的值(触发一次get)。
每次递归都需要访问对象、数组中的数据,所以每次递归都会触发对象、数组中的元素的get,所以都会触发收集Dep.target,也就是user watcher。
到这里,user watcher的初始化说完了,但是还少了user watcher触发更新的过程。触发user watcher的更新是在依赖调用了自身的set方法时,会调用dep.notify去遍历自身依赖列表,逐个触发上面的update方法。
watcher.update
update() {
/* istanbul ignore else */
if (this.lazy) {
//如果是个computed watcher
//将数据设置为 脏 状态,需要进行更新
this.dirty = true;
} else if (this.sync) {
//如果是同步watcher,直接就执行回调
this.run();
} else {
//其他情况的watcher就放在watcher队列,在下一个Tick去行回调
queueWatcher(this);
}
}
这边有三个分支:
- 如果是一个懒
watcher,也就是computed watcher,就设置当前watcher为脏状态。 - 如果是一个
sync watcher,直接就调用run,也就是调用回调函数 - 除了以上两种情况,都会调用
queueWatcher,将当前watcher放入watcher待更新队列,在下一个Tick中执行,详情请看:响应式原理篇:nextTick
总结
watch属性的使用方法有两种,一种是函数体,一种是配置体,都可以创建user watcher去监控响应的属性。其实理解了computed的运作过程,watch理解起来就非常简单了。