2024-09-11 10:55:44 +08:00

185 lines
6.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# nextTick实现原理
>面试题Vue 的 nextTick 是如何实现的?
```vue
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">增加计数</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => {
for (let i = 1; i <= 1000; i++) {
count.value = i
}
}
</script>
```
思考🤔:点击按钮后,页面会渲染几次?
答案:只会渲染一次,同步代码中多次对响应式数据做了修改,多次修改会被**合并**为一次,之后根据最终的修改结果**异步**的去更新 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<any>
// 一个全局变量,用于跟踪当前的刷新 Promise。
// 初始状态为 null表示当前没有刷新任务。
let currentFlushPromise: Promise<void> | 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<T = void, R = void>(
this: T,
fn?: (this: T) => R, // 可选的回调函数,在 DOM 更新之后执行
): Promise<Awaited<R>> {
// 如果 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 则为了兼容性考虑,实现层面存在更多的兼容性代码。