1. 程式人生 > >Vue中之nextTick函數源碼分析

Vue中之nextTick函數源碼分析

row watcher 準備 cti 方法調用 prop ews html line

Vue中之nextTick函數源碼分析

1. 什麽是Vue.nextTick()?
官方文檔解釋如下:
在下次DOM更新循環結束之後執行的延遲回調。在修改數據之後立即使用這個方法,獲取更新後的DOM。

2. 為什麽要使用nextTick?

<!DOCTYPE html>
<html>
  <head>
    <title>演示Vue</title>
    <script src="https://tugenhua0707.github.io/vue/vue1/vue.js"></script>
  </head
> <body> <div id="app"> <template> <div ref="list"> {{name}} </div> </template> </div> <script> new Vue({ el: #app, data: { name: aa }, mounted() {
this.updateData(); }, methods: { updateData() { var self = this; this.name = bb; console.log(this.$el.textContent); // aa this.$nextTick(function(){ console.log(self.$el.textContent); // bb }); } } });
</script> </body> </html>

如上代碼 在頁面視圖上顯示bb,但是當我在控制臺打印的時候,獲取的文本內容還是 aa,但是使用 nextTick後,獲取的文本內容就是最新的內容bb了,因此在這種情況下,我們可以使用nextTick函數了。
上面的代碼為什麽改變this.name = ‘bb‘;後,再使用console.log(this.$el.textContent);打印的值還是aa呢?那是因為設置name的值後,DOM還沒有更新到,所以獲取值還是之前的值,但是我們放到nextTick函數裏面的時候,代碼會在DOM更新後執行,因此DOM更新後,再去獲取元素的值就可以獲取到最新值了。

理解DOM更新:在VUE中,當我們修改了data中的某一個值後,並不會立即反應到該el中,vue將對更改的數據放到watcher的一個異步隊列中,只有在當前任務空閑時才會執行watcher隊列任務,這就有一個延遲時間,因此放到 nextTick函數後就可以獲取該el的最新值了。如果我們把上面的nextTick
改成setTimeout也是可以的。

3. Vue源碼詳解之nextTick(源碼在 vue/src/core/util/env.js)

在理解nextTick源碼之前,我們先來理解下 html5中新增的 MutationObserver的API,它的作用是用來監聽DOM變動的接口,它能監聽一個dom對象發生的子節點刪除,屬性修改,文本內容修改等等,具體用戶看我這邊博客(http://www.cnblogs.com/tugenhua0707/articles/6849948.html).
nextTick源碼如下:

export const nextTick = (function () {
  const callbacks = []
  let pending = false
  let timerFunc

  function nextTickHandler () {
    pending = false;
    /*
     之所以要slice復制一份出來是因為有的cb執行過程中又會往callbacks中加入內容,比如$nextTick的回調函數裏又有$nextTick,
     那麽這些應該放入到下一個輪次的nextTick去執行,所以拷貝一份,遍歷完成即可,防止一直循環下去。
     */
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
      copies[i]()
    }
  }

  // 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 if */
  /*
    nextTick行為利用了microtask隊列, 先使用 Promise.resolve().then(nextTickHandler)來將異步回調
    放入到microtask中,Promise 和 MutationObserver都可以使用,但是 MutationObserver 在IOS9.3以上的
    WebView中有bug,因此如果滿足第一項的話就可以執行,如果沒有原生Promise就用 MutationObserver。
   */
  if (typeof Promise !== ‘undefined‘ && isNative(Promise)) {
    var p = Promise.resolve()
    var logError = err => { console.error(err) }
    timerFunc = () => {
      p.then(nextTickHandler).catch(logError)
      // 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)
    }
  } else if (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 IE11, iOS7, Android 4.4
    /*
     創建一個MutationObserver,observe監聽到DOM改動之後執行的回調 nextTickHandler 
     */
    var counter = 1
    var observer = new MutationObserver(nextTickHandler)
    var textNode = document.createTextNode(String(counter));
    // 使用MutationObserver的接口,監聽文本節點的字符內容
    observer.observe(textNode, {
      characterData: true
    });
    /*
     每次執行timerFunc函數都會讓文本節點的內容在0/1之間切換,切換之後將新賦值到那個我們MutationObserver監聽的文本節點上去。
     */
    timerFunc = () => {
      counter = (counter + 1) % 2
      textNode.data = String(counter)
    }
  } else {
    // fallback to setTimeout
    /*
     如果上面的兩種都不支持的話,我們就使用setTimeout來執行
     */
    timerFunc = () => {
      setTimeout(nextTickHandler, 0)
    }
  }

  return function queueNextTick (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)
      }
    });
    /* 如果pending為true,表明本輪事件循環中已經執行過 timerFunc(nextTickHandler, 0) */
    if (!pending) {
      pending = true
      timerFunc()
    }
    if (!cb && typeof Promise !== ‘undefined‘) {
      return new Promise((resolve, reject) => {
        _resolve = resolve
      })
    }
  }
})()

整體思路理解:首先 nextTick 是一個閉包函數,代碼立即執行,在理解整體代碼之前,我們先來看個類似的demo,如下代碼:

<!DOCTYPE html>
<html>
  <head>
    <title>演示Vue</title>
  </head>
  <body>
    <div id="app">
      
    </div>
    <script>
      var nextTick = (function(){
        return function queueNextTick(cb, ctx) {
          if (cb) {
            try {
              cb.call(ctx)
            } catch (e) {
              console.log(出錯了);
            }
          }
        }
      })();

      // 方法調用
      nextTick(function(){
        console.log(2);  // 打印2
      })
    </script>
  </body>
</html>

demo代碼和上面的代碼很類似。
我們也可以再來抽離使用nextTick做demo代碼如下:

var nextTick2 = (function(){
  const callbacks = [];
  let pending = false;
  let timerFunc;

  function nextTickHandler () {
    pending = false
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
      copies[i]()
    }
  }
  if (typeof Promise !== ‘undefined‘) {
    var p = Promise.resolve()
    var logError = err => { console.error(err) }
    timerFunc = () => {
      p.then(nextTickHandler).catch(logError)
    }
  } else if (typeof MutationObserver !== ‘undefined‘ ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === ‘[object MutationObserverConstructor]‘
  ) {
    // use MutationObserver where native Promise is not available,
    // e.g. PhantomJS IE11, iOS7, Android 4.4
    var counter = 1
    var observer = new MutationObserver(nextTickHandler)
    var textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
      characterData: true
    })
    timerFunc = () => {
      counter = (counter + 1) % 2
      textNode.data = String(counter)
    }
  } else {
    // fallback to setTimeout
    /* istanbul ignore next */
    timerFunc = () => {
      setTimeout(nextTickHandler, 0)
    }
  }
  return function queueNextTick (cb, ctx) {
    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()
    }
    if (!cb && typeof Promise !== ‘undefined‘) {
      return new Promise((resolve, reject) => {
        _resolve = resolve
      })
    }
  }
})();
nextTick2(function(){
  console.log(2222);
});

如上代碼是nextTick源碼的抽離,為了更好的理解nextTick,做了如上的demo。
我們再來理解一下整體的代碼的含義;
先定義數組 callbacks = [];來存放所有需要執行的回調函數,定義let pending = false;判斷本輪事件是否執行過 timerFunc(nextTickHandler, 0)這個函數,為true說明執行過 timeFunc函數,接著定義nextTickHandler函數,該函數的作用是依次遍歷數組callbacks保存的函數,依次執行;
請看源代碼如下:

function nextTickHandler () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

然後就是三個判斷了,代碼如下:

if (typeof Promise !== ‘undefined‘ && isNative(Promise)) {
  var p = Promise.resolve();
  var logError = err => { console.error(err) }
  timerFunc = () => {
    p.then(nextTickHandler).catch(logError);
} else if (typeof MutationObserver !== ‘undefined‘ && (
    isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === ‘[object MutationObserverConstructor]‘
)){
  var counter = 1
  var observer = new MutationObserver(nextTickHandler)
  var textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
} else {
  timerFunc = () => {
    setTimeout(nextTickHandler, 0)
  }
}

首先判斷是否支持Promise對象,如果支持的話,定義了timeFunc()函數,為了下一步調用做準備,然後繼續判斷是否支持該對象 MutationObserver,
如果支持的話,創建一個文本節點,監聽該節點數據是否發生改變,如果發生改變的話,調用timerFunc函數,counter值會在0/1切換,如果值改變了的話,
把該數據值賦值到data屬性上面去,那麽data屬性發生改變了,就會重新渲染頁面(因為vue是通過Object.defineProperty來監聽屬性值是否發生改變),
如果上面兩種情況都不滿足的話,那麽直接使用setTimeout來執行nextTickHandler函數了;
最後nextTick代碼返回一個函數,代碼如下:

return function queueNextTick (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()
  }
  if (!cb && typeof Promise !== ‘undefined‘) {
    return new Promise((resolve, reject) => {
      _resolve = resolve
    })
  }
}

代碼的含義是:傳入的cb是否是函數,ctx參數是否是一個對象,如果cb是一個函數的話,使用cb.call(ctx), 如果timerFunc沒有執行過的話,那麽pending為
false,因此執行 timerFunc()函數。基本的思路就是這樣的。

Vue中之nextTick函數源碼分析