# vue 原理解析之 nextTick 探索
nextTick 是 Vue 的一个核心实现,在了解 nextTick 之前,先 js 运行机制
# JS 运行机制
js 执行是单线程,它是基于事件循环的,事件循环它分为以下步骤:
1.所有同步任务都会在主线程上执行,形成一个执行栈(execution context stack)。
2.在主线程之外;还存在一个 “任务队列”(task queue)。只要异步任务有了运行结果,就在 “任务队列中”放置一个事件
3.一旦同步任务执行完毕,系统就会读取“任务队列”;看看里面有哪些异步事件;那些对应的异步任务;于是结束等待;进入执行栈,开始执行
4.主线程会不段的重复第三步
主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 结束后,都要清空所有的 micro task。
在浏览器环境中,常见的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate;常见的 micro task 有 MutationObsever 和 Promise.then。
在 Nodejs 事件循环机制中,有任务两个队列:Macrotask 队列和 Microtask 队列。在一个事件循环里,这两个队列会分两步执行,第一步会固定地执行一个(且仅一个)Macrotask 任务,第二步会执行整个 Microtask 队列中的所有任务。并且,在执行 Microtask 队列任务的时候,也允许加入新的 Microtask 任务,直到所有 Microtask 任务全部执行完毕,才会结束循环。 Macrotasks 一般包括: setTimeout, setInterval, setImmediate, I/O, UI rendering; Microtasks 一般包括: process.nextTick, Promises, Object.observe, MutationObserver。
上代码
setTimeout(function() {
console.log(5);
}, 0);
setImmediate(function() {
console.log(6);
});
new Promise(function(resolve) {
console.log(1);
resolve();
console.log(2);
}).then(function() {
console.log(4);
});
console.log('打酱油');
2
3
4
5
6
7
8
9
10
11
12
13
14
var i = 0;
//microtask
new Promise((function (resolve, reject) {
resolve("ok")
})).then(function () {
i++;
console.log("2⃣️", i);
})
//UI
$("#test").html(i);
//macrotask
setTimeout(function () {
i++;
console.log("3⃣️", i);
}, 0)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Vue 的实现
在 Vue 源码 2.5+ 后,nextTick 的实现单独有一个 JS 文件来维护它,它的源码并不多,总共也就 100 多行。接下来我们来看一下它的实现,在 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
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
next-tick.js 申明了 microTimerFunc 和 macroTimerFunc 2 个变量,它们分别对应的是 micro task 的函数和 macro task 的函数。对于 macro task 的实现,优先检测是否支持原生 setImmediate,这是一个高版本 IE 和 Edge 才支持的特性,不支持的话再去检测是否支持原生的 MessageChannel,如果也不支持的话就会降级为 setTimeout 0;而对于 micro task 的实现,则检测浏览器是否原生支持 Promise,不支持的话直接指向 macro task 的实现。
next-tick.js 对外暴露了 2 个函数,先来看 nextTick,这就是我们在上一节执行 nextTick(flushSchedulerQueue) 所用到的函数。它的逻辑也很简单,把传入的回调函数 cb 压入 callbacks 数组,最后一次性地根据 useMacroTask 条件执行 macroTimerFunc 或者是 microTimerFunc,而它们都会在下一个 tick 执行 flushCallbacks,flushCallbacks 的逻辑非常简单,对 callbacks 遍历,然后执行相应的回调函数。
这里使用 callbacks 而不是直接在 nextTick 中执行回调函数的原因是保证在同一个 tick 内多次执行 nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。
nextTick 函数最后还有一段逻辑:
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
2
3
4
5
这是当 nextTick 不传 cb 参数的时候,提供一个 Promise 化的调用,比如:
nextTick().then(() => {})
2
当 _resolve 函数执行,就会跳到 then 的逻辑中。
next-tick.js 还对外暴露了 withMacroTask 函数,它是对函数做一层包装,确保函数执行过程中对数据任意的修改,触发变化执行 nextTick 的时候强制走 macroTimerFunc。比如对于一些 DOM 交互事件,如 v-on 绑定的事件回调函数的处理,会强制走 macro task。
# 总结
为了让 flush 动作能在当前 Task 结束后尽可能早的开始,Vue 会优先尝试将任务 micro-task 队列,具体来说,在浏览器环境中 Vue 会优先尝试使用 MutationObserver API 或 Promise,如果两者都不可用,则 fallback 到 setTimeout。