1. 程式人生 > >vue中nexttick原理(原始碼分析)

vue中nexttick原理(原始碼分析)

nexttick函式的作用是來延遲一個函式的執行。

結合vue nexttick.js原始碼進行分析:

/* @flow */
/* globals MessageChannel */
 
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'
 
const callbacks = [] // 回撥函式陣列
let pending = false // 是否正在執行的flag
 
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).
// microtasks和(macro) tasks是非同步延遲的包裹函式,2.4版本之前只用microtasks,但因為其在一些
// 情況下優先順序過高,且可能在一些連續事件中觸發,甚至是同一事件的冒泡間觸發。然而都用(macro) 
// task在state在重繪前改變也會存在一些問題。所以預設使用microtask,但也會再需要時強制改變為
// (macro) task
 
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.
// 定義(macro) task的延遲實現,setImmediate是最優選,但只在IE中可用。所以在同一迴圈中所有DOM
// 事件觸發後,要把回撥推進同一佇列中則使用MessageChannel
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { //如果有setImmediate且是原生程式碼則使用它來延遲
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' && ( //其次使用MessageChannel 
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else { // 最後考慮使用setTimeout
  /* istanbul ignore next */
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
 
// Determine microtask defer implementation.
// 定義microtask延遲的實現
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) { //支援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.
    // 在一些有問題的UIWebview中,Promise.then沒有完全中斷,但它可能卡在一個狀態:我已經將回調 
    // 推進佇列中但這個佇列並沒有重新整理。直到瀏覽器做一些其他工作,例如處理一個計時器。所以需要
    // 手動呼叫一個空的計數器來“強制”重新整理佇列
    if (isIOS) setTimeout(noop)
  }
} else { // 否則使用macroTimerFunc()
  // 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
  })
}
 
// vue原始碼中的nexttick接收兩個引數,要延遲執行的回撥函式(callback),和執行該函式的指定上下文
//(context),如果沒有上下文引數,則會預設為全域性上下文。
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) { /沒有返回context的promise
      _resolve(ctx)
    }
  })
  if (!pending) { // 如果當前沒有執行回撥則執行
    pending = true
    if (useMacroTask) { // 預設優先使用microTimerFunc()
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  // $flow-disable-line 
  if (!cb && typeof Promise !== 'undefined') { 
//如果麼有回撥,且支援promise,返回promise的resolve函式
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

我們可以看出nexttick將需要延遲的函式放到了一個非同步佇列中執行,setTimeout或Promise等,來起到延遲執行的作用。

很多用途都是用於將函式放到Dom更新後執行,比如在created生命週期中拿不到dom因為還沒渲染掛載到頁面,這時就需要將對dom的操作放到nexttick函式中。那麼為什麼nexttick中的函式能延遲到dom更新完成後呢?

因為採用的是非同步回撥,所有非同步函式都會在同步函式執行完之後在進行呼叫,而DOM的更新在同一事件迴圈中是同步的,所以能在其後執行,當然DOM操作可以放在非同步函式中,但它本身是同步的。

關於非同步函式的呼叫也有些不同,promise是在當前佇列的末尾,setTimeout是在新佇列的開頭,所以同為非同步函式,promise要比setTimeout先呼叫,即使setTimeout寫在前面。所有的同類型非同步函式也是按定義順序執行的。

本文屬於個人理解,如有錯誤,希望更正。