Vue系列---理解Vue.nextTick使用及原始碼分析(五)
閱讀目錄
- 一. 什麼是Vue.nextTick()?
- 二. Vue.nextTick()方法的應用場景有哪些? 2.1 更改資料後,進行節點DOM操作。 2.2 在created生命週期中進行DOM操作。
- 三. Vue.nextTick的呼叫方式如下:
- 四:vm.$nextTick 與 setTimeout 的區別是什麼?
- 五:理解 MutationObserver
- 六:nextTick原始碼分析
一. 什麼是Vue.nextTick()?
官方文件解釋為:在下次DOM更新迴圈結束之後執行的延遲迴調。在修改資料之後立即使用該方法,獲取更新後的DOM。
我們也可以簡單的理解為:當頁面中的資料發生改變了,就會把該任務放到一個非同步佇列中,只有在當前任務空閒時才會進行DOM渲染,當DOM渲染完成以後,該函式就會自動執行。
回到頂部2.1 更改資料後,進行節點DOM操作。
比如修改資料、修改節點樣式、等操作。比如說我修改data中的一個屬性資料後,如果我這個時候直接獲取該html內容的話,它還是老資料的,那麼此時此刻,我們可以使用 Vue.nextTick(), 在該函式內部獲取該資料即可: 如下程式碼:<!DOCTYPE html> <html> <head> <title>vue.nextTick()方法的使用</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <template> <div ref="list">{{name}}</div> </template> </div> <script type="text/javascript"> new Vue({ el: '#app', data: { name: 'kongzhi111' }, mounted() { this.updateData(); }, methods: { updateData() { this.name = 'kongzhi222'; console.log(this.$refs.list.textContent); // 列印 kongzhi111 this.$nextTick(() => { console.log('-------'); console.log(this.$refs.list.textContent); // 列印 kongzhi222 }); } } }) </script> </body> </html>
如上程式碼,頁面初始化時候,頁面顯示的是 "kongzhi111"; 當頁面中的所有的DOM更新完成後,我在mounted()生命週期中呼叫 updateData()方法,然後在該方法內部修改 this.name 這個資料,再列印 this.$refs.list.textContent, 可以看到列印的資料 還是 'kongzhi111'; 為什麼會是這樣呢?那是因為修改name資料後,我們的DOM還沒有被渲染完成,所以我們這個時候獲取的值還是之前的值,但是我們放在nextTick函式裡面的時候,程式碼會在DOM更新完成後 會自動執行 nextTick()函式,因此這個時候我們再去使用 this.$refs.list.textContent 獲取該值的時候,就可以獲取到最新值了。
2.2 在created生命週期中進行DOM操作。
在Vue生命週期中,只有在mounted生命週期中我們的HTML才渲染完成,因此在該生命週期中,我們就可以獲取到頁面中的html DOM節點,但是如果我們在 created生命週期中是訪問不到DOM節點的。
在該生命週期中我們想要獲取DOM節點的話,我們需要使用 this.$nextTick() 函式。
比如如下程式碼進行演示:
<!DOCTYPE html> <html> <head> <title>vue.nextTick()方法的使用</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <template> <div ref="list">{{name}}</div> </template> </div> <script type="text/javascript"> new Vue({ el: '#app', data: { name: 'kongzhi111' }, created() { console.log(this.$refs.list); // 列印undefined this.$nextTick(() => { console.log(this.$refs.list); // 打印出 "<div>kongzhi111</div>" }); }, methods: { } }) </script> </body> </html>
如上程式碼,在created生命週期內,我們列印 this.$refs.list 值為undefined,那是因為在created生命週期內頁面的html沒有被渲染完成,因此打印出為undefined; 但是我們把它放入 this.$nextTick函式內即可 打印出值出來,這也印證了 nextTick 是在下次DOM更新迴圈結束之後執行的延遲迴調。因此只有DOM渲染完成後才會自動執行的延遲迴調函式。
Vue的特點之一就是能實現響應式,但資料更新時,DOM不會立即更新,而是放入一個非同步佇列中,因此如果在我們的業務場景中,需要在DOM更新之後執行一段程式碼時,這個時候我們可以使用 this.$nextTick() 函式來實現。
回到頂部三. Vue.nextTick的呼叫方式如下:
Vue.nextTick([callback, context]) 和 vm.$nextTick([callback]);
Vue.nextTick([callback, context]); 該方法是全域性方法,該方法可接收2個引數,分別為回撥函式 和 執行回撥函式的上下文環境。
vm.$nextTick([callback]): 該方法是實列方法,執行時自動繫結this到當前的實列上。
回到頂部四:vm.$nextTick 與 setTimeout 的區別是什麼?
在區別他們倆之前,我們先來看一個簡單的demo如下:<!DOCTYPE html> <html> <head> <title>vue.nextTick()方法的使用</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <template> <div ref="list">{{name}}</div> </template> </div> <script type="text/javascript"> new Vue({ el: '#app', data: { name: 'kongzhi111' }, created() { console.log(this.$refs.list); // 列印undefined setTimeout(() => { console.log(this.$refs.list); // 打印出 "<div>kongzhi111</div>" }, 0); } }) </script> </body> </html>
如上程式碼,我們不使用 nextTick, 我們使用setTimeout延遲也一樣可以獲取頁面中的HTML元素的,那麼他們倆之間到底有什麼區別呢?
通過看vue原始碼我們知道,nextTick 原始碼在 src/core/util/next-tick.js 裡面。在vue中使用了三種情況來延遲呼叫該函式,首先我們會判斷我們的裝置是否支援Promise物件,如果支援的話,會使用 Promise.then 來做延遲呼叫函式。如果裝置不支援Promise物件,再判斷是否支援 MutationObserver 物件,如果支援該物件,就使用MutationObserver來做延遲,最後如果上面兩種都不支援的話,我們會使用 setTimeout(() => {}, 0); setTimeout 來做延遲操作。
在比較 nextTick 與 setTimeout 的區別,其實我們可以比較 promise 或 MutationObserver 物件 與 setTimeout的區別的了,因為nextTick會先判斷裝置是否支援promise及MutationObserver 物件的,只要我們弄懂 promise 和 setTimeout的區別,也就弄明白 nextTick 與 setTimeout的區別了。
在比較promise與setTimeout之前,我們先來看如下demo。
<!DOCTYPE html> <html> <head> <title></title> <meta charset="utf-8"> </head> <body> <script type="text/javascript"> console.log(1); setTimeout(function(){ console.log(2); }, 0); new Promise(function(resolve) { console.log(3); for (var i = 0; i < 100; i++) { i === 99 && resolve(); } console.log(4); }).then(function() { console.log(5); }); console.log(6); </script> </body> </html>
如上程式碼輸出的結果是:1, 3, 4, 6, 5, 2; 首先列印1,這個我們能理解的,其實為什麼列印3,在promise內部也屬於同步的,只有在then內是非同步的,因此列印 1, 3, 4 , 然後執行then函式是非同步的,因此列印6. 那麼結果為什麼是 1, 3, 4, 6, 5, 2 呢? 為什麼不是 1, 3, 4, 6, 2, 5呢?
我們都知道 Promise.then 和 setTimeout 都是非同步的,那麼在事件佇列中Promise.then的事件應該是在setTimeout的後面的,那麼為什麼Promise.then比setTimeout函式先執行呢?
理解Event Loop 的概念
我們都明白,javascript是單執行緒的,所有的任務都會在主執行緒中執行的,當主執行緒中的任務都執行完成之後,系統會 "依次" 讀取任務佇列裡面的事件,因此對應的非同步任務進入主執行緒,開始執行。
但是非同步任務佇列又分為: macrotasks(巨集任務) 和 microtasks(微任務)。 他們兩者分別有如下API:
macrotasks(巨集任務): setTimeout、setInterval、setImmediate、I/O、UI rendering 等。
microtasks(微任務): Promise、process.nextTick、MutationObserver 等。
如上我們的promise的then方法的函式會被推入到 microtasks(微任務) 佇列中,而setTimeout函式會被推入到 macrotasks(巨集任務) 任務佇列中,在每一次事件迴圈中 macrotasks(巨集任務) 只會提取一個執行,而 microtasks(微任務) 會一直提取,直到 microtasks(微任務)佇列為空為止。
也就是說,如果某個 microtasks(微任務) 被推入到執行中,那麼當主執行緒任務執行完成後,會迴圈呼叫該佇列任務中的下一個任務來執行,直到該任務佇列到最後一個任務為止。而事件迴圈每次只會入棧一個 macrotasks(巨集任務), 主執行緒執行完成該任務後又會迴圈檢查 microtasks(微任務) 佇列是否還有未執行的,直到所有的執行完成後,再執行 macrotasks(巨集任務)。 依次迴圈,直到所有的非同步任務完成為止。
有了上面 macrotasks(巨集任務) 和 microtasks(微任務) 概念後,我們再來理解上面的程式碼,上面所有的程式碼都寫在script標籤中,那麼讀取script標籤中的所有程式碼,它就是第一個巨集任務,因此我們就開始執行第一個巨集任務。因此首先列印 1, 然後程式碼往下讀取,我們遇到setTimeout, 它就是第二個巨集任務,會將它推入到 macrotasks(巨集任務) 事件佇列裡面排隊。
下面我們繼續往下讀取,
遇到Promise物件,在Promise內部執行它是同步的,因此會列印3, 4。 然後繼續遇到 Promise.then 回撥函式,他是一個 microtasks(微任務)的,因此將他 推入到 microtasks(微任務) 事件佇列中,最後程式碼執行 console.log(6); 因此列印6. 第一個macrotasks(巨集任務)執行完成後,然後我們會依次迴圈執行 microtasks(微任務), 直到最後一個為止,因此我們就執行 promise.then() 非同步回撥中的程式碼,因此列印5,那麼此時此刻第一個 macrotasks(巨集任務) 執行完畢,會執行下一個 macrotasks(巨集任務)任務。因此就執行到 setTimeout函數了,最後就列印2。到此,所有的任務都執行完畢。因此我們最後的結果為:1, 3, 4, 6, 5, 2;
我們可以繼續多新增幾個setTimeout函式和多加幾個Promise物件來驗證下,如下程式碼:
<script type="text/javascript"> console.log(1); setTimeout(function(){ console.log(2); }, 10); new Promise(function(resolve) { console.log(3); for (var i = 0; i < 10000; i++) { i === 9999 && resolve(); } console.log(4); }).then(function() { console.log(5); }); setTimeout(function(){ console.log(7); },1); new Promise(function(resolve) { console.log(8); resolve(); }).then(function(){ console.log(9); }); console.log(6); </script>
如上列印的結果為: 1, 3, 4, 8, 6, 5, 9, 7, 2;
首先列印1,這是沒有任何爭議的哦,promise內部也是同步程式碼,因此列印 3, 4, 然後就是第二個promise內部程式碼,因此列印8,再列印外面的程式碼,就是6。因此主執行緒執行完成後,列印的結果分別為:
1, 3, 4, 8, 6。 然後再執行 promise.then() 回撥的 microtasks(微任務)。因此列印 5, 9。因此microtasks(微任務)執行完成後,就執行第二個巨集任務setTimeout,由於第一個setTimeout是10毫秒後執行,第二個setTimeout是1毫秒後執行,因此1毫秒的優先順序大於10毫秒的優先順序,因此最後分別列印 7, 2 了。因此列印的結果是: 1, 3, 4, 8, 6, 5, 9, 7, 2;
總結: 如上我們也看到 microtasks(微任務) 包括 Promise 和 MutationObserver, 因此 我們可以知道在Vue中的nextTick 的執行速度上是快於setTimeout的。
我們從如下demo也可以得到驗證:
<!DOCTYPE html> <html> <head> <title>vue.nextTick()方法的使用</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <template> <div ref="list">{{name}}</div> </template> </div> <script type="text/javascript"> new Vue({ el: '#app', data: { name: 'kongzhi111' }, created() { console.log(this.$refs.list); // 列印undefined setTimeout(() => { console.log(this.$refs.list); // 打印出 "<div>kongzhi111</div>" }, 0); this.$nextTick(function(){ console.log('nextTick比setTimeout先執行'); }); } }) </script> </body> </html>
如上程式碼,先列印的是 undefiend, 其次是列印 "nextTick比setTimeout先執行" 資訊, 最後打印出 "<div>kongzhi111</div>" 資訊。
回到頂部五:理解 MutationObserver
在Vue中的nextTick的原始碼中,使用了3種情況來做延遲操作,首先會判斷我們的裝置是否支援Promsie物件,如果支援Promise物件,就使用Promise.then()非同步函式來延遲,如果不支援,我們會繼續判斷我們的裝置是否支援 MutationObserver, 如果支援,我們就使用 MutationObserver 來監聽。最後如果上面兩種都不支援的話,我們會使用 setTimeout 來處理,那麼我們現在要理解的是 MutationObserver 是什麼?
5.1 MutationObserver是什麼?
MutationObserver 中文含義可以理解為 "變動觀察器"。它是監聽DOM變動的介面,DOM發生任何變動,MutationObserver會得到通知。在Vue中是通過該屬性來監聽DOM更新完畢的。
它和事件類似,但有所不同,事件是同步的,當DOM發生變動時,事件會立刻處理,但是 MutationObserver 則是非同步的,它不會立即處理,而是等頁面上所有的DOM完成後,會執行一次,如果頁面上要操作100次DOM的話,如果是事件的話會監聽100次DOM,但是我們的 MutationObserver 只會執行一次,它是等待所有的DOM操作完成後,再執行。
它的特點是:
1. 等待所有指令碼任務完成後,才會執行,即採用非同步方式。
2. DOM的變動記錄會封裝成一個數組進行處理。
3. 還可以觀測發生在DOM的所有型別變動,也可以觀測某一類變動。
當然 MutationObserver 也是有瀏覽器相容的,我們可以使用如下程式碼來檢測瀏覽器是否支援該屬性,如下程式碼:
var MutationObserver = window.MutationObserver || window.WebkitMutationObserver || window.MozMutationObserver; // 監測瀏覽器是否支援 var observeMutationSupport = !!MutationObserver;
MutationObserver 建構函式
首先我們要使用 MutationObserver 建構函式的話,我們先要實列化 MutationObserver 建構函式,同時我們要指定該實列的回撥函式,如下程式碼:
var observer = new MutationObserver(callback);
觀察器callback回撥函式會在每次DOM發生變動後呼叫,它接收2個引數,第一個是變動的陣列,第二個是觀察器的實列。
MutationObserver 實列的方法
observe() 該方法是要觀察DOM節點的變動的。該方法接收2個引數,第一個引數是要觀察的DOM元素,第二個是要觀察的變動型別。
呼叫方式為:observer.observe(dom, options);
options 型別有如下:
childList: 子節點的變動。
attributes: 屬性的變動。
characterData: 節點內容或節點文字的變動。
subtree: 所有後代節點的變動。
需要觀察哪一種變動型別,需要在options物件中指定為true即可; 但是如果設定subtree的變動,必須同時指定childList, attributes, 和 characterData 中的一種或多種。
1. 監聽childList的變動
如下測試程式碼:
<!DOCTYPE html> <html> <head> <title>MutationObserver</title> <meta charset="utf-8"> </head> <body> <div id="app"> <ul> <li>kongzhi111</li> </ul> </div> <script type="text/javascript"> var MutationObserver = window.MutationObserver || window.WebkitMutationObserver || window.MozMutationObserver; var list = document.querySelector('ul'); var Observer = new MutationObserver(function(mutations, instance) { console.log(mutations); // 列印mutations 如下圖對應的 console.log(instance); // 列印instance 如下圖對於的 mutations.forEach(function(mutation){ console.log(mutation); // 列印mutation }); }); Observer.observe(list, { childList: true, // 子節點的變動 subtree: true // 所有後代節點的變動 }); var li = document.createElement('li'); var textNode = document.createTextNode('kongzhi'); li.appendChild(textNode); list.appendChild(li); </script> </body> </html>
如上程式碼,我們使用了 observe() 方法來觀察list節點的變化,只要list節點的子節點或後代的節點有任何變化都會觸發 MutationObserver 建構函式的回撥函式。因此就會列印該建構函式裡面的資料。
列印如下圖所示:
2. 監聽characterData的變動
如下測試程式碼:
<!DOCTYPE html> <html> <head> <title>MutationObserver</title> <meta charset="utf-8"> </head> <body> <div id="app"> <ul> <li>kongzhi111</li> </ul> </div> <script type="text/javascript"> var MutationObserver = window.MutationObserver || window.WebkitMutationObserver || window.MozMutationObserver; var list = document.querySelector('ul'); var Observer = new MutationObserver(function(mutations, instance) { mutations.forEach(function(mutation){ console.log(mutation); }); }); Observer.observe(list, { childList: true, // 子節點的變動 characterData: true, // 節點內容或節點文字變動 subtree: true // 所有後代節點的變動 }); // 改變節點中的子節點中的資料 list.childNodes[0].data = "kongzhi222"; </script> </body> </html>
列印如下效果:
3. 監聽屬性的變動
<!DOCTYPE html> <html> <head> <title>MutationObserver</title> <meta charset="utf-8"> </head> <body> <div id="app"> <ul> <li>kongzhi111</li> </ul> </div> <script type="text/javascript"> var MutationObserver = window.MutationObserver || window.WebkitMutationObserver || window.MozMutationObserver; var list = document.querySelector('ul'); var Observer = new MutationObserver(function(mutations, instance) { mutations.forEach(function(mutation){ console.log(mutation); }); }); Observer.observe(list, { attributes: true }); // 設定節點的屬性,會觸發回撥函式 list.setAttribute('data-value', 'tugenhua111'); // 重新設定屬性,會觸發回撥函式 list.setAttribute('data-value', 'tugenhua222'); // 刪除屬性,也會觸發回撥函式 list.removeAttribute('data-value'); </script> </body> </html>
如上就是MutationObserver的基本使用,它能監聽 子節點的變動、屬性的變動、節點內容或節點文字的變動 及 所有後代節點的變動。 下面我們來看下我們的 nextTick.js 中的原始碼是如何實現的。
回到頂部六:nextTick原始碼分析
vue原始碼在 vue/src/core/util/next-tick.js 中。原始碼如下:import { noop } from 'shared/util' import { handleError } from './error' import { isIE, isIOS, isNative } from './env' export let isUsingMicroTask = false 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]() } } let timerFunc; if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) 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)) { timerFunc = () => { setImmediate(flushCallbacks) } } else { // Fallback to setTimeout. timerFunc = () => { setTimeout(flushCallbacks, 0) } } export function nextTick (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() } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }
如上程式碼,我們從上往下看,首先定義變數 callbacks = []; 該變數的作用是: 用來儲存所有需要執行的回撥函式。let pending = false; 該變數的作用是表示狀態,判斷是否有正在執行的回撥函式。
也可以理解為,如果程式碼中 timerFunc 函式被推送到任務佇列中去則不需要重複推送。
flushCallbacks() 函式,該函式的作用是用來執行callbacks裡面儲存的所有回撥函式。如下程式碼:
function flushCallbacks () { /* 設定 pending 為 false, 說明該 函式已經被推入到任務佇列或主執行緒中。需要等待當前 棧執行完畢後再執行。 */ pending = false; // 拷貝一個callbacks函式陣列的副本 const copies = callbacks.slice(0) // 把函式陣列清空 callbacks.length = 0 // 迴圈該函式陣列,依次執行。 for (let i = 0; i < copies.length; i++) { copies[i]() } }
timerFunc: 儲存需要被執行的函式。
繼續看接下來的程式碼,我們上面講解過,在Vue中使用了幾種情況來延遲呼叫該函式。
1. promise.then 延遲呼叫, 基本程式碼如下:
if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) if (isIOS) setTimeout(noop) } isUsingMicroTask = true }
如上程式碼的含義是: 如果我們的裝置(或叫瀏覽器)支援Promise, 那麼我們就使用 Promise.then的方式來延遲函式的呼叫。Promise.then會將函式延遲到呼叫棧的最末端,從而會做到延遲。
2. MutationObserver 監聽, 基本程式碼如下:
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 }
如上程式碼,首先也是判斷我們的裝置是否支援 MutationObserver 物件, 如果支援的話,我們就會建立一個MutationObserver建構函式, 並且把flushCallbacks函式當做callback的回撥, 然後我們會建立一個文字節點, 之後會使用MutationObserver物件的observe來監聽該文字節點, 如果文字節點的內容有任何變動的話,它就會觸發 flushCallbacks 回撥函式。那麼要怎麼樣觸發呢? 在該程式碼內有一個 timerFunc 函式, 如果我們觸發該函式, 會導致文字節點的資料發生改變,進而觸發MutationObserver建構函式。
3. setImmediate 監聽, 基本程式碼如下:
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 = () => { setImmediate(flushCallbacks) } }
如果上面的 Promise 和 MutationObserver 都不支援的話, 我們繼續會判斷裝置是否支援 setImmediate, 我們上面分析過, 他屬於 macrotasks(巨集任務)的。該任務會在一個巨集任務裡執行回撥佇列。
4. 使用setTimeout 做降級處理
如果我們上面三種情況, 裝置都不支援的話, 我們會使用 setTimeout 來做降級處理, 實現延遲效果。如下基本程式碼:
else { // Fallback to setTimeout. timerFunc = () => { setTimeout(flushCallbacks, 0) } }
現在我們的原始碼繼續往下看, 會看到我們的nextTick函式被export了,如下基本程式碼:
export function nextTick (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() } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }
如上程式碼, nextTick 函式接收2個引數,cb 是一個回撥函式, ctx 是一個上下文。 首先會把它存入callbacks函式數組裡面去, 在函式內部會判斷cb是否是一個函式,如果是一個函式,就呼叫執行該函式,當然它會在callbacks函式陣列遍歷的時候才會被執行。其次 如果cb不是一個函式的話, 那麼會判斷是否有_resolve值, 有該值就使用Promise.then() 這樣的方式來呼叫。比如: this.$nextTick().then(cb) 這樣的使用方式。因此在下面的if語句內會判斷賦值給_resolve:
if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) }
使用Promise返回了一個 fulfilled 的Promise。賦值給 _resolve; 然後在callbacks.push 中會執行如下:
_resolve(ctx);
全域性方法Vue.nextTick在 /src/core/global-api/index.js 中宣告,是對函式nextTick的引用,所以使用時可以顯式指定執行上下文。程式碼初始化如下:
Vue.nextTick = nextTick;
我們可以使用如下的一個簡單的demo來簡化上面的程式碼。如下demo:
<script type="text/javascript"> var callbacks = []; var pending = false; function timerFunc() { const copies = callbacks.slice(0) callbacks.length = 0 for (var i = 0; i < copies.length; i++) { copies[i]() } } function nextTick(cb, ctx) { var _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() } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } } // 呼叫方式如下: nextTick(function() { console.log('打印出來了'); // 會被執行列印 }); </script>
如上我們已經知道了 nextTick 是Vue中的一個全域性函式, 在Vue裡面會有一個Watcher, 它用於觀察資料的變化, 然後更新DOM, 但是在Vue中並不是每次資料改變都會觸發更新DOM的, 而是將這些操作都快取到一個佇列中, 在一個事件迴圈結束後, 會重新整理佇列, 會統一執行DOM的更新操作。
在Vue中使用的是Object.defineProperty來監聽每個物件屬性資料變化的, 當監聽到資料發生變化的時候, 我們需要把該訊息通知到所有的訂閱者, 也就是Dep, 那麼Dep則會呼叫它管理的所有的Watch物件,因此會呼叫Watch物件中的update方法, 我們可以看下原始碼中的update的實現。原始碼在 vue/src/core/observer/watcher.js 中如下程式碼:
update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { // 同步執行渲染檢視 this.run() } else { // 非同步推送到觀察者佇列中 queueWatcher(this) } }
如上程式碼我們可以看到, 在Vue中它預設是使用非同步執行DOM更新的。當非同步執行update的時候,它預設會呼叫 queueWatcher 函式。
我們下面再來看下該 queueWatcher 函式程式碼如下: (原始碼在: vue/src/core/observer/scheduler.js) 中。
export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true 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. let 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 if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } nextTick(flushSchedulerQueue) } } }
如上原始碼, 我們從第一句程式碼執行過來, 首先獲取該 id = watcher.id; 然後判斷該id是否存在 if (has[id] == null) {} , 如果已經存在則直接跳過,不存在則執行if
語句內部程式碼, 並且標記雜湊表has[id] = true; 用於下次檢驗。如果 flushing 為false的話, 則把該watcher物件push到佇列中, 考慮到一些情況, 比如正在更新佇列中
的watcher時, 又有事件塞入進來怎麼處理? 因此這邊加了一個flushing來表示佇列的更新狀態。
如果加入佇列到更新狀態時,又分為兩種情況:
1. 這個watcher還沒有處理, 就找到這個watcher在佇列中的位置, 並且把新的放在後面, 比如如下程式碼:
if (!flushing) { queue.push(watcher) }
2. 如果watcher已經更新過了, 就把這個watcher再放到當前執行的下一位, 當前的watcher處理完成後, 立即會處理這個最新的。如下程式碼:
else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) }
接著如下程式碼:
if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } nextTick(flushSchedulerQueue) }
waiting 為false, 等待下一個tick時, 會執行重新整理佇列。 如果不是正式環境的話, 會直接 呼叫該函式 flushSchedulerQueue; (原始碼在: vue/src/core/observer/scheduler.js) 中。否則的話, 把該函式放入 nextTick 函式延遲處理。