跳到主要内容

认识

2024年03月18日
柏拉文
越努力,越幸运

一、认识


nextTick 在下次 DOM 更新循环结束之后执行延迟回调。一般用于修改数据之后, 立即使用 nextTick 获取更新后的 DOMVue 有个机制,更新 DOM 是异步执行的,当数据变化会产生一个异步更行队列,要等异步队列结束后才会统一进行更新视图,所以改了数据之后立即去拿 DOM 还没有更新就会拿不到最新数据。所以提供了一个 nextTick 函数,它的回调函数会在DOM 更新后立即执行。nextTicknextTick 注册的回调函数放入 callbacks 等待执行, 将执行函数放到微任务或者宏任务中, 事件循环到了微任务或者宏任务(根据当前环境支持什么方法则调用哪个, 优先级为: Promise.thenMutationObserversetImmediatesetTimeout), 执行函数一次执行 callbacks 中的回调。

$nextTick 工作流:

  1. 调用 dep.notify() 触发依赖通知更新,将负责更新的 watcher 放入 watcher 队列, 这个队列为 flushSchedulerQueue

  2. try catch 包装 flushSchedulerQueue 更新函数或者用户传入的回调函数,然后将其放入 callbacks 数组。存放 flushSchedulerQueuecallbacks 数组我们叫做**更新DOM**的数组

  3. 这时候, 我们调用 Vue.nextTick(cb), 将用户cb加入 callbacks , 这个 callbacks 数组我们叫做用户回调数组

  4. 如果 pendingfalse,表示现在浏览器的任务队列中没有 flushCallbacks 函数, 将 pending 置为 true, 执行 timeout 函数, 浏览器的任务队列中加入 flushCallbacks 函数, 执行 callbacks 数组中的每一个函数, 进而执行 watcher.run() 更新 DOM, 随后清空 callbacks 数组

  5. 待执行 flushCallbacks 完函数时,pending 会被再次置为 false,表示下一个 flushCallbacks 函数可以进入浏览器的任务队列了, 开始执行用户callbacks 数组, 这样可以保证先完成的 DOM 更新,再执行 cb 函数。

二、细节


2.1 pending

pending: 保证在同一时刻,浏览器的任务队列中只有一个 flushCallbacks 函数

2.2 添加回调

把传入的回调函数 cb 压入 callbacks 数组

2.3 执行 timeFunc

执行timeFunc函数,通过微任务或者宏任务将flushCallbacks加入到任务队列。其中, timeFunc 函数的优雅降级策略为:

  1. 微任务 Promise.then

  2. 微任务 MutationObserver: 创建一个 MutationObserver 对象, 并且把 flushCallbacks 作为 MutationObserver 构造函数的回调函数。之后我们创建一个文本节点, 通过 mutationObserver.observe() 来监听该文本节点, 如果文本节点的内容有任何变动的话, 它就会触发 flushCallbacks 回调函数。因此, 我们只需要 timerFunc 函数中, 修改文本内容, 文本节点内容发生变化之后, 进而触发 MutationObserver 的回调。

  3. 宏任务 setImmediate

  4. 宏任务 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 执行完毕。