想和你看看星空,只谈夜色与微风

image

this.$nextTick(() => ‘blablabla’)。。。

前言

在之前的项目中,因为经常会用的一些可视化库。所以就不必然的会在数据接收之后操作dom。当初年少无知的我经常因为dom还未“更新”去踩坑甚至到怀疑人生。。。。image虽然有些“云程序猿”给了个$(function(){})的解决方法。本着不装逼非程序猿的思维感觉还是有点小low。俗话说世间万物皆有因果,只能怪我当初太年轻没好好看文档。。。

nextTick作用是什么

首先我们知道vue的dom更新是异步的。而nextTick则是利用事件循环在dom更新(异步)完毕后再执行。这样其实就解决了上面的提出的获取不到数据变化后dom的问题。

关于异步更新队列

官网关于异步更新队列的说明:可能你还没有注意到,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 更新完成后就会调用

事件循环?也就是事件队列也就联系到了宏任务微任务也就可以联系到了setTimout等等

过滤watcher?不难想到上一篇关于双向绑定的watcher里一笔带过的watcher过滤(其实就是根据watcher的id的判断,虽然代码里也没写)。。。

带着这两个小问题我们看看vue nextTick源码的实现。

还是要看watcher

背景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default {
data () {
return {
msg: 0
}
},
mounted () {
this.msg = 1
this.msg = 2
this.msg = 3
},
watch: {
msg () {
console.log(this.msg) //只打印一次3
}
}
}

首先我们知道当数据变化会触发监听器的set方法 => 触发订阅器dep的notfiy方法 => 触发订阅者watcher的update方法 => 触发this.run执行相应回调 => 然后依次打印1,2,3。为什么现在却只打印一个3?

qunueWatcher的update

实际上watcher的update方法:

1
2
3
4
5
6
7
8
9
10
11
this.deep = this.user = this.lazy = this.sync = false
...
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run() //同步。。。
} else {
queueWatcher(this) //异步推送到观察者队列中,即异步更新调用这个方法。
}
}

我们发现其实update做的只是异步推送到观察者队列(缓存了msg的改变,异步调用时直接取到最后的值)。我们再看queueWatcher这个方法:

queueWatcher

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) { //这一块其实就是过滤id。当观察者队列已经有该观察者的id则跳过。
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作用保证回调函数flushSchedulerQueue只允许放入callbacks一次,并且再下一个tick时,Watch对象才会被遍历取出,更新视图。
waiting = true
nextTick(flushSchedulerQueue) //这才是执行nexttick的语句
}
}
}

从上面可以看出waiting其实就是控制在下一个tick时调用异步刷新视图的标识,而控制什么时候是下一个tick就是nextTick方法,刷新视图则是flushSchedulerQueue方法。所以要看看flushSchedulerQueue和nextTick这两个方法

nextTick(flushSchedulerQueue)

flushSchedulerQueue

flushSchedulerQueue方法 其实就是实际执行的watch的run来更新视图

1
2
3
4
5
6
7
8
9
10
11
12
function flushSchedulerQueue () {
flushing = true
let watcher, id
...
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
id = watcher.id
has[id] = null
watcher.run() //执行的watch视图更新
...
}
}

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
export const nextTick = (function () {
const callbacks = [] //存储回调函数
let pending = false // 一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送
let timerFunc //何时调用回调函数的标识位

function nextTickHandler () { //调用回调函数的方法
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]() ///触发callback的回调函数
}
}

// An asynchronous deferring mechanism.
// In pre 2.4, we used to use microtasks (Promise/MutationObserver)
// but microtasks actually has too high a priority and fires in between
// supposedly sequential events (e.g. #4521, #6690) or even between
// bubbling of the same event (#6566). Technically setImmediate should be
// the ideal choice, but it's not available everywhere; and the only polyfill
// that consistently queues the callback after all DOM events triggered in the
// same loop is by using MessageChannel. 总结就是:改了多次最终nextTick采取的策略是默认走microTask(虽然在dom事件中还有问题)。
/* istanbul ignore if */
if (typeof Promise !== 'undefined' && isNative(Promise)) { //优先用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' && ( //其次是html5的MutationObserver
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)) { //其其次就是setImmediate
// Fallback to setImmediate.
// Techinically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else { //最后没办法用settimeout
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}

export function nextTick (cb?: Function, ctx?: Object) { //真正调用的nextTick函数
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 //如果在执行回调中那么就要在下一个tick时调用
timerFunc() //执行回调
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
})()

所以nextTick方法其实就是看timerfunc方法何时调用而timerfunc则要根据事件循环的定义顺序,在vue中定义的顺序是优先microtask然后macrotask。microtask最佳选择是Promise其次是MutationObserver(h5新增的特性在ios上有些不兼容),macrotask最佳方案是setImmediate(仅仅在ie和nodeJs环境支持)其次也就是setTimeout了(虽然有4ms延迟)。

microtask与macrotask执行顺序排序

当一个程序有:setTimeout(macro), setInterval(macro) ,setImmediate(macro), I/O(macro), UI渲染(macro),Promise(micr0) ,process.nextTick(micr0), Object.observe(micr0), MutationObserver(micr0)的时候执行顺序是:

1
2
3
宏:I/O => UI渲染
微:process.nextTick => Promise => MutationObserver => Object.observe(废弃)
新的循环 宏:setImmediate => setTimeout,setInterval

总结:

  • nextTick作用:保证了更新视图操作DOM的动作是在当前栈执行完以后下一个tick的时候调用(拿到了更新后的dom),缓存了数据变化以及过滤了watcher队列从而大大优化了性能
  • nextTick原理:用异步队列的方式来控制DOM更新和nextTick回调先后执行。microtask因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕。因为兼容性问题,vue不得不做了microtask向macrotask的降级方案。