NextTick原理
Node.js中的process.nextTick
Node.js中有一个nextTick
函数和Vue中的nextTick
命名一致,很容易让人联想到一起(Node.js的Event Loop和浏览器的Event Loop有差异)。重点讲解一下Node.js中的nextTick
的执行机制,简单的举个栗子:
setTimeout(function() {
console.log('timeout')
})
process.nextTick(function(){
console.log('nextTick 1')
})
new Promise(function(resolve){
console.log('Promise 1')
resolve();
console.log('Promise 2')
}).then(function(){
console.log('Promise Resolve')
})
process.nextTick(function(){
console.log('nextTick 2')
})
在Node环境(10.3.0版本)中打印的顺序: Promise 1
> Promise 2
> nextTick 1
> nextTick 2
> Promise Resolve
> timeout
在Node.js的v10.x版本中对于process.nextTick
的说明如下:
The process.nextTick() method adds the callback to the "next tick queue". Once the current turn of the event loop turn runs to completion, all callbacks currently in the next tick queue will be called. This is not a simple alias to setTimeout(fn, 0). It is much more efficient. It runs before any additional I/O events (including timers) fire in subsequent ticks of the event loop.
Vue的API命名nextTick
Vue官方对nextTick
这个API的描述:
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
// DOM 更新了
})
// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick()
.then(function () {
// DOM 更新了
})
2.1.0 起新增:如果没有提供回调且在支持 Promise 的环境中,则返回一个 Promise。请注意 Vue 不自带 Promise 的 polyfill,所以如果你的目标浏览器不原生支持 Promise (IE:你们都看我干嘛),你得自己提供 polyfill。 0
可能你还没有注意到,Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MessageChannel,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。
例如,当你设置 vm.someData = 'new value' ,该组件不会立即重新渲染。当刷新队列时,组件会在事件循环队列清空时的下一个“tick”更新。多数情况我们不需要关心这个过程,但是如果你想在 DOM 状态更新后做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员沿着“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们确实要这么做。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。
Vue对于这个API的感情是曲折的,在2.4版本、2.5版本和2.6版本中对于nextTick
进行反复变动,原因是浏览器对于微任务的不兼容性影响、微任务和宏任务各自优缺点的权衡。
看以上流程图,如果Vue使用setTimeout
等宏任务函数,那么势必要等待UI渲染完成后的下一个宏任务执行,而如果Vue使用微任务函数,无需等待UI渲染完成才进行nextTick
的回调函数操作,可以想象在JS引擎线程和GUI渲染线程之间来回切换,以及等待GUI渲染线程的过程中,浏览器势必要消耗性能,这是一个严谨的框架完全需要考虑的事情。
当然这里所说的只是nextTick
执行用户回调之后的性能情况考虑,这中间当然不能忽略flushBatcherQueue
更新Dom的操作,使用异步函数的另外一个作用当然是要确保同步代码执行完毕Dom更新性能优化(例如同步操作对响应式数据使用for循环更新一千次,那么这里只有一次DOM更新而不是一千次)。
到了这里,对于Vue中nextTick
函数的命名应该是了然于心了,当然这个命名不知道和Node.js的process.nextTick
还有没有什么必然联系。
Vue中NextTick源码(这里加了一些简单的注释说明)
2.5版本
/* @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]()
}
}
// 在2.4中使用了microtasks ,但是还是存在问题,
// 在2.5版本中组合使用macrotasks和microtasks,组合使用的方式是对外暴露withMacroTask函数
// 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).
// 2.5版本在nextTick中对于调用microtask(微任务)还是macrotask(宏任务)声明了两个不同的变量
let microTimerFunc
let macroTimerFunc
// 默认使用microtask(微任务)
let useMacroTask = false
// 这里主要定义macrotask(宏任务)函数
// macrotask(宏任务)的执行优先级
// setImmediate -> MessageChannel -> setTimeout
// setImmediate是最理想的选择
// 最Low的状况是降级执行setTimeout
// 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)
}
}
// 这里主要定义microtask(微任务)函数
// microtask(微任务)的执行优先级
// Promise -> macroTimerFunc
// 如果原生不支持Promise,那么执行macrotask(宏任务)函数
// 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
}
// 对外暴露withMacroTask 函数
// 触发变化执行nextTick时强制执行macrotask(宏任务)函数
/**
* 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
try {
return fn.apply(null, arguments)
} finally {
useMacroTask = false
}
})
}
// 这里需要注意pending
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
})
}
}
2.6版本
/* @flow */
/* globals MutationObserver */
import { noop } from "shared/util";
import { handleError } from "./error";
import { isIE, isIOS, isNative } from "./env";
export let isUsingMicroTask = false; // 是否启用微任务开关标识符
const callbacks = []; // 回调队列
let pending = false; // 异步控制开关,标记是否正在执行回调函数
function flushCallbacks() {
pending = false;
// 防止nextTick里有nextTick出现的问题,执行前备份并清空回调队列。
const copies = callbacks.slice(0);
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}
/**
*
*
* 第一步环境判断:主要是判断用哪个宏任务或微任务,因为宏任务优先级大于微任务的,所以成先使用微任务,判断顺序如下
*
* Promise
* MutationObserver
* setImmediate
* setTimeout
*
*
* */
// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc;
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== "undefined" && isNative(Promise)) {
const p = Promise.resolve();
timerFunc = () => {
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);
};
isUsingMicroTask = true;
}
else if (
!isIE &&
typeof MutationObserver !== "undefined" &&
(isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === "[object MutationObserverConstructor]")
) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
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);
};
isUsingMicroTask = true;
} else if (typeof setImmediate !== "undefined" && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
/**
*
* 第二步:执行异步函数 执行回调队列
* 把传入的回调函数放进回调队列 callbacks。
* 执行保存的异步任务 timeFunc,遍历 callbacks 执行相应的回调函数。
*
* */
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();
}
// 如果没有提供回调函数,并且支持 Promise,就返回一个 Promise
// $flow-disable-line
if (!cb && typeof Promise !== "undefined") {
return new Promise((resolve) => {
_resolve = resolve;
});
}
}
3.0版本
源码版本:3.2.11
,源码地址:packages/runtime-core/src/sheduler.ts
const resolvedPromise: Promise<any> = Promise.resolve()
let currentFlushPromise: Promise<void> | null = null
export function nextTick<T = void>(this: T, fn?: (this: T) => void): Promise<void> {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p
}
可以看出 nextTick 接受一个函数为参数,同时会创建一个微任务
在我们页面调用 nextTick
的时候,会执行该函数,把我们的参数 fn
赋值给 p.then(fn)
,在队列的任务完成后,fn 就执行了
由于加了几个维护队列的方法,所以执行顺序是这样的:
queueJob` -> `queueFlush` -> `flushJobs` -> `nextTick参数的 fn
现在不知道都是干嘛的不要紧,几分钟后你就会清楚了
我们按顺序来,先看一下入口函数 queueJob
是在哪里调用的,看代码
// packages/runtime-core/src/renderer.ts - 1555行
function baseCreateRenderer(){
const setupRenderEffect: SetupRenderEffectFn = (...) => {
const effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(instance.update), // 当作参数传入
instance.scope
)
}
}
在 ReactiveEffect
这边接收过来的形参就是 scheduler
,最终被用到了下面这里,看过响应式源码的这里就熟悉了,就是派发更新的地方
// packages/reactivity/src/effect.ts - 330行
export function triggerEffects(
...
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
然后是 queueJob
里面干了什么?我们一个一个的来
queueJob()
该方法负责维护主任务队列,接受一个函数作为参数,为待入队任务,会将参数 push
到 queue
队列中,有唯一性判断。会在当前宏任务执行结束后,清空队列
const queue: SchedulerJob[] = []
export function queueJob(job: SchedulerJob) {
// 主任务队列为空 或者 有正在执行的任务且没有在主任务队列中 && job 不能和当前正在执行任务及后面待执行任务相同
if ((!queue.length ||
!queue.includes( job, isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex )
) && job !== currentPreFlushParentJob
) {
// 可以入队就添加到主任务队列
if (job.id == null) {
queue.push(job)
} else {
// 否则就删除
queue.splice(findInsertionIndex(job.id), 0, job)
}
// 创建微任务
queueFlush()
}
}
queueFlush()
该方法负责尝试创建微任务,等待任务队列执行
let isFlushing = false // 是否正在执行
let isFlushPending = false // 是否正在等待执行
const resolvedPromise: Promise<any> = Promise.resolve() // 微任务创建器
let currentFlushPromise: Promise<void> | null = null // 当前任务
function queueFlush() {
// 当前没有微任务
if (!isFlushing && !isFlushPending) {
// 避免在事件循环周期内多次创建新的微任务
isFlushPending = true
// 创建微任务,把 flushJobs 推入任务队列等待执行
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
flushJobs()
该方法负责处理队列任务,主要逻辑如下:
- 先处理前置任务队列
- 根据
Id
排队队列 - 遍历执行队列任务
- 执行完毕后清空并重置队列
- 执行后置队列任务
- 如果还有就递归继续执行
function flushJobs(seen?: CountMap) {
isFlushPending = false // 是否正在等待执行
isFlushing = true // 正在执行
if (__DEV__) seen = seen || new Map() // 开发环境下
flushPreFlushCbs(seen) // 执行前置任务队列
// 根据 id 排序队列,以确保
// 1. 从父到子,因为父级总是在子级前面先创建
// 2. 如果父组件更新期间卸载了组件,就可以跳过
queue.sort((a, b) => getId(a) - getId(b))
try {
// 遍历主任务队列,批量执行更新任务
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job && job.active !== false) {
if (__DEV__ && checkRecursiveUpdates(seen!, job)) {
continue
}
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
}
} finally {
flushIndex = 0 // 队列任务执行完,重置队列索引
queue.length = 0 // 清空队列
flushPostFlushCbs(seen) // 执行后置队列任务
isFlushing = false // 重置队列执行状态
currentFlushPromise = null // 重置当前微任务为 Null
// 如果主任务队列、前置和后置任务队列还有没被清空,就继续递归执行
if ( queue.length || pendingPreFlushCbs.length || pendingPostFlushCbs.length ) {
flushJobs(seen)
}
}
}
flushPreFlushCbs()
该方法负责执行前置任务队列,说明都写在注释里了
export function flushPreFlushCbs( seen?: CountMap, parentJob: SchedulerJob | null = null) {
// 如果待处理的队列不为空
if (pendingPreFlushCbs.length) {
currentPreFlushParentJob = parentJob
// 保存队列中去重后的任务为当前活动的队列
activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
// 清空队列
pendingPreFlushCbs.length = 0
// 开发环境下
if (__DEV__) { seen = seen || new Map() }
// 遍历执行队列里的任务
for ( preFlushIndex = 0; preFlushIndex < activePreFlushCbs.length; preFlushIndex+ ) {
// 开发环境下
if ( __DEV__ && checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])) {
continue
}
activePreFlushCbs[preFlushIndex]()
}
// 清空当前活动的任务队列
activePreFlushCbs = null
preFlushIndex = 0
currentPreFlushParentJob = null
// 递归执行,直到清空前置任务队列,再往下执行异步更新队列任务
flushPreFlushCbs(seen, parentJob)
}
}
flushPostFlushCbs()
该方法负责执行后置任务队列,说明都写在注释里了
let activePostFlushCbs: SchedulerJob[] | null = null
export function flushPostFlushCbs(seen?: CountMap) {
// 如果待处理的队列不为空
if (pendingPostFlushCbs.length) {
// 保存队列中去重后的任务
const deduped = [...new Set(pendingPostFlushCbs)]
// 清空队列
pendingPostFlushCbs.length = 0
// 如果当前已经有活动的队列,就添加到执行队列的末尾,并返回
if (activePostFlushCbs) {
activePostFlushCbs.push(...deduped)
return
}
// 赋值为当前活动队列
activePostFlushCbs = deduped
// 开发环境下
if (__DEV__) seen = seen || new Map()
// 排队队列
activePostFlushCbs.sort((a, b) => getId(a) - getId(b))
// 遍历执行队列里的任务
for ( postFlushIndex = 0; postFlushIndex < activePostFlushCbs.length; postFlushIndex++ ) {
if ( __DEV__ && checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])) {
continue
}
activePostFlushCbs[postFlushIndex]()
}
// 清空当前活动的任务队列
activePostFlushCbs = null
postFlushIndex = 0
}
}