使用場景
在進行獲取數據后,需要對新視圖進行下一步操作或者其他操作時,發現獲取不到 DOM。
原因:
這里就涉及到 Vue 一個很重要的概念:異步更新隊列(JS運行機制 、 事件循環)。
Vue 在觀察到數據變化時並不是直接更新 DOM,而是開啟一個隊列,並緩沖在同一事件循環中發生的所有數據改變。
在緩沖時會去除重復數據,從而避免不必要的計算和DOM操作。
然后,在下一個事件循環 tick 中,Vue 刷新隊列並執行實際(已去重的)工作。
所以如果用 for 循環來動態改變數據100次,其實它只會應用最后一次改變,如果沒有這種機制,DOM就要重繪100次,是一個很大的開銷,損耗性能。
一個this.$nextTick的實現
首先,定義變量:
var callbacks = []; // 緩存函數的數組 var pending = false; // 是否正在執行 var timerFunc; // 保存着要執行的函數
然后,創建 $nextTick 內實際調用的函數
function nextTickHandler () { pending = false; // 拷貝出函數數組副本 var copies = callbacks.slice(0); // 把函數數組清空 callbacks.length = 0; // 依次執行函數 for (var i = 0; i < copies.length; i++) { copies[i](); } }
其次, Vue 會根據當前瀏覽器環境優先使用原生的 Promise.then 和 MutationObserver,如果都不支持,就會采用 setTimeout 代替,目的是 延遲函數到 DOM 更新后再使用
一、Promise.then 的延遲調用
if (typeof Promise !== 'undefined' && isNative(Promise)) { var p = Promise.resolve(); var logError = function (err) { console.error(err); }; timerFunc = function () { p.then(nextTickHandler).catch(logError); if (isIOS) { setTimeout(noop); } }; }
如果瀏覽器支持Promise,那么就用Promise.then的方式來延遲函數調用,Promise.then方法可以將函數延遲到當前函數調用棧最末端,也就是函數調用棧最后調用該函數。從而做到延遲。
二、MutationObserver
else if (typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]' )) { 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); }; }
MutationObserver是h5新加的一個功能,其功能是監聽dom節點的變動,在所有dom變動完成后,執行回調函數。
具體有一下幾點變動的監聽:
childList:子元素的變動
attributes:屬性的變動
characterData:節點內容或節點文本的變動
subtree:所有下屬節點(包括子節點和子節點的子節點)的變動
可以看出,以上代碼是創建了一個文本節點,來改變文本節點的內容來觸發的變動,因為我們在數據模型更新后,將會引起dom節點重新渲染,所以,我們加了這樣一個變動監聽,用一個文本節點的變動觸發監聽,等所有dom渲染完后,執行函數,達到我們延遲的效果。
三、setTimeOut 延遲器
else { timerFunc = function () { setTimeout(nextTickHandler, 0); }; }
利用setTimeout的延遲原理,setTimeout(func, 0)會將func函數延遲到下一次函數調用棧的開始,也就是當前函數執行完畢后再執行該函數,因此完成了延遲功能。
閉包函數
return function queueNextTick (cb, ctx) { var _resolve; callbacks.push(function () { if (cb) { cb.call(ctx); } if (_resolve) { _resolve(ctx); } }); // 如果沒有函數隊列在執行才執行 if (!pending) { pending = true; timerFunc(); } // promise化 if (!cb && typeof Promise !== 'undefined') { console.log('進來了') return new Promise(function (resolve) { _resolve = resolve; }) } }
這個return的函數就是我們實際使用的閉包函數,每一次添加函數,都會想callbacks這個函數數組入棧。然后監聽當前是否正在執行,如果沒有,執行函數。這個很好理解。下面一個if是promise化。
this.$nextTick(function () { }) // promise化 this.$nextTick().then(function () { }.bind(this))
以上代碼中第二種寫法我們不常見,直接調用$nextTick函數然后用promise格式去書寫代碼,不過這個then里面需要手動綁定this,vue內部沒有給做處理。
附上完整代碼:
var nextTick=(function () { //存儲需要觸發的回調函數 var callbacks=[]; //是否正在等待的標志(false:允許觸發在下次事件循環觸發callbacks中的回調, // true: 已經觸發過,需要等到下次事件循環) var pending=false; //設置在下次事件循環觸發callbacks的觸發函數 var timerFunc; //處理callbacks的函數 function nextTickHandler() { // 可以觸發timeFunc pending=false; //復制callback var copies=callbacks.slice(0); //清除callback callbacks.length=0; for(var i=0;i<copies.length;i++){ //觸發callback的回調函數 copies[i](); } } //如果支持promise,使用promise實現 if(typeof Promise !=='undefined' && isNative(promise)){ var p=Promise.resolve(); var logError=function (err) { console.error(err); }; timerFunc=function () { p.then(nextTickHandler).catch(logError); //iOS的webview下,需要強制刷新隊列,執行上面的回調函數 if(isIOS) {setTimeout(noop);} }; // 如果Promise不支持,但支持MutationObserver // H5新特性,異步,當dom變動是觸發,注意是所有的dom都改變結束后觸發 } else if (typeof MutationObserver !=='undefined' && ( isNative(MutationObserver) || MutationObserver.toString()==='[object MutationObserverConstructor]')){ 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 { //上面兩種都不支持,用setTimeout timerFunc=function () { setTimeout(nextTickHandler,0); }; } //nextTick接收的函數,參數1:回調函數 參數2:回調函數的執行上下文 return function queueNextTick(cb,ctx) { //用於接收觸發Promise.then中回調的函數 //向回調函數中pushcallback var _resolve; callbacks.push(function () { //如果有回調函數,執行回調函數 if(cb) {cb.call(ctx);} //觸發Promise的then回調 if(_resolve) {_resolve(ctx);} }); //是否執行刷新callback隊列 if(!pending){ pending=true; timerFunc(); } //如果沒有傳遞回調函數,並且當前瀏覽器支持promise,使用promise實現 if(!cb && typeof Promise !=='undefined'){ return new Promise(function (resolve) { _resolve=resolve; }) } } })