1. 程式人生 > >Vue $nextTick 原理

Vue $nextTick 原理

this 重要 ctx 場景 下一個 sna ati cal list

使用場景

  在進行獲取數據後,需要對新視圖進行下一步操作或者其他操作時,發現獲取不到 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.thenMutationObserver,如果都不支持,就會采用 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;
            })
        }
    }
})

隨筆整理自
  https://www.cnblogs.com/xujiazheng/p/6852124.html
https://www.jianshu.com/p/a7550c0e164f
感謝博主分享!

Vue $nextTick 原理