官方定义
- 类型:
{ [key: string]: string | Function | Object | Array }
- 详细:
一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个属性。
初次探索
我们的意图是 —— 监测app
这个变量,并在函数中打下一个断点。
我们期待的是 —— 断点停下后,调用栈中出现相关的函数,提供我们分析watch
原理的依据。
抱着上面的意图以及期待,我们新建一个Vue
项目,同时写入以下代码:
created () {
this.app = 233
},
watch: {
app (val) {
debugger
console.log('val:', val)
}
}
刷新页面后右边的调用栈显示如下👇:
app
run
flushSchedulerQueue
anonymous
flushCallbacks
timeFunc
nextTick
queueWatcher
update
notify
reactiveSetter
proxySetter
created
- …
看到需要经过这么多的调用过程,不禁心里一慌… 然而,如果你理解了上一篇关于computed
的文章,你很容易就能知道:
Vue
通过对变量进行
依赖收集,进而在变量的值变化时进行消息提醒。最后,依赖该变量的
computed
最后决定需要重新计算还是使用缓存
computed
跟watch
还是有些相似的,所以在看到reactiveSetter
的时候,我们心中大概想到,watch
一定也利用了依赖收集。
为什么执行了queueWatcher
单看调用栈的话,这个watch
过程中执行了queueWatcher
,这个函数是放在update
中的
update
的实现👇:
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
显然,queueWatcher
函数是否调用,取决于这两个变量:
this.lazy
this.sync
这两个变量实际上是在Watcher
类里初始化的,所以在这里打下断点,下面直接给出调用顺序👇:
initWatch
createWatcher
Vue.$watch
Watcher
initWatch
👇
function initWatch (vm, watch) {
// 遍历watch属性
for (var key in watch) {
var handler = watch[key];
// 如果是数组,那么再遍历一次
if (Array.isArray(handler)) {
for (var i = 0; i < handler.length; i++) {
// 调用createWatcher
createWatcher(vm, key, handler[i]);
}
} else {
// 同上
createWatcher(vm, key, handler);
}
}
}
createWatcher
👇
function createWatcher (
vm,
expOrFn,
handler,
options
) {
// 传值是对象时重新拿一次属性
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
// 兼容字符类型
if (typeof handler === 'string') {
handler = vm[handler];
}
return vm.$watch(expOrFn, handler, options)
}
Vue.prototype.$watch
👇
Vue.prototype.$watch = function (
expOrFn,
cb,
options
) {
var vm = this;
// 如果传的cb是对象,那么再调用一次createWatcher
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {};
options.user = true;
// 新建一个Watcher的实例
var watcher = new Watcher(vm, expOrFn, cb, options);
// 如果在watch的对象里设置了immediate为true,那么立即执行这个它
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
的初始化过程比较简单,光看上面给的注释也是足够清晰的了。当然,前面提到的this.lazy
和this.sync
变量,由于在初始化过程中没有传入true
值,那么在update
触发时直接走入了queueWatcher
函数
深入研究
queueWatcher
的实现
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
function queueWatcher (watcher) {
var id = watcher.id;
// 判断是否已经在队列中,防止重复触发
if (has[id] == null) {
has[id] = true;
// 没有刷新队列的话,直接将wacher塞入队列中排队
if (!flushing) {
queue.push(watcher);
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
// 如果正在刷新,那么这个watcher会按照id的排序插入进去
// 如果已经刷新了这个watcher,那么它将会在下次刷新再次被执行
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
// queue the flush
// 排队进行刷新
if (!waiting) {
waiting = true;
// 如果是开发环境,同时配置了async为false,那么直接调用flushSchedulerQueue
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue();
return
}
// 否则在nextTick里调用flushSchedulerQueue
nextTick(flushSchedulerQueue);
}
}
}
queueWatcher
是一个很重要的函数,从上面的代码我们可以提炼出一些关键点👇
- 对
watcher.id
做去重处理,对于同时触发queueWatcher
的同一个watcher
,只push
一个进入队列中 - 一个异步刷新队列(
flashSchedulerQueue
)在下一个tick
中执行,同时使用waiting
变量,避免重复调用 - 如果在刷新阶段触发了
queueWatcher
,那么将它按id
顺序从小到大的方式插入到队列中;如果它已经刷新过了,那么它将在队列的下一次调用中立即执行
如何理解在刷新阶段触发queueWatcher
的操作?
其实理解这个并不难,我们将断点打入flushSchedulerQueue
中,这里只列出简化后的代码👇
function flushSchedulerQueue () {
currentFlushTimestamp = getNow();
flushing = true;
var watcher, id;
...
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
watcher.run();
...
}
...
}
其中两个关键的变量:
fluashing
has[id]
都是在watcher.run()
之前变化的。这意味着,在对应的watch
函数执行前/执行时(此时处于刷新队列阶段),其他变量都能在这个刷新阶段重新加入到这个刷新队列中
最后放上完整的代码:
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
currentFlushTimestamp = getNow();
flushing = true;
var watcher, id;
// 刷新之前对队列做一次排序
// 这个操作可以保证:
// 1. 组件都是从父组件更新到子组件(因为父组件总是在子组件之前创建)
// 2. 一个组件自定义的watchers都是在它的渲染watcher之前执行(因为自定义watchers都是在渲染watchers之前执行(render watcher))
// 3. 如果一个组件在父组件的watcher执行期间刚好被销毁,那么这些watchers都将会被跳过
queue.sort(function (a, b) { return a.id - b.id; });
// 不对队列的长度做缓存,因为在刷新阶段还可能会有新的watcher加入到队列中来
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
// 执行watch里面定义的方法
watcher.run();
// 在测试环境下,对可能出现的死循环做特殊处理并给出提示
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1;
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? ("in watcher with expression \"" + (watcher.expression) + "\"")
: "in a component render function."
),
watcher.vm
);
break
}
}
}
// 重置状态前对activatedChildren、queue做一次浅拷贝(备份)
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();
// 重置定时器的状态,也就是这个异步刷新中的has、waiting、flushing三个变量的状态
resetSchedulerState();
// 调用组件的 updated 和 activated 钩子
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
// deltools 的钩子
if (devtools && config.devtools) {
devtools.emit('flush');
}
}
nextTick
异步刷新队列(flushSchedulerQueue
)其实是在nextTick
中执行的,这里我们简单分析下nextTick
的实现,具体代码如下👇
// 两个参数,一个cb(回调),一个ctx(上下文对象)
function nextTick (cb, ctx) {
var _resolve;
// 把毁掉函数放入到callbacks数组里
callbacks.push(function () {
if (cb) {
try {
// 调用回调
cb.call(ctx);
} catch (e) {
// 捕获错误
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) { // 如果cb不存在,那么调用_resolve
_resolve(ctx);
}
});
if (!pending) {
pending = true;
timerFunc();
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
我们看到这里其实还调用了一个timeFunc
函数(偷个懒,这段代码的注释就不翻译了🤣)👇
var timerFunc;
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
timerFunc = function () {
p.then(flushCallbacks);
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) { setTimeout(noop); }
};
isUsingMicroTask = true;
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
var counter = 1;
var observer = new MutationObserver(flushCallbacks);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true
});
timerFunc = function () {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Techinically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = function () {
setImmediate(flushCallbacks);
};
} else {
// Fallback to setTimeout.
timerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}
timerFunc
的代码其实很简单,无非是做了这些事情:
检查浏览器对于
Promise
、MutationObserver
、setImmediate
的兼容性,并按优先级从大到小的顺序分别选择Promise
MutationObserver
setImmediate
setTimeout
- 在支持
Promise
/MutationObserver
的情况下便可以触发微任务(microTask
),在兼容性较差的时候只能使用setImmediate
/setTimeout
触发宏任务(macroTask
)
当然,关于宏任务(macroTask
)和微任务(microTask
)的概念这里就不详细阐述了,我们只要知道,在异步任务执行过程中,在同一起跑线下,微任务(microTask
)的优先级永远高于宏任务(macroTask
)。
tips
- 全局检索其实可以发现
nextTick
这个方法被绑定在了Vue
的原型上👇
Vue.prototype.$nextTick = function (fn) {
return nextTick(fn, this)
};
-
nextTick
并不能被随意调起👇
if (!pending) {
pending = true;
timerFunc();
}
总结
-
watch
跟computed
一样,依托于Vue
的响应式系统 - 对于一个异步刷新队列(
flushSchedulerQueue
),刷新前 / 刷新后都可以有新的watcher
进入队列,当然前提是nextTick
执行之前 - 与
computed
不同的是,watch
并不是立即执行的,而是在下一个tick
里执行,也就是微任务(microTask
) / 宏任务(macroTask
)