nextTick原理

首先呢咱們先來了解一下js的運行機制.vue

js運行機制

js執行是單線程的,它是基於事件循環的。事件循環大體分如下幾個步驟: 1.全部同步任何都在主線程上執行,造成一個執行棧(execution context stack)。 2.主線程以外,還存在一個「任務隊列」(task queue).只要異步任務有了運行結果,就在「任務隊列」之中放置一個事件。 3.一旦「執行棧」中的全部同步任務執行完畢,系統就會讀取「任務隊列」,看看裏面有哪些事件。那麼對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。 4.主線程不斷重複上面的(3).ios

主線程的執行過程就是一個tick,而全部的異步結果都是經過「任務隊列」來調度被調度。消息隊列中存放的是一個個的任務(task)。task分爲兩大類,一個是macro task(宏任務)、macro task (微任務),而且每一個marco task結束後,都要清空全部的micro task.在瀏覽器環境中,常見的macro task 有setTimeout,MessageChannel,postMessage,setImmediate,常見的micro task有MutationObsever 和Promise.then.

Vue中的nextTick在2.5+後有一個單獨的文件。在src/core/util/next-tick.js,數組

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 microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire 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 microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
let microTimerFunc
let macroTimerFunc
let 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 (macro) 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 }) } } 複製代碼

首先定義了一個callbacks,而後設置了一個狀態pending,而後定義了microTimerFunc和macroTimerFunc,這兩個根據瀏覽器的支持程度,而後作不一樣的處理。咱們來看下macroTimerFunc,先判斷當前運行環境支不支持setImmediate而且是瀏覽器原生支持的,那麼macroTimerFunc內部實現呢就去調用setImmediate,不然就執行new MessageChannel實例,拿到port2,去調用port.postMessage,而後就會執行flushCallbacks,不然的話,macroTimerFunc就會降級爲setTimeout(flushCallbacks,0)的方式。 而後看一下microtask方式的實現,若是原生瀏覽器支持promise,經過promise.then(flushCallbacks).若是不支持就直接降級爲macrotask的實現。promise

next-tick.js對外暴露了2個函數,先來看nextTick,把傳入的回調函數cb壓入callbacks數組,最後一次性地根據useMacroTask條件執行macroTimerFunc或者是microTimerFunc,而它們都會在下一個tick執行flushCallbacks,flushCallbacks的邏輯也比較簡單,對callbacks遍歷,而後執行相應的回調函數。 這裏使用callbacks而不是直接在nextTick中執行回調函數的緣由是保證在同一個tick內屢次執行nextTick,不會開啓多個異步任務,而把這些異步任務都壓成一個同步任務,在下一個tick執行完畢。瀏覽器

nextTick函數最後是當nextTick不傳cb參數的時候,提供一個Promise化的調用,好比:nextTick().then(()=>{}) 當_resolve函數執行,就會跳到then的邏輯中。 next-tick.js還對外暴露了withMacroTask函數,它是對函數作一層包裝,確保函數執行過程當中對數據任意的修改,觸發變化執行nextTick的時候強制走macroTimerFunc。好比對於一些DOM交互事件,如v-on綁定的事件回調函數的處理,會強制走macrotask。bash

咱們瞭解到數據的變化到DOM的從新渲染是一個異步過程,發生在下一個tick.這就是咱們在平時的開發過程當中,好比從服務端接口去獲取數據的時候,數據作了修改,而咱們的某些方法又依賴數據修改後的DOM變化,因此咱們就必須在nextTick後執行。 vue提供了2種調用nextTick的方式,一種是全局API vue.nextTick,一種是實例上的方法vm.$nextTick,不管咱們使用哪種,最後都是調用next-tick.js中實現的nextTick方法。markdown

相關文章
相關標籤/搜索