nextTick原理
nextTick的作用是在DOM更新后执行回调,可以用来获取状态改变后更新的DOM节点。
光是言语比较难以理解,可以用一个例子解释。
<div> <h3 id="a">{{a}}</h3> </div> { data(){ return { a: 1 } } mounted(){ this.a = 10 console.log(document.querySelector('#a').innerText); // 1 } }
打印的结果是1而不是更新后的10,这是因为状态的改变并不会立即触发DOM重新渲染,这是出于性能的考虑,频繁地更新DOM会导致大量的性能浪费。
当状态改变后,watcher会得到通知,然后触发虚拟DOM的渲染流程,但是watcher触发渲染这个操作是异步的,它会将渲染函数放到一个队列中,在同一个事件循环中,所有要重新的渲染的渲染函数都会放到这个队列中,而这个队列会利用微任务或宏任务让队列中的函数在下个事件循环周期中执行。
关于事件循环,可以看事件循环
Vue会首先使用Promise实现将异步更新队列延迟到下个事件循环周期执行的目的,如果Promise不可用,则会采用回退方案,一直回退到使用setTimeout(宏任务)。
接下来我们来看下代码,在原来的代码中添加
debugger
mounted() { this.a = 10 console.log(document.querySelector('#a').innerText); debugger; this.$nextTick(() => { console.log('a', this.a); }) },
然后步进到
$nextTick
函数内部Vue.prototype.$nextTick = function (fn) { return nextTick(fn, this); };
可以看到vm.$nextTick函数其实内部使用了nextTick函数,然后我们步进到这个函数内。
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) { _resolve(ctx); } }); // pending是一个标识 if (!pending) { pending = true; timerFunc(); } // $flow-disable-line if (!cb && typeof Promise !== "undefined") { return new Promise(function (resolve) { _resolve = resolve; }); } }
callbacks就是那个异步更新队列,可以看到,Vue将
nextTick
的回调cb包装了一层,然后加入到异步更新队列中。而pending是一个标识,作用是避免更新队列被多次加到事件循环任务队列中,如果为true,则表示异步更新队列已经被加入到微任务队列中,如果为false,则会执行timerFunc函数将队列加入到事件循环任务队列中去。
const callbacks = []; let pending = false; // 更新队列执行函数 function flushCallbacks() { pending = false; // 用一个额外的数组存放要执行的函数,防止在执行时又有函数到队列中执行。 const copies = callbacks.slice(0); callbacks.length = 0; for (let i = 0; i < copies.length; i++) { copies[i](); } }
而imerFunc函数的作用是将异步更新队列加入到微任务队列或者宏任务队列中去,Vue会首先使用Promise,不行则会进行回退处理。
if (typeof Promise !== "undefined" && isNative(Promise)) { const p = Promise.resolve(); timerFunc = () => { 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) let counter = 1; const observer = new MutationObserver(flushCallbacks); const textNode = document.createTextNode(String(counter)); observer.observe(textNode, { characterData: true, }); timerFunc = () => { 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. timerFunc = () => { setImmediate(flushCallbacks); }; } else { // Fallback to setTimeout. timerFunc = () => { setTimeout(flushCallbacks, 0); }; }