1. 程式人生 > 實用技巧 >vue中nextTick的原始碼解讀,分析js事件迴圈機制

vue中nextTick的原始碼解讀,分析js事件迴圈機制

程式碼位置

  • nextTick的實現在src/core/util/next-tick.js中。

主要程式碼塊

  • 根據當前環境,選擇實現nextTick非同步回撥的途徑。
// 首先是看當前環境支不支援Promise,如果支援Promise就使用Promise,添加了一個微任務
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]'
)) { // 如果不支援Promise,就看支援不支援MutationObserver,相當於添加了一個微任務
  // e.g. PhantomJS, iOS7, Android 4.4
  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)) { // 再看支不支援setImmediate,添加了一個巨集任務,在下一輪的事件佇列中呼叫執行
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else { // 都不支援就使用setTimeout來代替,添加了一個巨集任務,在下一輪的事件佇列中呼叫執行
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
  • nextTick的實現。
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve;
  callbacks.push(() => {
    // 判斷nextTick中有沒有傳入回撥,傳入了回撥就把回撥放到callbacks的回撥佇列中
    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') { // 當沒有傳入回撥,並且Promise可以使用時,把預設的回撥變成Promise的resolve操作
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
  • flushCallbacks清空佇列的實現
function flushCallbacks () {
  pending = false // 清空佇列之後,當前的狀態又是pending等待中
  const copies = callbacks.slice(0) // 將回調佇列拷貝一份出來
  callbacks.length = 0 // 清空回撥佇列
  for (let i = 0; i < copies.length; i++) {
    copies[i]() // 以此呼叫回撥佇列的方法
  }
}

原理解讀

  • nextTick的本質是利用了JS的事件佇列的機制,動態的新增微任務或者巨集任務,使得在nextTick
    中的回撥可以非同步呼叫,通常是在頁面的data更新過之後。

事件佇列

  • JS存在一個主執行緒,專門用來執行所有的同步任務,形成了一個執行棧。
  • 主執行緒之外,存在一個任務佇列,非同步任務的回撥就放在任務佇列中。
  • 一旦當前執行棧中的同步任務全部執行完畢,系統就會去依次讀取任務佇列中的回撥,放到主執行緒上去執行。
  • 進入下一個事件迴圈,重複以上三步。
事件佇列中的常見巨集任務和微任務
  • 巨集任務
    1. I/O
    2. setTimeout,將事件插入到了事件佇列,直到過了設定的時間之後才可以執行,不一定是剛好那個時間,而且必須要等待當前的執行棧執行完畢才會去呼叫,即使時間間隔為0。(瀏覽器預設還是會給一個1ms的延遲)
    3. setInterval,與setTimeout類似。
    4. setImmediate,如果不發生任何I/O操作,那麼它會比setTimeout(fn, 0)要快,與setTimeout(fn, 0)功能類似,兩個方法不能確定誰先執行。但是當主執行緒執行的時間比較長(大於setTimeout的延遲時間)的時候,往往setTimeout(fn, 0)要比setImmediate先執行。
    5. requestAnimationFrame,間隔時間是根據瀏覽器的重新整理頻率決定的,與setTimeout也有點類似。
  • 微任務
    1. process.nextTick,將事件插入到主執行緒的末尾,也就是所有微任務和巨集任務之前。
    2. MutationObserver,當監聽的dom發生變化時,加入微任務佇列。
    3. Promise.then(catch,finally),直接加入微任務佇列。
  • 執行順序的問題:
    1. 一次事件迴圈中,一定要把微任務執行完畢之後才會去執行巨集任務,包括微任務中動態新增的微任務。
    2. 看下面的程式碼。
      // 當只有三個巨集任務的時候,結果是123/132/213,都有可能
      setTimeout(() => {
          console.log(1)
      }, 0)
      setImmediate(() => {
          console.log(2)
      })
      setTimeout(() => {
          console.log(3)
      }, 0)
      
    3. 再看另一種情況
      // 當主執行緒存在其它任務時,基本上就是setImmediate要比setTimeout(fn, 0)要執行的晚一點,所以嘗試了很多次還是48756132
      setTimeout(() => {
          console.log(1) // 巨集任務第一步
      }, 0)
      setImmediate(() => {
          console.log(2) // 巨集任務第三步
      })
      setTimeout(() => {
          console.log(3) // 巨集任務第二步
      }, 0)
      new Promise((resolve) => {
          console.log(4); // 主執行緒第一步
          resolve();
      }).then(() => {
          console.log(5); // 微任務佇列第二步
      }).finally(() => {
          console.log(6) // 微任務佇列第三步
      });
      process.nextTick(() => {
          console.log(7) // 主執行緒末尾執行,微任務第一步
      });
      console.log(8); // 主執行緒第二步
      
    4. 最後看一種情況
      // 由於必須要先把此次事件佇列中的微任務執行完畢後才可以去執行巨集任務
      // 又因為在微任務中定義的微任務依然需要在此次事件迴圈中執行完
      // 微任務或巨集任務中定義的巨集任務會放到下一次的事件迴圈中去執行
      // 所以最終結果就是:1 > 4 > 3 > 5 > 7 > 6 > 2 > 8
      process.nextTick(() => {
          console.log(1); // 微任務第一步
      
          setTimeout(() => {
              console.log(2); // 下一輪的巨集任務第一步
          }, 0)
      
          Promise.resolve(3).then((data) => {
              console.log(data) // 微任務第三步
          });
      })
      
      Promise.resolve(4).then(data => {
          console.log(data); // 微任務第二步
      })
      
      setTimeout(() => {
          console.log(5); // 巨集任務第一步
      
          Promise.resolve(6).then(data => {
              console.log(data); // 下一輪的微任務第二步
          })
      
          process.nextTick(() => {
              console.log(7); // 下一輪的微任務第一步
          })
      
          setTimeout(() => {
              console.log(8); // 下一輪的巨集任務第二步
          })
      })