從JS事件循環(Event Loop)機制到vue.nextTick的實現

衆所周知,爲了與瀏覽器進行交互,Javascript是一門非阻塞單線程腳本語言。vue

  1. 爲什麼單線程? 由於若是在DOM操做中,有兩個線程一個添加節點,一個刪除節點,瀏覽器並不知道以哪一個爲準,因此只能選擇一個主線程來執行代碼,以防止衝突。雖然現在添加了webworker等新技術,但其依然只是主線程的子線程,並不能執行諸如I/O類的操做。長期來看,JS將一直是單線程。
  2. 爲什麼非阻塞?由於單線程意味着任務須要排隊,任務按順序執行,若是一個任務很耗時,下一個任務不得不等待。因此爲了不這種阻塞,咱們須要一種非阻塞機制。這種非阻塞機制是一種異步機制,即須要等待的任務不會阻塞主執行棧中同步任務的執行。這種機制是以下運行的:
  • 全部同步任務都在主線程上執行,造成一個執行棧(execution context stack 
  • 等待任務的回調結果進入一種任務隊列(task queue)
  • 當主執行棧中的同步任務執行完畢後纔會讀取任務隊列,任務隊列中的異步任務(即以前等待任務的回調結果)會塞入主執行棧,
  • 異步任務執行完畢後會再次進入下一個循環。此即爲今天文章的主角事件循環(Event Loop) 

用一張圖展現這個過程:ios

1.macro task與micro task

在實際狀況中,上述的任務隊列(task queue)中的異步任務分爲兩種:微任務(micro task)宏任務(macro task)git

  • micro task事件:Promises(瀏覽器實現的原生Promise)MutationObserverprocess.nextTick 
    <br />
  • macro task事件:setTimeoutsetIntervalsetImmediateI/OUI rendering
    這裏注意:script(總體代碼)即一開始在主執行棧中的同步代碼本質上也屬於macrotask,屬於第一個執行的task 

microtask和macotask執行規則:github

    • macrotask按順序執行,瀏覽器的ui繪製會插在每一個macrotask之間
    • microtask按順序執行,會在以下狀況下執行:
      • 每一個callback以後,只要沒有其餘的JS在主執行棧中
      • 每一個macrotask結束時

下面來個簡單例子:web

console.log(1);promise

 

setTimeout(function() {瀏覽器

  console.log(2);app

}, 0);dom

new Promise(function(resolve,reject){異步

    console.log(3)

    resolve()

}).then(function() {

  console.log(4);

}).then(function() {

  console.log(5);

});

console.log(6);


一步一步分析以下:

 

1.同步代碼做爲第一個macrotask,按順序輸出:1 3 6

2.microtask按順序執行:4 5

 

3.microtask清空後執行下一個macrotask:2

再來一個複雜的例子:

// Let's get hold of those elements

var outer = document.querySelector('.outer');

var inner = document.querySelector('.inner');

// Let's listen for attribute changes on the

// outer element

new MutationObserver(function() {

  console.log('mutate');

}).observe(outer, {

  attributes: true

});

// Here's a click listener…

function onClick() {

  console.log('click');

 

  setTimeout(function() {

    console.log('timeout');

  }, 0);

 

  Promise.resolve().then(function() {

    console.log('promise');

  });

 

  outer.setAttribute('data-random', Math.random());

}

// …which we'll attach to both elements

inner.addEventListener('click', onClick);

outer.addEventListener('click', onClick);

假設咱們建立一個有裏外兩部分的正方形盒子,裏外都綁定了點擊事件,此時點擊內部,代碼會如何執行?一步一步分析以下:

  • 1.觸發內部click事件,同步輸出:click
  • 2.setTimeout回調結果放入macrotask隊列
  • 3.promise回調結果放入microtask
  • 4.Mutation observers放入microtask隊列,主執行棧中onclick事件結束,主執行棧清空
  • 5.依序執行microtask隊列中任務,輸出:promise mutate
  • 6.注意此時事件冒泡,外部元素再次觸發onclick回調,因此按照前5步再次輸出:click promise mutate(咱們能夠注意到事件冒泡甚至會在microtask中的任務執行以後,microtask優先級很是高) 
  • 7.macrotask中第一個任務執行完畢,依次執行macrotask中剩下的任務輸出:timeout timeout

 

2.vue.nextTick實現

 

Vue.js 裏是數據驅動視圖變化,因爲 JS 執行是單線程的,在一個 tick 的過程當中,它可能會屢次修改數據,但 Vue.js 並不會傻到每修改一次數據就去驅動一次視圖變化,它會把這些數據的修改所有 push 到一個隊列裏,而後內部調用 一次 nextTick 去更新視圖,因此數據到 DOM 視圖的變化是須要在下一個 tick 才能完成。這即是咱們爲何須要vue.nextTick.

 

這樣一個功能和事件循環很是類似,在每一個 task 運行完之後,UI 都會重渲染,那麼很容易想到在 microtask 中就完成數據更新,當前 task 結束就能夠獲得最新的 UI 了。反之若是新建一個 task 來作數據更新,那麼渲染就會進行兩次。

 

因此在vue 2.4以前使用microtask實現nextTick,直接上源碼

 

var counter = 1var observer = new MutationObserver(nextTickHandler)var textNode = document.createTextNode(String(counter))

 

observer.observe(textNode, {

 

    characterData: true

 

})

 

timerFunc = () => {

 

    counter = (counter + 1) % 2

 

    textNode.data = String(counter)

 

}

能夠看到使用了MutationObserver

然而到了vue 2.4以後卻混合�使用microtask macrotask來實現,源碼以下

/* @flow *//* globals MessageChannel */

import { noop } from 'shared/util'import { handleError } from './error'import { isIOS, isNative } from './env'

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]()

  }

}

// Here we have async deferring wrappers using both micro and macro tasks.// In < 2.4 we used micro tasks everywhere, but there are some scenarios where// micro tasks have too high a priority and fires in between supposedly// sequential events (e.g. #4521, #6690) or even between bubbling of the same// event (#6566). However, using macro tasks everywhere also has subtle problems// when state is changed right before repaint (e.g. #6813, out-in transitions).// Here we use micro task by default, but expose a way to force macro task when// needed (e.g. in event handlers attached by v-on).let microTimerFunclet macroTimerFunclet useMacroTask = false

// Determine (macro) Task defer implementation.// Technically setImmediate should be the ideal choice, but it's only available// in IE. The only polyfill that consistently queues the callback after all DOM// events triggered in the same loop is by using MessageChannel./* istanbul ignore if */if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {

  macroTimerFunc = () => {

    setImmediate(flushCallbacks)

  }

} else if (typeof MessageChannel !== 'undefined' && (

  isNative(MessageChannel) ||

  // PhantomJS

  MessageChannel.toString() === '[object MessageChannelConstructor]'

)) {

  const channel = new MessageChannel()

  const port = channel.port2

  channel.port1.onmessage = flushCallbacks

  macroTimerFunc = () => {

    port.postMessage(1)

  }

} else {

  /* istanbul ignore next */

  macroTimerFunc = () => {

    setTimeout(flushCallbacks, 0)

  }

}

// Determine MicroTask defer implementation./* istanbul ignore next, $flow-disable-line */if (typeof Promise !== 'undefined' && isNative(Promise)) {

  const p = Promise.resolve()

  microTimerFunc = () => {

    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)

  }

} else {

  // fallback to macro

  microTimerFunc = macroTimerFunc

}

/**

 * Wrap a function so that if any code inside triggers state change,

 * the changes are queued using a Task instead of a MicroTask.

 */export function withMacroTask (fn: Function): Function {

  return fn._withTask || (fn._withTask = function () {

    useMacroTask = true

    const res = fn.apply(null, arguments)

    useMacroTask = false

    return res

  })

}

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()

    }

  }

  // $flow-disable-line

  if (!cb && typeof Promise !== 'undefined') {

    return new Promise(resolve => {

      _resolve = resolve

    })

  }

}

能夠看到使用setImmediateMessageChannelmascrotask事件來實現nextTick

爲何會如此修改,其實看以前的事件冒泡例子就能夠知道,因爲microtask優先級過高,甚至會比冒泡快,因此會形成一些詭異的bug。如 issue #4521#6690#6556;可是若是所有都改爲 macro task,對一些有重繪和動畫的場景也會有性能影響,如 issue #6813。因此最終 nextTick 採起的策略是默認走 micro task,對於一些 DOM 交互事件,如 v-on 綁定的事件回調函數的處理,會強制走 macro task

相關文章
相關標籤/搜索