閱讀目錄javascript
一. 什麼是Vue.nextTick()?html
官方文檔解釋爲:在下次DOM更新循環結束以後執行的延遲迴調。在修改數據以後當即使用該方法,獲取更新後的DOM。vue
咱們也能夠簡單的理解爲:當頁面中的數據發生改變了,就會把該任務放到一個異步隊列中,只有在當前任務空閒時纔會進行DOM渲染,當DOM渲染完成之後,該函數就會自動執行。java
2.1 更改數據後,進行節點DOM操做。api
<!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 獲取該值的時候,就能夠獲取到最新值了。
理解DOM更新:在VUE中,當咱們修改了data中的某一個值後,並不會馬上去渲染html頁面,而是將vue更改的數據放到watcher的一個異步隊列中,只有在當前任務空閒時纔會執行watcher中的隊列任務,所以這就會有一個延遲時間,所以咱們把代碼放到nextTick函數後就能夠獲取到該 html 頁面的最新值了。數組
2.2 在created生命週期中進行DOM操做。promise
在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渲染完成後纔會自動執行的延遲迴調函數。app
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 的區別是什麼?
<!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源碼分析
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 函數延遲處理。