类型:{ [key: string]: string | Function | Object | Array }
javascript
详细:前端
一个对象,键是须要观察的表达式,值是对应回调函数。值也能够是方法名,或者包含选项的对象。Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每个属性。java
咱们的意图是 —— 监测app
这个变量,并在函数中打下一个断点。
咱们期待的是 —— 断点停下后,调用栈中出现相关的函数,提供咱们分析watch
原理的依据。react
抱着上面的意图以及期待,咱们新建一个Vue
项目,同时写入如下代码:express
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
中的app
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
)。
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
)扫描下方的二维码或搜索「tony老师的前端补习班」关注个人微信公众号,那么就能够第一时间收到个人最新文章。