准备
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();
}
这边要注意的有两行代码:
this.getter = parsePath(expOrFn);
parsePath
这个方法可以用于解析对象的.
语法,如:expOrFn
传入的是"a.b.c"
这样一个字符串,parsePath
就返回一个函数赋值给this.getter
,用于寻找this.a.b.c
的值。
以及:
this.value = this.lazy ? undefined : this.get();
直接调用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
理解起来就非常简单了。