先看看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队列,那么先进入队列的先执行才对。
举个例子:
1let next = Promise.resolve()
2let ref = Promise.resolve()
3next.then(()=>{
4 console.log('先执行')
5})
6ref.then(()=>{
7 console.log('后执行')
8})
但是实际使用vue:
1<template>
2 <div>{{ msg }}</div>
3</template>
4<script setup lang="ts">
5const msg = ref(1)
6nextTick(() => {
7 debugger
8 console.info("执行")
9})
10msg.value=4
11</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源码:
1const resolvedPromise = Promise.resolve() as Promise<any>
2let currentFlushPromise: Promise<void> | null = null
3
4const RECURSION_LIMIT = 100
5type CountMap = Map<SchedulerJob, number>
6
7export function nextTick<T = void, R = void>(
8 this: T,
9 fn?: (this: T) => R,
10): Promise<Awaited<R>> {
11 const p = currentFlushPromise || resolvedPromise
12 return fn ? p.then(this ? fn.bind(this) : fn) : p
13}
找到queueJob、queueFlush、flushJobs源码:
flushJobs
函数代表从缓存队列里面执行响应式数据对应的副作用函数(渲染dom)。
1export function queueJob(job: SchedulerJob) {
2 if (
3 !queue.length ||
4 !queue.includes(
5 job,
6 isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex,
7 )
8 ) {
9 if (job.id == null) {
10 queue.push(job)
11 } else {
12 queue.splice(findInsertionIndex(job.id), 0, job)
13 }
14 queueFlush()
15 }
16}
17
18function queueFlush() {
19 if (!isFlushing && !isFlushPending) {
20 isFlushPending = true
21 currentFlushPromise = resolvedPromise.then(flushJobs)
22 }
23}
找到页面初始化时候的代码:
github1s链接:https://github1s.com/vuejs/core/blob/main/packages/runtime-core/src/renderer.ts
看一下渲染一个组件的测试用例:
渲染一个组件的执行链路是:
调用createRenderer -> baseCreateRenderer
会返回一个渲染函数 render(渲染函数,传入虚拟节点对象和要挂载的节点)
上图可以看到。vue组件初始化渲染
的时候就会把update(副作用函数)传递到queueJob
方法内、并且放到queue内。然后执行queueFlush
方法。
真正的代码执行流程:
1const msg = ref(1)
2
1nextTick(() => {
2 console.info("执行")
3})
执行nextTick之前currentFlushPromise
已经变成了resolvedPromise.then(flushJobs)
那么nextTick的执行自然只能等flushJobs执行完后执行
。
1msg.value=4
2
总结
本质上nextTick
的调用和vue组件执行渲染函数的队列
都通过同一个Promose进行调度。
所以才可以实现nextTick延迟执行
的效果。
形如:
1let p = Promise.resolve()
2let reactive = ()=>{console.log('响应式更新')}
3let nextTick = ()=>{console.log('nextTick回调')}
4p=p.then(reactive)
5p=p.then(nextTick)
6
7