Vue源码之nextTick

前言

今天我们开始讲一下 VuenextTick 方法的实现,无论是源码还是开发的过程中,经常需要使用到 nextTickVue 在更新 DOM 时是异步执行的,只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环 “tick” 中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserversetImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

源码

https://github.com/wclimb/vue/blob/dev/src/core/util/next-tick.js

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
export let isUsingMicroTask = false

// 储存回调
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 timerFunc
// 判断是否支持Promise,使用Promise的异步,属于微任务
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
// 判断是否支持MutationObserver,属于微任务
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// 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)
}
isUsingMicroTask = true
// 是否支持setImmediate,属于宏任务
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 都不支持就直接使用宏任务 setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// nextTick方法
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,执行的代码其实就是callback this.$nextTick().then(callback)
_resolve(ctx)
}
})
// 如果状态没有正在处理执行
if (!pending) {
// 置为处理中
pending = true
// 执行刚刚一系列判断下来获得的函数,最终会执行flushCallbacks方法
timerFunc()
}
// 如果没有传递回调函数就会返回一个Promise,this.$nextTick().then(callback),执行 _resolve(ctx) 之后会执行callback
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}

我们可以看到 Vue 会借助 timerFunc 方法异步批量处理回调函数,timerFunc 可能是 Promise.thenMutationObserversetImmediatesetTimeout。判断他们的支持程度,降级处理。
如果没有传递回调函数会返回一个Promise,并把 resolve 方法赋值给 _resolve,这样我们 this.$nextTick().then(callback)callback就会被触发了。至于为什么要使用 timerFunc 这种方式,开头已经讲了,是因为 Vue 是异步更新队列,这样做的好处是去除重复数据对于避免不必要的计算和 DOM 操作,比如我们操作一个数据,并且重复多次给他赋不一样的值,this.a = 1;this.a = 2; this.a = 3,答案结果最后自然是3,但是 Vue 不会去更新三次 DOM 或者数据,这样会造成不必要的浪费,所以需要做异步处理去 update 他们,既然更新是异步的,我们如果想直接马上获取最新的数据自然是不行的,需要借助 nextTick,在下一次事件循环中去获取,可以看下面👇的代码

实际代码

我们看下面的代码

1
<div id="app" @click="fn">{{code}}</div>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var vm = new Vue({
el: '#app',
data: {
code: 0
},
methods:{
fn(){
this.code = 1
console.log(this.$el.textConent); // 0
this.$nextTick(()=>{
console.log(this.$el.textConent) // 1
})
}
}
})

你也可以这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var vm = new Vue({
el: '#app',
data: {
code: 0
},
methods:{
async fn(){
this.code = 1
console.log(this.$el.textConent); // 0
- this.$nextTick(()=>{
- console.log(this.$el.textConent) // 1
- })
+ await this.$nextTick()
+ console.log(this.$el.textConent) // 1
}
}
})

总结

今天带大家了解了一下 nextTick 的内部实现,虽然你也可以直接使用 setTimeout 去做,但是基于性能和执行顺序的问题(微任务执行快于宏任务),推荐还是使用 nextTick 更好一点。

本文地址 Vue源码之nextTick

坚持原创技术分享,您的支持将鼓励我继续创作!