深刻淺出理解vm.$nextTick

使用場景:

在咱們開發項目的時候,總會碰到一些場景:當咱們使用vue操做更新dom後,須要對新的dom作一些操做時,可是這個時候,咱們每每會獲取不到跟新後的DOM.由於這個時候,dom尚未從新渲染,因此咱們就要使用vm.$nextTick方法。前端

用法:

nextTick接受一個回調函數做爲參數,它的做用將回調延遲到下次DOM跟新週期以後執行。vue

methods:{
example:function(){
 //修改數據
 this.message='changed'
//此時dom尚未跟新,不能獲取新的數據
 this.$nextTick(function(){
   //dom如今跟新了
   //能夠獲取新的dom數據,執行操做
   this.doSomeThing()
  })
 }
}
複製代碼

小思考:

在用法中,咱們發現,什麼是下次DOM更新週期以後執行,具體是何時,因此,咱們要明白什麼是DOM更新週期。 在Vue當中,當視圖狀態發生變化時,watcher會獲得通知,而後觸發虛擬DOM的渲染流程,渲染這個操做不是同步的,是異步。Vue中有一個隊列,每當渲染時,會將watcher推送這個隊列,在下一次事件循環中,讓watcher觸發渲染流程。算法

爲何Vue使用異步更新隊列?

簡單來講,就是提高性能,提高效率。 咱們知道Vue2.0使用虛擬dom來進行渲染,變化偵測的通知只發送到組件上,組件上的任意一個變化都會通知到一個watcher上,而後虛擬DOM會對整個組件進行比對(diff算法,之後有時間我會詳細研究一下),而後更新DOM.若是在同一輪事件循環中有兩個數據發生變化了,那麼組件的watcher會收到兩次通知,從而進行兩次渲染(同步跟新也是兩次渲染),事實上咱們並不須要渲染這麼屢次,只須要等全部狀態都修改完畢後,一次性將整個組件的DOM渲染到最新便可。緩存

如何解決一次事件循環組件屢次狀態改變只須要一次渲染更新?

其實很簡單,就是將收到的watcher實例加入隊列裏緩存起來,而且再添加隊列以前檢查這個隊列是否已存在相同watcher。不存在時,纔將watcher實例添加到隊列中。而後再下一次事件循環中,Vue會讓這個隊列中的watcher觸發渲染並清空隊列。這樣就保證一次事件循環組件屢次狀態改變只須要一次渲染更新。bash

什麼是事件循環?

咱們知道js是一門單線程非阻塞的腳本語言,意思是執行js代碼時,只有一個主線程來處理全部任務。非阻塞是指當代碼須要處理異步任務時,主線程會掛起(pending),當異步任務處理完畢,主線程根據必定的規則去執行回調。事實上,當任務執行完畢,js會將這個事件加入一個隊列(事件隊列)。被放入隊列中的事件不會馬上執行其回調,而是當前執行棧中全部任務執行完畢後,主線程會去查找事件隊列中是否有任務。
異步任務有兩種類型,微任務和宏任務。不一樣類型的任務會被分配到不一樣的任務隊列中。
執行棧中全部任務執行完畢後,主線程會去查找事件隊列中是否有任務,若是存在,依次執行全部隊列中的回調,只到爲空。而後再去宏任務隊列中取出一個事件,把對應的回調加入當前執行棧,當前執行棧中全部任務都執行完畢,檢查微任務隊列是否有事件。無線循環此過程,叫作事件循環。dom

常見的微任務

  • Promise.then
  • Object.observe
  • MutationObserver

常見的宏任務

  • setTimeout
  • setInterval
  • setImmediate
  • UI交互事件

在咱們使用vm.$nextTick中獲取跟新後DOM時,必定要在更改數據的後面使用nextTick註冊回調。異步

methods:{
example:function(){
 //修改數據
 this.message='changed'
//此時dom尚未跟新,不能獲取新的數據
 this.$nextTick(function(){
   //dom如今跟新了
   //能夠獲取新的dom數據,執行操做
   this.doSomeThing()
  })
 }
}
複製代碼

若是是先使用nextTick註冊回調,而後修改數據,在微任務隊列中先執行使用nextTick註冊的回調,而後才執行跟新DOM的回調,因此回調中得不到新的DOM,由於尚未更新。ide

methods:{
example:function(){
//此時dom尚未跟新,不能獲取新的數據
 this.$nextTick(function(){
 //dom沒有跟新,不能獲取新的dom
   this.doSomeThing()
  })
   //修改數據
 this.message='changed'
 }
}
複製代碼

咱們知道,添加微任務隊列中的任務執行機制要高於宏任務的執行機制(下面代碼必須理解)

methods:{
example:function(){
//先試用setTimeout向宏任務中註冊回調
setTimeout(()=>{
//如今DOM已經跟新了,能夠獲取最新DOM
})
   //而後修改數據
 this.message='changed'
 }
}
複製代碼

setTimeout屬於宏任務,使用它註冊回調會加入宏任務中,宏任務執行要比微任務晚,因此即使是先註冊,也是先跟新DOM後執行setTineout中設置回調。函數

理解nextTick的做用後,咱們如下來介紹實現原理

實現原理剖析:

因爲nextTick會將回調添加到任務隊列中延遲執行,因此在回調執行以前,若是反覆使用nextTick,Vue並不會將回調添加到任務隊列中,只會添加一個任務。Vue內部有一個列表來存儲nextTick參數中提供的回調,當任務觸發時,以此執行列表裏的全部回調並清空列表,其代碼以下(簡易版):oop

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 microTimeFun
const p=Promise.resolve()
microTimeFun=()=>{
  p.then(flushCallBacks)
}

export function nextTick(cb,ctx){
  callbacks.push(()=>{
    if(cb){
      cb.call(ctx)
    }
  })
  if(!pending){
    pending=true
    microTimeFun()
  }
}
複製代碼

理解相關變量:

  • callbacks:用來存儲用戶註冊的回調函數(得到了更新後DOM所進行的操做)
  • pending:用來標記是否向任務隊列添加任務,pending爲false,表示任務隊列沒有nextTIck任務,須要添加nextTick任務,當添加一個nextTick任務時,pending爲ture,在回調執行以前還有nextTick時,並不會重複添加任務到任務隊列,當回調函數開始執行時,pending爲flase,進行新的一輪事件循環。
  • flushCallbacks:就是咱們所說的被註冊在任務隊列中的任務,當這個函數執行,callbacks中全部函數依次執行,而後清空callbacks,並重置pending爲false,因此說,一輪事件循環中,flushCallbacks只會執行一次。
  • microTimerFunc:它的做用就是使用Promise.then將flushCallbacks添加到微任務隊列中。

下圖給出nextTick內部註冊流程和執行流程。

官方文檔裏面還有這麼一句話,若是沒有提供回調且支持Promise的環境下,則返回一個Promise。也就是說。能夠這樣使用nextTick

this.$nextTick().then(function(){
    //dom跟新了
})
複製代碼

要實現這個功能,只須要在nextTIck中判斷,若是沒有提供回調且當前支持Promise,那麼返回Promise,而且在callbacks中添加一個函數,當這個函數執行時,執行Promise的resolve,便可,代碼以下

function nextTick (cb, ctx) {
    var _resolve;
    callbacks.push(function () {
     if (cb) {
        cb.call(ctx);
      } else if (_resolve) {
        _resolve(ctx);
      }
    });
    if (!pending) {
      pending = true;
      timerFunc();
    }
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise(function (resolve) {
        _resolve = resolve;
      })
    }
  }
複製代碼

nextTick源碼查看

到此,nextTick原理基本上已經講完了。那咱們如今能夠看看真正vue中關於nextTick中的源碼,大概咱們都能理解的過來了,源碼以下。

var timerFunc;

  // 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 next, $flow-disable-line */
  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve();
    timerFunc = function () {
      p.then(flushCallbacks);
      // 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); }
    };
    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)
    var counter = 1;
    var observer = new MutationObserver(flushCallbacks);
    var textNode = document.createTextNode(String(counter));
    observer.observe(textNode, {
      characterData: true
    });
    timerFunc = function () {
      counter = (counter + 1) % 2;
      textNode.data = String(counter);
    };
    isUsingMicroTask = true;
  } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    // Fallback to setImmediate.
    // Technically it leverages the (macro) task queue,
    // but it is still a better choice than setTimeout.
    timerFunc = function () {
      setImmediate(flushCallbacks);
    };
  } else {
    // Fallback to setTimeout.
    timerFunc = function () {
      setTimeout(flushCallbacks, 0);
    };
  }

  function nextTick (cb, ctx) {
    var _resolve;
    callbacks.push(function () {
      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(function (resolve) {
        _resolve = resolve;
      })
    }
  }
複製代碼

總結

這篇文章大概花了兩天時間才寫出來的,充分的參考了<深刻淺出vue.js>這本書,充分了理解書上關於vm.$nextTick中的每一句話,同時也對js中的事件循環有了進一步認識,對js運行機制也進一步加深。做爲前端小白,不想只侷限於調用各類API,更要知道其原理,天天進步一小步。但願你們能多多與我討論交流。

相關文章
相關標籤/搜索