背景
今天看到MutationObserver,突然想起了Vue.nextTick的回退方案里使用到了它
但明明MutationObserver是监听DOM元素的方法,尽管它回调函数是推入到微任务队列里的,但还是不明白Vue.nextTick是如何借助它实现微任务的
故特地看了一下源码
Vue.nextTick常被用来获取DOM重新渲染后的数据
官网是这么讲的
异步更新队列
可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的
Promise.then
、MutationObserver
和setImmediate
,如果执行环境不支持,则会采用setTimeout(fn, 0)
代替。例如,当你设置
vm.someData = 'new value'
,该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用Vue.nextTick(callback)
。
我把整个nextTick的源码摘了过来并大概翻译了一下注释,版本为[2.6.0, 3.0.0)(也加上了一些个人理解)
在版本为(?, 2.5.9]版本中,MutationObserver的方案是MessageChannel来代替的,可移至window.MessageChannel——虫洞般的存在了解
如果只想看MutationObserver相关代码的话,请点击传送门
/* */
var isUsingMicroTask = false; // 用于标记是否使用了微任务
var callbacks = []; // nextTick的事件队列
var pending = false;
function flushCallbacks () {
pending = false;
var copies = callbacks.slice(0); // 拷贝一份callbacks
callbacks.length = 0; // 清空callbacks
for (var i = 0; i < copies.length; i++) {
copies[i](); // 执行callbacks中所有的事件
}
}
// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
/**
* 意思是在Vue2.5版本用了宏任务和微任务的混合方案有一定问题,比如out-in模式的transition
* 在事件处理的时候用宏任务也会有一些无法规避的奇怪现象
* 所以在之后的版本任何地方都尽量使用微任务
* 但有时候微任务优先级过高也会有一些问题
*/
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 */
/**
* nextTick使用了微任务,尽量先使用原生Promise然后才是MutationObserver
* 原因是:虽然MutationObserver兼容性更高,但是它在iOS >= 9.3.3触发多次touch事件的时候失效了
* PS:nextTick有回退方案,在运行环境不支持微任务的情况会回退到宏任务
* 回退方案是:原生Promise -> MutationObserver -> setImmediate -> setTimeout
*/
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.
/**
* 虽然在某些有问题的UIWebViews上,使用Promise会出现回调函数推入微任务队列但是微任务队列不清空的奇怪情况
* 但我们可以通过推入一个空的宏任务来强迫浏览器执行微任务队列
*/
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
});
/**
* 创建并监听一个TextNode,在timerFunc里修改TextNode的值
* 变相利用MutationObserver监听DOM元素的方式来创建微任务
*/
timerFunc = function () {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
/**
* 理论上来说它是宏任务,但依旧是个比setTimeout更好的选择
*/
timerFunc = function () {
setImmediate(flushCallbacks);
};
} else {
// Fallback to setTimeout.
timerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}
function nextTick (cb, ctx) {
var _resolve;
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
timerFunc();
}
// $flow-disable-line
/**
* 用于这样的调用方式(2.1.0起新增)
* 作为一个Promise使用
* Vue.nextTick().then(function() {
* // DOM更新了
* })
*/
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
MutationObserver相关源码
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);
};
可以看到Vue是这样利用MutationObserver实现主动创建微任务的
创建并监听一个TextNode,在timerFunc里修改TextNode的值
变相利用MutationObserver监听DOM元素的方式来创建微任务
方法其实很简单,并没有我想得那么复杂,但也很巧妙
既然这个API是只能监听变化的,那我就主动创造变化让它监听,化被动为主动
希望以后自己多多思考,锻炼自己的创造力和解决问题的能力