深入nextTick
# 前言
vue的
异步更新策略
是性能优化中一个重要的一环,也是vue自身很巧妙的一个设计。当我们在修改data
中的属性的时候,vue并不是立即
去更新视图
的,而是采用异步更新策略
,所以我们在修改data之后是不能立即
获取到最新
的dom的。vue这么做是为了防止频繁变更数据导致频繁更新视图从而出现的性能问题,所以即使在同一个事件循环中更新了一个data一千次,vue也只会执行一次渲染视图。有些时候可能需要在data更新之后就立马获取dom,这个时候就可以使用vue官方提供的$nextTick
方法,下面就一起来深入了解一下这个api吧
在阅读此文之间请先了解js事件循环
nextTick
的源码并不长,主要就是利用js的事件循环
来做的一个缓冲的效果
/* @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
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// 在2.5版本中组合使用microtasks 和macrotasks,但是重绘的时候还是存在一些小问题,而且使用macrotasks在任务队列中会有几个特别奇怪的行为没办法避免,So又回到了之前的状态,在任何地方优先使用microtasks 。
// 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 */
// task的执行优先级
// Promise -> MutationObserver -> setImmediate -> setTimeout
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.
// Techinically 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)
}
}
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
})
}
}
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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
115
116
# 分析
其实nextTick
内部的逻辑非常简单,除去一些宏任务微任务的降级
之外几乎就一二十行代码。他内部主要分为以下几个成员
callback
:callback
是一个保存任务
的队列,我们在调用nextTick
时传入的回调函数就会被推入
这个队列
flushCallbacks
:调用所有callback
的函数,我们存入的callback
最终会有这个函数遍历逐一执行timerFunc
:经过nextTick
内部包装后的一个函数,他本质上是一个微任务
或者宏任务
(根据浏览器兼容性
来降级
),由他来触发flushCallbacks
pending
:状态机
,控制是否执行当前的callback
当我们调用一个nextTick
的时候大概会发生这些事情:
- 首先
timerFunc
会被初始化成一个最适合当前浏览器的任务
(nextTick内部总共采用了四种任务
,分别是微任务Promise.then
,MutationObserver
宏任务setImmediate
,setTimeout
。) - 进入
nextTick
,首先将传入的cb
添加进callback
数组 - 判断当前状态
pending
是否为false
,如果是则去修改状态为true
并且调用timerFunc
,也就是开始执行
我们传入
的所有回调
- 由于浏览器的
js引擎线程
和GUI渲染进程
互斥的关系,当GUI渲染线程
在更新的时候是不会
去执行js代码
的,所以如果我们的回调总是会被控制在dom渲染结束
之后去执行。这也就是nextTick的绝妙之处。 flushCallbacks
内部会将pending
重置为false
,使得我们的下一次循环得以继续
执行,并且他会拷贝
一份callbacks
,然后清空
他。这样可以保证下一次循环期间内保存的事件不会
被上一次循环调用,也就是每一次事件循环都只调用本次循环内所产生的callback
。
编辑 (opens new window)
上次更新: 2023/04/07, 16:21:51