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回调
相关推荐
喵叔哟18 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
man20171 小时前
【2024最新】基于springboot+vue的闲一品交易平台lw+ppt
vue.js·spring boot·后端
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js