认识
一、认识
nextTick
在下次 DOM
更新循环结束之后执行延迟回调。一般用于修改数据之后, 立即使用 nextTick
获取更新后的 DOM
。Vue
有个机制,更新 DOM
是异步执行的,当数据变化会产生一个异步更行队列,要等异步队列结束后才会统一进行更新视图,所以改了数据之后立即去拿 DOM
还没有更新就会拿不到最新数据。所以提供了一个 nextTick
函数,它的回调函数会在DOM
更新后立即执行。nextTick
将 nextTick
注册的回调函数放入 callbacks
等待执行, 将执行函数放到微任务或者宏任务中, 事件循环到了微任务或者宏任务(根据当前环境支持什么方法则调用哪个, 优先级为: Promise.then
、MutationObserver
、setImmediate
、setTimeout
), 执行函数一次执行 callbacks
中的回调。
$nextTick
工作流:
-
调用
dep.notify()
触发依赖通知更新,将负责更新的watcher
放入watcher
队列, 这个队列为flushSchedulerQueue
-
用
try catch
包装flushSchedulerQueue
更新函数或者用户传入的回调函数,然后将其放入callbacks
数组。存放flushSchedulerQueue
的callbacks
数组我们叫做**更新DOM
**的数组 -
这时候, 我们调用
Vue.nextTick(cb)
, 将用户cb
加入callbacks
, 这个callbacks
数组我们叫做用户回调数组 -
如果
pending
为false
,表示现在浏览器的任务队列中没有flushCallbacks
函数, 将pending
置为true
, 执行timeout
函数, 浏览器的任务队列中加入flushCallbacks
函数, 执行callbacks
数组中的每一个函数, 进而执行watcher.run()
更新DOM
, 随后清空callbacks
数组 -
待执行
flushCallbacks
完函数时,pending
会被再次置为false
,表示下一个flushCallbacks
函数可以进入浏览器的任务队列了, 开始执行用户callbacks
数组, 这样可以保证先完成的DOM
更新,再执行cb
函数。
二、细节
2.1 pending
pending
: 保证在同一时刻,浏览器的任务队列中只有一个 flushCallbacks
函数
2.2 添加回调
把传入的回调函数 cb
压入 callbacks
数组
2.3 执行 timeFunc
执行timeFunc
函数,通过微任务或者宏任务将flushCallbacks
加入到任务队列。其中, timeFunc
函数的优雅降级策略为:
-
微任务
Promise.then
-
微任务
MutationObserver
: 创建一个MutationObserver
对象, 并且把flushCallbacks
作为MutationObserver
构造函数的回调函数。之后我们创建一个文本节点, 通过mutationObserver.observe()
来监听该文本节点, 如果文本节点的内容有任何变动的话, 它就会触发flushCallbacks
回调函数。因此, 我们只需要timerFunc
函数中, 修改文本内容, 文本节点内容发生变化之后, 进而触发MutationObserver
的回调。 -
宏任务
setImmediate
-
宏任务
setTimeout
2.4 执行 flushCallbacks
当任务触发时, 执行flushCallbacks
, 遍历callbacks
, 执行相应的回调函数, 并清空列表
三、思考
3.1 为什么 Vue.js 使用异步更新队列
我们更新状态, Vue.js
的变化侦测的通知发送到该组件,组件内部用到的所有状态的变化都会通知到同一个 watcher
, 然后虚拟 DOM
会对整个组件进行比对 Diff
并更改 DOM
。 也就是说, 如果在同一轮事件循环中有两个数据发生了变化, 那么组件的 watcher
会收到两份通知, 从而进行两次渲染。事实上, 并不需要两次渲染, 只需要等待所有状态都修改完成后,一次性的将整个组件的 DOM
渲染到最新的即可。
为了解决重复渲染的问题, Vue.js
的实现方式是将收到通知的 watcher
实例添加到队列中缓存起来, 并且在添加到队列之前检查其中是否已经存在相同的 watcher
, 只有不存在时, 才将 watcher
实例添加到队列中。然后在下一次事件循环 Event Loop
中, Vue.js
会让队列中 watcher
触发渲染流程并清空队列。这样就可以保证即便在同一事件循环中有两个状态发生改变, watcher
最后也只执行一次渲染流程。
3.2 为什么 Vue.js 的异步队列优先使用微任务?
在 Vue.js 2.6
版本之后, nextTick
优先将回调函数添加到微任务异步队列。那么, 添加到微任务异步队列的缺点是: 在事件循环中, 必须当微任务队列中的事件都执行完之后, 才会从宏任务队列中取出一个事件执行下一轮。所以添加到微任务队列中的人物执行时机优先于向宏任务队列中添加的任务。因此, 微任务的优先级非常高,在某些场景下会有一些问题。比如: 在某些情况下会发生连续事件; 甚至在同一事件冒泡之间。
但是之前的 Vue.js
版本, 使用宏任务与微任务结合, 带来的问题更多一些。比如: 在事件处理程序中使用(宏)任务会导致一些无法避免的奇怪行为
所以, Vue.js
优先选择微任务只是介于两者之间的权衡。
3.3 为什么要将回调 callback 将入到 callbacks 中? 直接在 nextTick 中执行 callback 可以吗?
答: 使用 callbacks
而不是直接在 nextTick
中执行回调函数的原因是保证在同一个 tick
内多次执行 nextTick
,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 tick
执行完毕。