Vue
中$nextTick
方法將回調延遲到下次DOM
更新循環以後執行,也就是在下次DOM
更新循環結束以後執行延遲迴調,在修改數據以後當即使用這個方法,可以獲取更新後的DOM
。簡單來講就是當數據更新時,在DOM
中渲染完成後,執行回調函數。javascript
經過一個簡單的例子來演示$nextTick
方法的做用,首先須要知道Vue
在更新DOM
時是異步執行的,也就是說在更新數據時其不會阻塞代碼的執行,直到執行棧中代碼執行結束以後,纔開始執行異步任務隊列的代碼,因此在數據更新時,組件不會當即渲染,此時在獲取到DOM
結構後取得的值依然是舊的值,而在$nextTick
方法中設定的回調函數會在組件渲染完成以後執行,取得DOM
結構後取得的值即是新的值。css
<!DOCTYPE html> <html> <head> <title>Vue</title> </head> <body> <div id="app"></div> </body> <script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { msg: 'Vue' }, template:` <div> <div ref="msgElement">{{msg}}</div> <button @click="updateMsg">updateMsg</button> </div> `, methods:{ updateMsg: function(){ this.msg = "Update"; console.log("DOM未更新:", this.$refs.msgElement.innerHTML) this.$nextTick(() => { console.log("DOM已更新:", this.$refs.msgElement.innerHTML) }) } }, }) </script> </html>
官方文檔中說明,Vue
在更新DOM
時是異步執行的,只要偵聽到數據變化,Vue
將開啓一個隊列,並緩衝在同一事件循環中發生的全部數據變動,若是同一個watcher
被屢次觸發,只會被推入到隊列中一次。這種在緩衝時去除重複數據對於避免沒必要要的計算和DOM
操做是很是重要的。而後,在下一個的事件循環tick
中,Vue
刷新隊列並執行實際工做。Vue
在內部對異步隊列嘗試使用原生的Promise.then
、MutationObserver
和setImmediate
,若是執行環境不支持,則會採用 setTimeout(fn, 0)
代替。
Js
是單線程的,其引入了同步阻塞與異步非阻塞的執行模式,在Js
異步模式中維護了一個Event Loop
,Event Loop
是一個執行模型,在不一樣的地方有不一樣的實現,瀏覽器和NodeJS
基於不一樣的技術實現了各自的Event Loop
。瀏覽器的Event Loop
是在HTML5
的規範中明肯定義,NodeJS
的Event Loop
是基於libuv
實現的。
在瀏覽器中的Event Loop
由執行棧Execution Stack
、後臺線程Background Threads
、宏隊列Macrotask Queue
、微隊列Microtask Queue
組成。html
setTimeout
、setInterval
、XMLHttpRequest
等等的執行線程。setTimeout
、setInterval
、setImmediate(Node)
、requestAnimationFrame
、UI rendering
、I/O
等操做Promise
、process.nextTick(Node)
、Object.observe
、MutationObserver
等操做當Js
執行時,進行以下流程vue
// Step 1 console.log(1); // Step 2 setTimeout(() => { console.log(2); Promise.resolve().then(() => { console.log(3); }); }, 0); // Step 3 new Promise((resolve, reject) => { console.log(4); resolve(); }).then(() => { console.log(5); }) // Step 4 setTimeout(() => { console.log(6); }, 0); // Step 5 console.log(7); // Step N // ... // Result /* 1 4 7 5 2 3 6 */
// 執行棧 console // 微隊列 [] // 宏隊列 [] console.log(1); // 1
// 執行棧 setTimeout // 微隊列 [] // 宏隊列 [setTimeout1] setTimeout(() => { console.log(2); Promise.resolve().then(() => { console.log(3); }); }, 0);
// 執行棧 Promise // 微隊列 [then1] // 宏隊列 [setTimeout1] new Promise((resolve, reject) => { console.log(4); // 4 // Promise是個函數對象,此處是同步執行的 // 執行棧 Promise console resolve(); }).then(() => { console.log(5); })
// 執行棧 setTimeout // 微隊列 [then1] // 宏隊列 [setTimeout1 setTimeout2] setTimeout(() => { console.log(6); }, 0);
// 執行棧 console // 微隊列 [then1] // 宏隊列 [setTimeout1 setTimeout2] console.log(7); // 7
// 執行棧 then1 // 微隊列 [] // 宏隊列 [setTimeout1 setTimeout2] console.log(5); // 5
// 執行棧 setTimeout1 // 微隊列 [then2] // 宏隊列 [setTimeout2] console.log(2); // 2 Promise.resolve().then(() => { console.log(3); });
// 執行棧 then2 // 微隊列 [] // 宏隊列 [setTimeout2] console.log(3); // 3
// 執行棧 setTimeout2 // 微隊列 [] // 宏隊列 [] console.log(6); // 6
在瞭解異步任務的執行隊列後,回到中$nextTick
方法,當用戶數據更新時,Vue
將會維護一個緩衝隊列,對於全部的更新數據將要進行的組件渲染與DOM
操做進行必定的策略處理後加入緩衝隊列,而後便會在$nextTick
方法的執行隊列中加入一個flushSchedulerQueue
方法(這個方法將會觸發在緩衝隊列的全部回調的執行),而後將$nextTick
方法的回調加入$nextTick
方法中維護的執行隊列,在異步掛載的執行隊列觸發時就會首先會首先執行flushSchedulerQueue
方法來處理DOM
渲染的任務,而後再去執行$nextTick
方法構建的任務,這樣就能夠實如今$nextTick
方法中取得已渲染完成的DOM
結構。在測試的過程當中發現了一個頗有意思的現象,在上述例子中的加入兩個按鈕,在點擊updateMsg
按鈕的結果是3 2 1
,點擊updateMsgTest
按鈕的運行結果是2 3 1
。java
<!DOCTYPE html> <html> <head> <title>Vue</title> </head> <body> <div id="app"></div> </body> <script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { msg: 'Vue' }, template:` <div> <div ref="msgElement">{{msg}}</div> <button @click="updateMsg">updateMsg</button> <button @click="updateMsgTest">updateMsgTest</button> </div> `, methods:{ updateMsg: function(){ this.msg = "Update"; setTimeout(() => console.log(1)) Promise.resolve().then(() => console.log(2)) this.$nextTick(() => { console.log(3) }) }, updateMsgTest: function(){ setTimeout(() => console.log(1)) Promise.resolve().then(() => console.log(2)) this.$nextTick(() => { console.log(3) }) } }, }) </script> </html>
這裏假設運行環境中Promise
對象是徹底支持的,那麼使用setTimeout
是宏隊列在最後執行這個是沒有異議的,可是使用$nextTick
方法以及自行定義的Promise
實例是有執行順序的問題的,雖然都是微隊列任務,可是在Vue
中具體實現的緣由致使了執行順序可能會有所不一樣,首先直接看一下$nextTick
方法的源碼,關鍵地方添加了註釋,請注意這是Vue2.4.2
版本的源碼,在後期$nextTick
方法可能有所變動。git
/** * Defer a task to execute it asynchronously. */ var nextTick = (function () { // 閉包 內部變量 var callbacks = []; // 執行隊列 var pending = false; // 標識,用以判斷在某個事件循環中是否爲第一次加入,第一次加入的時候才觸發異步執行的隊列掛載 var timerFunc; // 以何種方法執行掛載異步執行隊列,這裏假設Promise是徹底支持的 function nextTickHandler () { // 異步掛載的執行任務,觸發時就已經正式準備開始執行異步任務了 pending = false; // 標識置false var copies = callbacks.slice(0); // 建立副本 callbacks.length = 0; // 執行隊列置空 for (var i = 0; i < copies.length; i++) { copies[i](); // 執行 } } // 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 if */ if (typeof Promise !== 'undefined' && isNative(Promise)) { var p = Promise.resolve(); var logError = function (err) { console.error(err); }; timerFunc = function () { p.then(nextTickHandler).catch(logError); // 掛載異步任務隊列 // 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); } }; } else if (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 IE11, iOS7, Android 4.4 var counter = 1; var observer = new MutationObserver(nextTickHandler); var textNode = document.createTextNode(String(counter)); observer.observe(textNode, { characterData: true }); timerFunc = function () { counter = (counter + 1) % 2; textNode.data = String(counter); }; } else { // fallback to setTimeout /* istanbul ignore next */ timerFunc = function () { setTimeout(nextTickHandler, 0); }; } return function queueNextTick (cb, ctx) { // nextTick方法真正導出的方法 var _resolve; callbacks.push(function () { // 添加到執行隊列中 並加入異常處理 if (cb) { try { cb.call(ctx); } catch (e) { handleError(e, ctx, 'nextTick'); } } else if (_resolve) { _resolve(ctx); } }); //判斷在當前事件循環中是否爲第一次加入,如果第一次加入則置標識爲true並執行timerFunc函數用以掛載執行隊列到Promise // 這個標識在執行隊列中的任務將要執行時便置爲false並建立執行隊列的副本去運行執行隊列中的任務,參見nextTickHandler函數的實現 // 在當前事件循環中置標識true並掛載,而後再次調用nextTick方法時只是將任務加入到執行隊列中,直到掛載的異步任務觸發,便置標識爲false而後執行任務,再次調用nextTick方法時就是一樣的執行方式而後不斷如此往復 if (!pending) { pending = true; timerFunc(); } if (!cb && typeof Promise !== 'undefined') { return new Promise(function (resolve, reject) { _resolve = resolve; }) } } })();
回到剛纔提出的問題上,在更新DOM
操做時會先觸發$nextTick
方法的回調,解決這個問題的關鍵在於誰先將異步任務掛載到Promise
對象上。
首先對有數據更新的updateMsg
按鈕觸發的方法進行debug
,斷點設置在Vue.js
的715
行,版本爲2.4.2
,在查看調用棧以及傳入的參數時能夠觀察到第一次執行$nextTick
方法的實際上是因爲數據更新而調用的nextTick(flushSchedulerQueue);
語句,也就是說在執行this.msg = "Update";
的時候就已經觸發了第一次的$nextTick
方法,此時在$nextTick
方法中的任務隊列會首先將flushSchedulerQueue
方法加入隊列並掛載$nextTick
方法的執行隊列到Promise
對象上,而後纔是自行自定義的Promise.resolve().then(() => console.log(2))
語句的掛載,當執行微任務隊列中的任務時,首先會執行第一個掛載到Promise
的任務,此時這個任務是運行執行隊列,這個隊列中有兩個方法,首先會運行flushSchedulerQueue
方法去觸發組件的DOM
渲染操做,而後再執行console.log(3)
,而後執行第二個微隊列的任務也就是() => console.log(2)
,此時微任務隊列清空,而後再去宏任務隊列執行console.log(1)
。
接下來對於沒有數據更新的updateMsgTest
按鈕觸發的方法進行debug
,斷點設置在一樣的位置,此時沒有數據更新,那麼第一次觸發$nextTick
方法的是自行定義的回調函數,那麼此時$nextTick
方法的執行隊列纔會被掛載到Promise
對象上,很顯然在此以前自行定義的輸出2
的Promise
回調已經被掛載,那麼對於這個按鈕綁定的方法的執行流程即是首先執行console.log(2)
,而後執行$nextTick
方法閉包的執行隊列,此時執行隊列中只有一個回調函數console.log(3)
,此時微任務隊列清空,而後再去宏任務隊列執行console.log(1)
。
簡單來講就是誰先掛載Promise
對象的問題,在調用$nextTick
方法時就會將其閉包內部維護的執行隊列掛載到Promise
對象,在數據更新時Vue
內部首先就會執行$nextTick
方法,以後便將執行隊列掛載到了Promise
對象上,其實在明白Js
的Event Loop
模型後,將數據更新也看作一個$nextTick
方法的調用,而且明白$nextTick
方法會一次性執行全部推入的回調,就能夠明白其執行順序的問題了,下面是一個關於$nextTick
方法的最小化的DEMO
。github
var nextTick = (function(){ var pending = false; const callback = []; var p = Promise.resolve(); var handler = function(){ pending = true; callback.forEach(fn => fn()); } var timerFunc = function(){ p.then(handler); } return function queueNextTick(fn){ callback.push(() => fn()); if(!pending){ pending = true; timerFunc(); } } })(); (function(){ nextTick(() => console.log("觸發DOM渲染隊列的方法")); // 註釋 / 取消註釋 來查看效果 setTimeout(() => console.log(1)) Promise.resolve().then(() => console.log(2)) nextTick(() => { console.log(3) }) })();
https://github.com/WindrunnerMax/EveryDay
https://www.jianshu.com/p/e7ce7613f630 https://cn.vuejs.org/v2/api/#vm-nextTick https://segmentfault.com/q/1010000021240464 https://juejin.im/post/5d391ad8f265da1b8d166175 https://juejin.im/post/5ab94ee251882577b45f05c7 https://juejin.im/post/5a45fdeb6fb9a044ff31c9a8