Vue源碼中的nextTick的實現邏輯

咱們知道vue中有一個api。Vue.nextTick( [callback, context] )
他的做用是在下次 DOM 更新循環結束以後執行延遲迴調。在修改數據以後當即使用這個方法,獲取更新後的 DOM。那麼這個api是怎麼實現的呢?你確定也有些疑問或者好奇。下面就是個人探索,分享給你們,也歡迎你們到github上和我進行討論哈~~vue

首先貼一下vue的源碼,而後咱們再一步步的分析ios

/* @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 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
    })
  }
}

這麼多代碼,可能猛的一看,可能有點懵,沒關係,咱們一步一步抽出枝幹。首先咱們看一下這個js文件裏的nextTick的定義git

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

我將代碼精簡一下。以下所示,下面的代碼估計就比較容易看懂了。把cb函數放到會掉隊列裏去,若是支持macroTask,則利用macroTask在下一個事件循環中執行這些異步的任務,若是不支持macroTask,那就利用microTask在下一個事件循環中執行這些異步任務。github

export function nextTick (cb?: Function, ctx?: Object) {
  callbacks.push(cb)
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
}

這裏再次普及一下js的event loop的相關知識,js中的兩個任務隊列 :macrotasks、microtasksapi

macrotasks: script(一個js文件),setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks: process.nextTick, Promises, Object.observe, MutationObserverpromise

執行過程:
1.js引擎從macrotask隊列中取一個任務執行
2.而後將microtask隊列中的全部任務依次執行完
3.再次從macrotask隊列中取一個任務執行
4.而後再次將microtask隊列中全部任務依次執行完
……
循環往復瀏覽器

那麼咱們再看咱們精簡掉的代碼都是幹什麼的呢?咱們往異步隊列裏放回調函數的時候,咱們並非直接放回調函數,而是包裝了一個函數,在這個函數裏調用cb,而且用try catch包裹了一下。這是由於cb在運行時出錯,咱們不try catch這個錯誤的話,會致使整個程序崩潰掉。 咱們還精簡掉了以下代碼app

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

這段代碼是幹嗎的呢?也就是說咱們用nextTick的時候,還能夠有promise的寫法。若是沒有向nextTick中傳入cb,而且瀏覽器支持Promise的話,咱們的nextTick返回的將是一個Promise。因此,nextTick的寫法也能夠是以下這樣的異步

nextTick().then(()=>{console.log("XXXXX")})

vue源碼裏關於nextTick的封裝的思路,也給咱們一些很是有益的啓示,就是咱們平時在封裝函數的時候,要想同時指出回調和promise的話,就能夠借鑑vue中的思路。async

大體的思路咱們已經捋順了。可是爲何執行macroTimerFunc或者microTimerFunc就會在下一個tick執行咱們的回調隊列呢?下面咱們來分析一下這兩個函數的定義。首先咱們分析macroTimerFunc

let macroTimerFunc

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

從上邊的代碼能夠看出,若是瀏覽器支持setImmediate,咱們就用setImmediate,若是瀏覽器支持MessageChannel,咱們就用MessageChannel的異步特性,若是二者都不支持,咱們就降價到setTimeout
,用setTimeout來把callbacks中的任務在下一個tick中執行

macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }

分析完macroTimerFunc,下面咱們開始分析microTimerFunc,我把vue源碼中關於microTimerFunc的定義稍微精簡一下

let microTimerFunc;
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)
  }
} else {
  // fallback to macro
  microTimerFunc = macroTimerFunc
}

從上邊精簡以後的代碼,咱們能夠看到microTimerFunc的實現思路。若是支持瀏覽器支持promise,就用promise實現。若是不支持,就下降到用macroTimerFunc

over,總體邏輯就是這樣。。看着嚇人,掰開了以後好好分析一下,仍是挺簡單的。

相關文章
相關標籤/搜索