# $nextTick 原理
本篇文章讲解 Vue2 和 Vue3 中都使用到的一个非常核心的 API:nexTick 的实现原理。
# 1、nextTick
这是一个非常常用的 API,因为 Vue 存在异步更新的策略,所有 DOM 更新都延迟到当前同步任务执行完成之后再执行,所以,下面这种情况,我们并不能获取到更新之后的 DOM,因为此时 DOM 并没有更新。
data(){
a = 50
}
a = 100
console.log(app.innerHTML) 获取到 a 的值是50
此时就需要用到 nextTick 了,我们看 Vue 官方对 nextTick 的定义:
等待下一次 DOM 更新刷新的工具方法
作用是:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
很明显,nextTick 的最大作用就是用于在修改响应式数据之后,获取到更新之后的 DOM
# 2、nextTick 的实现原理
nextTick 接收一个回调作为参数。前面我们说了,Vue 是有异步更新的策略的,所有的 DOM 更新,都会在当前同步代码执行完成之后再执行。换句话说,Vue 将 DOM 更新封装成了一个异步任务,所以只有当前同步代码执行完成了之后,才会执行 DOM 更新。具体是封装为了宏任务还是微任务将会在下节讲解,因为实现异步更新也用到了 nextTick API。
既然 DOM 更新操作是一个异步任务,那么我们要获取到更新之后的 DOM,一定是在 DOM 更新这个异步任务之后才能获取到。所以,nextTick 的原理就是将传入的回调,包装成一个异步任务,并保证其在更新 DOM 的异步任务之后再执行。......很多博客都这样说,但事实真是如此吗?
先说结论:nextTick 并不开启一个异步任务,而是将传入的回调维护到调度队列中。
我们来看源码:
// nextTick 的任务排队
let callbacks = []
// 防抖
let waiting = false
// 清空调度队列
function flushCallbacks() {
let cds = callbacks.slice(0)
waiting = false
callbacks = []
cds.forEach(cb => cb())
}
// nextTick 优雅降级
let timerFunc;
if (Promise) {
timerFunc = () => {
Promise.resolve().then(flushCallbacks)
}
} else if (MutationObserver) {
let observer = new MutationObserver(flushCallbacks)
let textNode = document.createTextNode(1)
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
textNode.textContent = 2
}
} else if (setImmediate) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0);
}
}
export function nextTick(cb) {
callbacks.push(cb)
if (!waiting) {
timerFunc()
waiting = true
}
}
步骤:
- 首先,Vue 中为 nextTick 配置了一个调度队列,每次执行 nextTick 的时候,都会将传入的回调加入调度队列中。
- 当第一次执行 nextTick 的时候,由于 waiting 为 false,那么就会开启一个异步任务,这个异步任务的回调就是 flushCallbacks ,也就是刷新调度队列方法。开启异步任务之后,将 wating 设为 true,后面再次执行 nextTick 时,就不会再开启异步任务了。
- 由于被异步方法包装,flushCallbacks 会被延迟到当前同步代码执行完成之后才会执行,也就是必须等待所有的同步代码执行完成之后,我们才会刷新调度队列,按顺序执行 nextTick 调度队列中的每一个回调。
- 刷新队列时,先将队列拷贝一份,然后将队列置空,设置 wating 为 false,这样可以保证在执行队列中的回调时调用的 nextTick 可以加入调度队列,并开启新的异步任务区进行下一次队列刷新
# 3、优雅降级
前面说了,刷新调度队列方法 flushCallbacks 会在我们第一次调用 nextTick 时,被包装为一个异步方法的回调。那么我们使用什么异步方法去封装他呢?首先考虑的就是微任务,因为微任务会在当前同步代码执行完成之后立即执行,而宏任务必须等待所有的微任务都执行完成之后再执行。越快的相应就会带来越好的用户体验。基于这个原因,nextTick 采取了优雅降级的方式,也就是尽可能的尝试使用微任务去包装 flushCallbacks 方法,以求我们的回调可以在同步代码执行完成之后更快的被执行。
首先,Vue 会尝试使用 promise.then 方法来进行包装;如果当前环境不支持 promise,则尝试使用 MutationObserver 方法;如果还不支持,则当前代码很可能是在 IE 这个前端毒瘤上运行的,那么就会尝试一个 IE 独有的 API :setImmediate;最后如果还是不支持的话,就放弃使用微任务,转而使用宏任务的 settimeout 来包装,毕竟性能差总比没有好。
Vue3 不存在优雅降级,他直接使用 promise.then 方法来进行刷新队列方法的异步包装。因为 Vue3 本身就不兼容 IE,Vue3 的兼容性瓶颈在于 proxy 这个 API。Proxy 是无法使用其他低级 API 来模拟的,换句话说,即使使用 Bable,也无法对其进行降级。所以 Vue3 不兼容 IE,而 promise 和 Proxy 都是 ES6 中新增的 API,如果浏览器支持 Proxy,一般来说也会支持 promise。
# 4、补充 MutationObserver
MutationObserver 接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Events 规范的一部分。
构造函数 MutationObserver 会创建并返回一个新的观察器。构造函数接收一个回调函数,它会在触发指定 DOM 事件时,调用这个回调函数。MutationObserver 对 DOM 的观察不会立即启动;而必须先调用 observe 方法来确定,要监听哪一部分的 DOM 以及要响应哪些更改。
observe 方法:
用于指定要观察的 DOM,以及哪些 DOM 的变化会触发回调。 参数:
mutationObserver.observe(target[, options])
target 指定 DOM
options 为配置选项,选择监听 DOM 的哪些变化,options 的属性:
subtree 可选
当为
true时,将会监听以target为根节点的整个子树。包括子树中所有节点的属性,而不仅仅是针对target。默认值为false。childList 可选
当为
true时,监听target节点中发生的节点的新增与删除(同时,如果subtree为true,会针对整个子树生效)。默认值为false。attributes 可选
当为
true时观察所有监听的节点属性值的变化。默认值为true,当声明了attributeFilter或attributeOldValue,默认值则为false。attributeFilter 可选
一个用于声明哪些属性名会被监听的数组。如果不声明该属性,所有属性的变化都将触发通知。
attributeOldValue 可选
当为
true时,记录上一次被监听的节点的属性变化;默认值为false。characterData 可选
当为
true时,监听声明的target节点上所有字符的变化。默认值为true,如果声明了characterDataOldValue,默认值则为falsecharacterDataOldValue 可选
当为
true时,记录前一个被监听的节点中发生的文本变化。默认值为false
# 5、补充 setImmediate
没啥可说的,IE 专享(node也有)API,类似于 settimeout,但是它会将传入的回调加入微任务队列。
神奇的兼容性:

本文共 1927 字(含代码),截止本篇,本系列累计 16233 字
← 依赖收集和视图同步更新 异步更新 →