先看看vue官网nextTick方法的定义:
nextTick
是等待下一次 DOM 更新刷新的工具方法。当你在 Vue 中更改响应式状态时,最终的
DOM 更新并不是同步生效的
,而是由 Vue 将它们缓存在一个队列
中,直到下一个"tick"才一起执行。这样是为了确保每个组件无论发生多少状态改变,都仅执行一次更新。
nextTick()
可以在状态改变后立即使用,以等待 DOM 更新完成。你可以传递一个回调函数作为参数,或者 await 返回的 Promise。
根据官网的定义,修改一个响应式数据后,页面dom的更新渲染是异步的。
用nextTick
方法可以确保页面dom更新后执行回调。
问题来了:
vue的响应式数据的修改会触发副作用函数,副作用函数会执行渲染函数。渲染函数使用diff算法更新dom。
这个副作用函数的执行也是先推送到一个异步队列(Promise队列)
但是nextTick本质上也是:Promise.resolve.then(callback)
既然都是promise队列,那么先进入队列的先执行才对。
举个例子:
js
let next = Promise.resolve()
let ref = Promise.resolve()
next.then(()=>{
console.log('先执行')
})
ref.then(()=>{
console.log('后执行')
})
但是实际使用vue:
js
<template>
<div>{{ msg }}</div>
</template>
<script setup lang="ts">
const msg = ref(1)
nextTick(() => {
debugger
console.info("执行")
}) //回调函数推到promise队列(先进入队列应该先执行)
msg.value=4 //渲染函数推到promise队列(后进入队列应该后执行)
</script>
实际结果是 页面渲染了4。才执行nextTick的回调函数。
很明显vue做了处理。让nextTick的执行在渲染函数的后面。但是大家都是promise凭啥后执行的能在前面执行?
深入源码看看具体实现:
调度器源码路径:packages/runtime-core/src/scheduler.ts
github1s链接:https://github1s.com/vuejs/core/blob/main/packages/runtime-core/src/scheduler.ts
找到nextTick源码:
js
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
let currentFlushPromise: Promise<void> | null = null
const RECURSION_LIMIT = 100
type CountMap = Map<SchedulerJob, number>
export function nextTick<T = void, R = void>(
this: T,
fn?: (this: T) => R,
): Promise<Awaited<R>> {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p
}
找到queueJob、queueFlush、flushJobs源码 :
flushJobs
函数代表从缓存队列里面执行响应式数据对应的副作用函数(渲染dom)。
js
export function queueJob(job: SchedulerJob) {
if (
!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex,
)
) {
if (job.id == null) {
queue.push(job)
} else {
queue.splice(findInsertionIndex(job.id), 0, job)
}
queueFlush()
}
}
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
找到页面初始化时候的代码:
github1s链接:https://github1s.com/vuejs/core/blob/main/packages/runtime-core/src/renderer.ts
看一下渲染一个组件的测试用例:
渲染一个组件的执行链路是:
调用createRenderer -> baseCreateRenderer
会返回一个渲染函数 render(渲染函数,传入虚拟节点对象和要挂载的节点)
上图可以看到。vue组件初始化渲染
的时候就会把update(副作用函数)传递到queueJob
方法内、并且放到queue内。然后执行queueFlush
方法。
真正的代码执行流程:
js
const msg = ref(1)
// 这里会把页面的渲染函数添加到queueJob队列。并且去重。
js
nextTick(() => {
console.info("执行")
})
执行nextTick之前currentFlushPromise
已经变成了resolvedPromise.then(flushJobs)
那么nextTick的执行自然只能等flushJobs执行完后执行
。
js
msg.value=4
// 这里会把页面的渲染函数添加到queueJob队列。并且去重。
总结
本质上nextTick
的调用和vue组件执行渲染函数的队列
都通过同一个Promose进行调度。
所以才可以实现nextTick延迟执行
的效果。
形如:
js
let p = Promise.resolve() //类似上面的currentFlushPromise
let reactive = ()=>{console.log('响应式更新')}
let nextTick = ()=>{console.log('nextTick回调')}
p=p.then(reactive)
p=p.then(nextTick)
// 控制台输出:nextTick回调响应式更新
// 控制台输出:nextTick回调