Vue中之nextTick函式原始碼分析
1. 什麼是Vue.nextTick()?
官方文件解釋如下:
在下次DOM更新迴圈結束之後執行的延遲迴調。在修改資料之後立即使用這個方法,獲取更新後的DOM。
2. 為什麼要使用nextTick?
<!DOCTYPE html> <html> <head> <title>演示Vue</title> <script src="https://tugenhua0707.github.io/vue/vue1/vue.js"></script> </head> <body> <div id="app"> <template> <div ref="list"> {{name}} </div> </template> </div> <script> new Vue({ el: '#app', data: { name: 'aa' }, mounted() { this.updateData(); }, methods: { updateData() { var self = this; this.name = 'bb'; console.log(this.$el.textContent); // aa this.$nextTick(function(){ console.log(self.$el.textContent); // bb }); } } }); </script> </body> </html>
如上程式碼 在頁面檢視上顯示bb,但是當我在控制檯列印的時候,獲取的文字內容還是 aa,但是使用 nextTick後,獲取的文字內容就是最新的內容bb了,因此在這種情況下,我們可以使用nextTick函式了。
上面的程式碼為什麼改變this.name = 'bb';後,再使用console.log(this.$el.textContent);列印的值還是aa呢?那是因為設定name的值後,DOM還沒有更新到,所以獲取值還是之前的值,但是我們放到nextTick函式裡面的時候,程式碼會在DOM更新後執行,因此DOM更新後,再去獲取元素的值就可以獲取到最新值了。
理解DOM更新:在VUE中,當我們修改了data中的某一個值後,並不會立即反應到該el中,vue將對更改的資料放到watcher的一個非同步佇列中,只有在當前任務空閒時才會執行watcher佇列任務,這就有一個延遲時間,因此放到 nextTick函式後就可以獲取該el的最新值了。如果我們把上面的nextTick
改成setTimeout也是可以的。
3. Vue原始碼詳解之nextTick(原始碼在 vue/src/core/util/env.js)
在理解nextTick原始碼之前,我們先來理解下 html5中新增的 MutationObserver的API,它的作用是用來監聽DOM變動的介面,它能監聽一個dom物件發生的子節點刪除,屬性修改,文字內容修改等等,具體使用看我這邊部落格(http://www.cnblogs.com/tugenhua0707/articles/6849948.html).
nextTick原始碼如下:
export const nextTick = (function () { const callbacks = [] let pending = false let timerFunc function nextTickHandler () { pending = false; /* 之所以要slice複製一份出來是因為有的cb執行過程中又會往callbacks中加入內容,比如$nextTick的回撥函式裡又有$nextTick, 那麼這些應該放入到下一個輪次的nextTick去執行,所以拷貝一份,遍歷完成即可,防止一直迴圈下去。 */ const copies = callbacks.slice(0) callbacks.length = 0 for (let 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 */ /* nextTick行為利用了microtask佇列, 先使用 Promise.resolve().then(nextTickHandler)來將非同步回撥 放入到microtask中,Promise 和 MutationObserver都可以使用,但是 MutationObserver 在IOS9.3以上的 WebView中有bug,因此如果滿足第一項的話就可以執行,如果沒有原生Promise就用 MutationObserver。 */ if (typeof Promise !== 'undefined' && isNative(Promise)) { var p = Promise.resolve() var logError = err => { console.error(err) } timerFunc = () => { 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 /* 建立一個MutationObserver,observe監聽到DOM改動之後執行的回撥 nextTickHandler */ var counter = 1 var observer = new MutationObserver(nextTickHandler) var textNode = document.createTextNode(String(counter)); // 使用MutationObserver的介面,監聽文字節點的字元內容 observer.observe(textNode, { characterData: true }); /* 每次執行timerFunc函式都會讓文字節點的內容在0/1之間切換,切換之後將新賦值到那個我們MutationObserver監聽的文字節點上去。 */ timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } } else { // fallback to setTimeout /* 如果上面的兩種都不支援的話,我們就使用setTimeout來執行 */ timerFunc = () => { setTimeout(nextTickHandler, 0) } } return function queueNextTick (cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }); /* 如果pending為true,表明本輪事件迴圈中已經執行過 timerFunc(nextTickHandler, 0) */ if (!pending) { pending = true timerFunc() } if (!cb && typeof Promise !== 'undefined') { return new Promise((resolve, reject) => { _resolve = resolve }) } } })()
整體思路理解:首先 nextTick 是一個閉包函式,程式碼立即執行,在理解整體程式碼之前,我們先來看個類似的demo,如下程式碼:
<!DOCTYPE html> <html> <head> <title>演示Vue</title> </head> <body> <div id="app"> </div> <script> var nextTick = (function(){ return function queueNextTick(cb, ctx) { if (cb) { try { cb.call(ctx) } catch (e) { console.log('出錯了'); } } } })(); // 方法呼叫 nextTick(function(){ console.log(2); // 列印2 }) </script> </body> </html>
demo程式碼和上面的程式碼很類似。
我們也可以再來抽離使用nextTick做demo程式碼如下:
var nextTick2 = (function(){ const callbacks = []; let pending = false; let timerFunc; function nextTickHandler () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } if (typeof Promise !== 'undefined') { var p = Promise.resolve() var logError = err => { console.error(err) } timerFunc = () => { p.then(nextTickHandler).catch(logError) } } else if (typeof MutationObserver !== 'undefined' || // 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 = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } } else { // fallback to setTimeout /* istanbul ignore next */ timerFunc = () => { setTimeout(nextTickHandler, 0) } } return function queueNextTick (cb, ctx) { let _resolve callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true timerFunc() } if (!cb && typeof Promise !== 'undefined') { return new Promise((resolve, reject) => { _resolve = resolve }) } } })(); nextTick2(function(){ console.log(2222); });
如上程式碼是nextTick原始碼的抽離,為了更好的理解nextTick,做了如上的demo。
我們再來理解一下整體的程式碼的含義;
先定義陣列 callbacks = [];來存放所有需要執行的回撥函式,定義let pending = false;判斷本輪事件是否執行過 timerFunc(nextTickHandler, 0)這個函式,為true說明執行過 timeFunc函式,接著定義nextTickHandler函式,該函式的作用是依次遍歷陣列callbacks儲存的函式,依次執行;
請看原始碼如下:
function nextTickHandler () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } }
然後就是三個判斷了,程式碼如下:
if (typeof Promise !== 'undefined' && isNative(Promise)) { var p = Promise.resolve(); var logError = err => { console.error(err) } timerFunc = () => { p.then(nextTickHandler).catch(logError); } else if (typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )){ var counter = 1 var observer = new MutationObserver(nextTickHandler) var textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } } else { timerFunc = () => { setTimeout(nextTickHandler, 0) } }
首先判斷是否支援Promise物件,如果支援的話,定義了timeFunc()函式,為了下一步呼叫做準備,然後繼續判斷是否支援該物件 MutationObserver,
如果支援的話,建立一個文字節點,監聽該節點資料是否發生改變,如果發生改變的話,呼叫timerFunc函式,counter值會在0/1切換,如果值改變了的話,
把該資料值賦值到data屬性上面去,那麼data屬性發生改變了,就會重新渲染頁面(因為vue是通過Object.defineProperty來監聽屬性值是否發生改變),
如果上面兩種情況都不滿足的話,那麼直接使用setTimeout來執行nextTickHandler函式了;
最後nextTick程式碼返回一個函式,程式碼如下:
return function queueNextTick (cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true timerFunc() } if (!cb && typeof Promise !== 'undefined') { return new Promise((resolve, reject) => { _resolve = resolve }) } }
程式碼的含義是:傳入的cb是否是函式,ctx引數是否是一個物件,如果cb是一個函式的話,使用cb.call(ctx), 如果timerFunc沒有執行過的話,那麼pending為
false,因此執行 timerFunc()函式。基本的思路就是這樣的。