

本文结构
- 带着问题看这篇文章
- event loop中任务的执行顺序
- 微任务 & 宏任务
- Vue中nextTick的实现
- 对nextTick这个词的理解
本文共计:1940字0图
预计阅读时间:3min50s
(关于 Event Loop的细节,我写过一篇很详细的总结试图解释清楚Javascript Event Loop[1]。)
根据event loop的执行机制,微任务的调度优先级比宏任务高.
微任务异步API:Promise.then,MutationObserver
宏任务异步API:setTimeout,MessageChannel,postMessage,setImmediate
vue中的 nextTick 实现在 util 模块的单个文件中,代码总共100多行:
// src\core\util\next-tick.js
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, 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]( "i")
  }
}
let timerFunc
// nextTick行为利用了微任务队列,微任务队列可以通过原生Promise.then或MutationObserver访问到。 
// MutationObserver具有更广泛的支持,但是在iOS> = 9.3.3中的UIWebView中,在触摸事件处理程序中触发时会发生错误。触发几次后,它将完全停止工作
// 因此,如果原生Promise可用,优先使用Promise:
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // 在有问题的UIWebViews中,会出现奇怪的状态:微任务队列中有回调但是不被清空,直到浏览器有其他任务,例如处理计时器
    // 因此此处使用一个空计时器,来强制触发微任务队列执行
    if (isIOS) setTimeout(noop)
  }
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // setImmediate,宏任务,但是相比 setTimeout 是个更好的选择
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // setTimeout 0 宏任务
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
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
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
源码中, Vue.nextTick/vm.$nextTick 的具体逻辑:
callbacks 数组,用于存储 nextTick 接口传来的回调函数们flushCallbacks  方法,用于遍历执行 callbacks 数组中的所有回调函数timerFunc 方法,将 flushCallbacks  方法作为回调任务,添加到异步队列Promise.then > MutationObserver > setImmediate > setTimeout 0一句话总结:将回调作为异步任务,添加到(微/宏)任务队列,在当前调用栈清空后再执行。
对于tick我的理解是:每次从调用栈开始有函数帧,直到调用栈被清空为止的过程,这个过程可能是:
对于nextTick(cb):回调函数cb不在当前调用栈执行期间立即执行,而是被立即添加在任务队列中,在当前调用栈清空后执行。
使用nextTick的目的:必须等待当前调用栈的后续代码执行完,才能执行回调,例如这种情况:回调函数中,需要依赖上一个调用栈操作后的某些状态。
举个例子:
画一个 echarts 图表,希望根据数据的长度来动态调整图表的宽度
..
<template>
	<div id="chart" :style='{width:chartWidth,height:"200px"}'
</template>
...
this.chartWidth = getWidthByData(data)
this.nextTick(()=>{
	let chart= echarts.init(document.getElementById('chart'))
	chart.setOption({...})  //echarts渲染
})
宽度属性chartWidth存在vue data中,由于vue data是响应式的,变更data值后,div#chart的宽度并不是立即变更的,中间存在一系列过程:
setter 向其依赖的(vue组件的renderWatcher)发布更新因此在chartWidth变更后,对应的dom宽度不是立即更新的,此时如果立即执行echarts的渲染工作,会导致echarts不能按照最新宽度来渲染。
- END -