这里是咕了一个月终于开了新坑的雪之樱,上午在b站看了个电影《为美好的世界献上祝福!红传说 》,决定水篇博客(所以有什么关系吗)。好吧,言归正传,这次要学习的是$nextTick的源码,相信小伙伴们对$nextTick一定不会陌生,啥,忘了,好吧,笔者在这里贴心地附上vue文档对它的说明。

看~是不是很简单,它用于在组件完成下次更新时执行一个回调,你可以在这里获取更新过的DOM状态,然后做一些操作。然后我们就从源码角度看看他做了什么吧。

$nextTick的混入在vue/src/core/instance/render下的renderMixin方法中

1
2
3
4
5
6
7
8
import {nextTick} from '../util/index'
export function renderMixin(Vue: Class) {
// ...
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
};
// ...
}

可以看到,$nextTick是调用了nextTick函数,然后把cb和this作为参数传给了nextTick,那我们就来看看nextTick的实现

nextTick的代码并不多,去掉一些平台相关的代码和注释后,核心逻辑不到一百行的样子,我们先把它贴出来,然后再慢慢分析

1
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
/* @flow */
/* globals MessageChannel */

import {handleError} from './error'
import {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]()
}
}


// 微任务的实现
let microTimerFunc;
// 宏任务的实现
let macroTimerFunc;
// 是否主动降级使用宏任务
let useMacroTask = false;


// 具体实现根据浏览器的兼容实现宏任务
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
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)
}
}

// 具体实现根据浏览器的兼容实现微任务
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve();
microTimerFunc = () => {
p.then(flushCallbacks);
}
} else {
// fallback to macro
microTimerFunc = macroTimerFunc
}

export function withMacroTask(fn: Function): Function {
return fn._withTask || (fn._withTask = function () {
useMacroTask = true;
const res = fn.apply(null, arguments);
useMacroTask = false;
return res
})
}

// cb是传入的回调, ctx是this
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve;
// 收集cb
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
})
}
}

我们可以看到,nextTick中定义了一个变量_resolve,这个变量只有当你没有传入cb,而是打算使用$nextTick().then()这种方式设置回调时才会用到,我们先不分析这个,先按照传入cb的方式看看nextTick做了什么。

在下一行里,nextTick往callbacks里插入了一个匿名函数,这个匿名函数会判断cb是否存在,如果存在就执行,不存在就调用_resolve(ctx),很明显,nextTick并没有马上执行这个回调,而是把它缓存到了一个队列中,那么这个队列什么时候执行呢,我们接着往下看。

然后,nextTick会判断pending的值,pending是一个全局变量,标记着当前的callbacks队列正等待被执行,如果判断pending的值是false,就是不处于等待被执行的状态,就会根据useMacroTask这个标记,执行macroTimerFunc,或者是microTimerFunc,先剧透一下,这两个函数都是用于把callbacks里的回调拿出来执行的,最后,判断如果没有传入cb,那么我们就创建一个new Promise并返回,同时把resolve赋值给_resolve,之前我们已经看到,_resolve会在被推入了callbacks的匿名函数中执行,也就是说,和直接传入cb一样,_resolve也会在callbacks里的函数被取出执行时执行。

然后我们看看 macroTimerFuncmicroTimerFunc 的实现,这两个函数的作用都是把清空callbacks的任务(flushCallbacks函数)推到执行队列中等待被执行,只不过是推入到宏任务队列还是微任务队列的区别而已,我们先看看看 microTimerFunc

1
2
3
4
5
6
7
8
9
10
11
// 具体实现根据浏览器的兼容实现微任务
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve();
microTimerFunc = () => {
p.then(flushCallbacks);
}
} else {
// fallback to macro
// 降级使用宏任务
microTimerFunc = macroTimerFunc
}

这段代码首先会判断浏览器有没有实现Promise,如果有,就在then里面执行flushCallbacks,也就是说, flushCallbacks被推入到了微任务的执行队列中,等待当前执行栈的任务结束,就可以被执行。如果没有实现Promise(鞭尸IE),就取macroTimerFunc的值

然后我们看看macroTimerFunc的实现,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 具体实现根据浏览器的兼容实现宏任务
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
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)
}
}

可以看到,这里前后依次会尝试使用setImmediateMessageChannelsetTimeout来实现 macroTimerFunc ,他们要做的事情都是把flushCallbacks推入到宏任务队列中。

之前已经说过, 真正把cb从callbacks中取出执行的逻辑在flushCallbacks中,那我们来看看它的实现

1
2
3
4
5
6
7
8
function flushCallbacks() {
pending = false;
const copies = callbacks.slice(0);
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}

flushCallbacks函数的实现非常简单,首先把pending置为false,标记着已经清空执行过callbacks队列,接着callbacks复制一份拷贝给copies,然后清空callbacks,依次执行回调。

好啦,这就是nextTick的全部源码解析啦,但我们似乎忘了点什么,没错,还记得为什么$nextTick的作用吗,没错,那就是在dom更新后执行一个回调,那这个又是怎么实现的呢,其实很简单,在修改了数据后,会把修改dom的回调通过nextTick添加到callbacks中,如果在修改数据后在通过$nextTick添加回调,而这次添加的回调会被添加到修改dom的回调后,所以在执行时,也是先执行更新dom的操作,再执行用户通过$nextTick传入的回调。这样就可以在cb里拿到最新的dom,我们来看看源码

1
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
export default class Watcher {

/**
* Subscriber interface.
* Will be called when a dependency changes.
* update函数在数据更新时触发
*/
update() {
/* istanbul ignore else */
// 计算属性的更新
if (this.computed) {
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(() => {
this.dep.notify()
})
}
}
// 同步更新
else if (this.sync) {
this.run()
} else {
// 正常的派发更新
queueWatcher(this)
}
}
}

在数据更新后,watcherupdate方法会被触发,然后会触发queueWatcher方法

1
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
export function queueWatcher(watcher: Watcher) {
const id = watcher.id;
// 判断watcher在不在queue里面
// 即使一个watcher在一个tick内多次触发update,也不会造成多次更新
if (has[id] == null) {
has[id] = true;
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
// 如果正在刷新
let i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true;
// 在下一个tick去执行
nextTick(flushSchedulerQueue)
}
}
}

看,我们的想法得到了验证,更新dom也是通过nextTick添加的,在这之后通过$nextTick添加的回调自然就在更新dom之后啦。

好了,这就是这篇文章全部的内容了,希望大佬们能给个赞鼓励一下我未来的高产orz,这里是划水咸鱼雪之樱,我们下次见。