iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 20
0
Modern Web

技術在走,Vue.js 要有系列 第 20

|D20| 從原始碼看 Vue 響應式原理 (6) - nextTick 異步更新

介紹 Vue 異步更新前,要先了解一下 JS 運行時如何處理程式碼的異步執行

Event Loop

js 執行時是單執行緒,它基於事件循環達到異步執行。
我們用下面範例來看一下,一個事件循環(Event Loop)的週期

  1. 左邊是我們要執行的程式碼

    圖片來源:What the heck is the event loop anyway? | Philip Roberts | JSConf EU

  2. 可以看到 stack 開始運行程式碼console.log('Hi')

  1. console.log('Hi')執行完會從 stack 消失,接著執行 setTimeout

  2. setTimeout 是異步任務,要等待運行結果

  3. setTimeout 還再等待運行結果,但對 stack 來說需要繼續執行console.log('JSConfEU')

  4. 當程式碼都執行完,stack 也清空了

  5. setTimeout 運行完就把 cb function 推到任務隊列(task queue)裡

  6. 事件循環()的工作就是當 stack 清空了,就把在任務隊列(task queue)裡的 call back 推到 stack 裡繼續執行 console.log('there')

其實異步任務分兩種

  • macro task: 常見的有 setTimeout、setInterval 等
  • micro task: 常見的有 Promise.then、MutationObserver 等
    每個 macro task 结束後,都要清空所有的 micro task

nextTick

異步更新可以避免過度渲染,以下面範例來看

// Task 1
this.content = '第一次'

// Task 2
this.content = '第二次'

// Task 3
this.content = '第三次'

觀察數據的多次變化,我們對同一個狀態修改了三次,如果採取同步更新,同步調用 watcher.run 就會觸發 3 次 DOM 操作,但其實只需要把第3次的內容渲染出就好。

所以 Vue 採取異步執行,推入 task queue 中等待下一個 Event Loop 再操作 task queue 中的 Watcher,因為是同一個 Watcher,只會調用一次 watcher.run,達到只觸發 1 次 DOM 操作。

nextTick 是 Vue 用來把更新狀態的操作包裝成異步操作派發出去

// src/core/util/next-tick.js

export function nextTick (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
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }

  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

nextTick 把傳入的callback function cb 推到 callbacks 陣列中,最後根據 useMacroTask 條件判斷要執行 macroTimerFuncmicroTimerFunc 派發方式

macroTimerFuncmicroTimerFunc 實現如下

// packages/weex-vue-framework/factory.js

if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = function () {
    setImmediate(flushCallbacks);
  };
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  var channel = new MessageChannel();
  var port = channel.port2;
  channel.port1.onmessage = flushCallbacks;
  macroTimerFunc = function () {
    port.postMessage(1);
  };
} else {
  /* istanbul ignore next */
  macroTimerFunc = function () {
    setTimeout(flushCallbacks, 0);
  };
}
// packages/weex-vue-framework/factory.js

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  var p = Promise.resolve();
  microTimerFunc = function () {
    p.then(flushCallbacks);
    if (isIOS) { setTimeout(noop); }
  };
} else {
  // fallback to macro
  microTimerFunc = macroTimerFunc;
}

不管是派發 macroTimerFuncmicroTimerFunc,派發的任務對象都是 flushCallbacks

// src/core/util/next-tick.js

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

flushCallbacks 處理幾件事

  1. Vue 中每產生一個更新任務,該任務會被推進 callbacks 陣列中。callbacks 陣列是實現任務隊列(task queue)的形式
  2. 這個任務隊列(callbacks 陣列)再推進 micro 或 macro 隊列前,會先去檢查當前是否有異步任務正在執行(即檢查 pending)
  3. pending 是用來保證更新任務的有序進行,避免發生混亂
  4. 對當前任務隊列(callbacks 陣列)的任務進行派發(推進 micro 或 macro 隊列)和執行

上一篇
|D19| 從原始碼看 Vue 響應式原理 (5) - Watcher
下一篇
|D21| 從原始碼看 Vue 響應式原理 (7) - computed
系列文
技術在走,Vue.js 要有30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Chris
iT邦新手 4 級 ‧ 2019-10-06 19:45:59

任務隊列(task queue)分兩種?

mangoSu iT邦新手 5 級 ‧ 2019-10-06 21:09:52 檢舉

寫錯惹!是異步任務分為 macro task 和 micro task 才對

我要留言

立即登入留言