Vue的nextTick方法源码深度解读

先看看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回调
相关推荐
abc80021170341 小时前
前端Bug 修复手册
前端·bug
Best_Liu~1 小时前
el-table实现固定列,及解决固定列导致部分滚动条无法拖动的问题
前端·javascript·vue.js
_斯洛伐克2 小时前
下降npm版本
前端·vue.js
苏十八3 小时前
前端进阶:Vue.js
前端·javascript·vue.js·前端框架·npm·node.js·ecmascript
st紫月3 小时前
用MySQL+node+vue做一个学生信息管理系统(四):制作增加、删除、修改的组件和对应的路由
前端·vue.js·mysql
乐容4 小时前
vue3使用pinia中的actions,需要调用接口的话
前端·javascript·vue.js
似水明俊德4 小时前
ASP.NET Core Blazor 5:Blazor表单和数据
java·前端·javascript·html·asp.net
至天5 小时前
UniApp 中 Web/H5 正确使用反向代理解决跨域问题
前端·uni-app·vue3·vue2·vite·反向代理
与墨学长5 小时前
Rust破界:前端革新与Vite重构的深度透视(中)
开发语言·前端·rust·前端框架·wasm
H-J-L6 小时前
Web基础与HTTP协议
前端·http·php