# nextTick实现原理
>面试题:Vue 的 nextTick 是如何实现的?
```vue
```
思考🤔:点击按钮后,页面会渲染几次?
答案:只会渲染一次,同步代码中多次对响应式数据做了修改,多次修改会被**合并**为一次,之后根据最终的修改结果**异步**的去更新 DOM.
思考🤔:倘若不合并,并且同步的去修改DOM,会有什么样的问题?
答案:如果不进行合并,并且数据一变就同步更新DOM,会导致频繁的重绘和重排,这非常耗费性能。
思考🤔:异步更新会带来问题
答案:无法及时获取到更新后的DOM值
原因:因为获取DOM数据是同步代码,DOM的更新是异步的,同步代码会先于异步代码执行。
解决方案:将获取DOM数据的同步任务包装成一个微任务,浏览器在完成一次渲染后,就会立即执行微任务。
当前我们自己的解决方案:
```js
const increment = () => {
count.value++
Promise.resolve().then(() => {
console.log('最新的数据:', count.value)
console.log('通过DOM拿textContent数据:', counterRef.value.textContent)
console.log('通过DOM拿textContent数据:', document.getElementById('counter').textContent)
console.log('通过DOM拿innerHTML数据:', counterRef.value.innerHTML)
console.log('通过DOM拿innerHTML数据:', document.getElementById('counter').innerHTML)
})
}
```
nextTick 帮我们做的就是上面的事情,将一个任务包装成一个微任务。
```js
const increment = () => {
count.value++
nextTick(() => {
console.log('最新的数据:', count.value)
console.log('通过DOM拿textContent数据:', counterRef.value.textContent)
console.log('通过DOM拿textContent数据:', document.getElementById('counter').textContent)
console.log('通过DOM拿innerHTML数据:', counterRef.value.innerHTML)
console.log('通过DOM拿innerHTML数据:', document.getElementById('counter').innerHTML)
})
}
```
nextTick 返回的是一个 Promise
```js
const increment = async () => {
count.value++
await nextTick()
console.log('最新的数据:', count.value)
console.log('通过DOM拿textContent数据:', counterRef.value.textContent)
console.log('通过DOM拿textContent数据:', document.getElementById('counter').textContent)
console.log('通过DOM拿innerHTML数据:', counterRef.value.innerHTML)
console.log('通过DOM拿innerHTML数据:', document.getElementById('counter').innerHTML)
}
```
$nextTick,首先这是一个方法,是 Vue 组件实例的方法,用于 OptionsAPI 风格的。
```js
export default {
data() {
return {
count: 1,
counterRef: null
}
},
methods: {
increment() {
this.count++
this.$nextTick(() => {
// 在下一个 DOM 更新循环后执行的回调函数
console.log('最新数据为:', this.count)
console.log('拿到的DOM:', document.getElementById('counter'))
console.log('拿到的DOM:', this.$refs.counterRef)
console.log('通过DOM拿数据:', document.getElementById('counter').textContent)
console.log('通过DOM拿数据:', document.getElementById('counter').innerHTML)
console.log('通过DOM拿数据:', this.$refs.counterRef.textContent)
console.log('通过DOM拿数据:', this.$refs.counterRef.innerHTML)
})
}
}
}
```
[nextTick源码](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/scheduler.ts)
```js
// 创建一个已经解析的 Promise 对象,这个 Promise 会立即被解决,
// 用于创建一个微任务(microtask)。
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise
// 一个全局变量,用于跟踪当前的刷新 Promise。
// 初始状态为 null,表示当前没有刷新任务。
let currentFlushPromise: Promise | null = null
// queueFlush 函数负责将刷新任务(flushJobs)放入微任务队列。
// 这是 Vue 的异步更新机制的核心部分,用于优化性能。
function queueFlush() {
// 检查是否已经在刷新(isFlushing)或者刷新任务是否已被挂起(isFlushPending)。
if (!isFlushing && !isFlushPending) {
// 设置 isFlushPending 为 true,表示刷新任务已被挂起,正在等待执行。
isFlushPending = true
// 将 currentFlushPromise 设置为 resolvedPromise.then(flushJobs)
// 这将创建一个微任务,当 resolvedPromise 被解决时,执行 flushJobs 函数。
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
// nextTick 函数用于在下一个 DOM 更新循环之后执行一个回调函数。
// 它返回一个 Promise,这个 Promise 会在 DOM 更新完成后解决。
export function nextTick(
this: T,
fn?: (this: T) => R, // 可选的回调函数,在 DOM 更新之后执行
): Promise> {
// 如果 currentFlushPromise 不为 null,使用它;否则使用 resolvedPromise。
// 这样可以确保在 DOM 更新之后再执行回调。
const p = currentFlushPromise || resolvedPromise
// 如果传入了回调函数 fn,返回一个新的 Promise,在 p 解决之后执行 fn。
// 使用 this 绑定来确保回调函数的上下文正确。
return fn ? p.then(this ? fn.bind(this) : fn) : p
// 如果没有传入回调函数 fn,直接返回 Promise p,这样外部代码可以使用 await 等待 DOM 更新完成。
}
```
>面试题:Vue 的 nextTick 是如何实现的?
>
>参考答案:
>
>nextTick 的本质将回调函数包装为一个微任务放入到微任务队列,这样浏览器在完成渲染任务后会优先执行微任务。
>
>nextTick 在 Vue2 和 Vue3 里的实现有一些不同:
>
>1. Vue2 为了兼容旧浏览器,会根据不同的环境选择不同包装策略:
>
> - 优先使用 Promise,因为它是现代浏览器中最有效的微任务实现。
>
> - 如果不支持 Promise,则使用 MutationObserver,这是另一种微任务机制。
>
> - 在 IE 环境下,使用 setImmediate,这是一种表现接近微任务的宏任务。
>
> - 最后是 setTimeout(fn, 0) 作为兜底方案,这是一个宏任务,但会在下一个事件循环中尽快执行。
>
>
>2. Vue3 则是只考虑现代浏览器环境,直接使用 Promise 来实现微任务的包装,这样做的好处在于代码更加简洁,性能更高,因为不需要处理多种环境的兼容性问题。
>
>整体来讲,Vue3 的 nextTick 实现更加简洁和高效,是基于现代浏览器环境的优化版本,而 Vue2 则为了兼容性考虑,实现层面存在更多的兼容性代码。