Vue3 渲染调度机制(异步更新)

目录

前言

Vue3 在响应式数据触发更新时,不会立即执行组件渲染,而是通过调度器将更新任务放入队列,并利用微任务在当前事件循环结束后统一批量执行。调度器内部会对任务去重,并按照组件创建顺序排序,确保父子组件更新顺序正确。同时 Vue 还维护 pre 和 post 两类副作用队列,用于控制 watch 等副作用的执行时机,从而实现高性能、可预测的 UI 更新机制。

一、「渲染调度机制」总览

Vue3 渲染调度机制 = 把多次状态变化合并起来,按顺序、分批次、异步地执行组件更新

关键词就三个:

  • 队列
  • 去重
  • 异步批处理

二、为什么需要"调度"?

看一段代码:

typescript 复制代码
state.count++
state.count++
state.count++

如果每次修改都立刻:

typescript 复制代码
trigger → effect → render → patch

那页面会 连续重渲染 3 次,纯纯浪费性能。

Vue 的目标是:

  • 不管你一口气改多少次数据,一个组件一轮事件循环只更新一次

这就是调度器的使命。

三、Vue 的队列

1、Vue 组件更新会立即执行吗?

不会。

Vue 组件更新不会立刻执行,而是先进"更新队列"。

当响应式数据触发 trigger() 时:

typescript 复制代码
triggerEffects(dep)

内部不会直接执行组件的副作用,而是:

typescript 复制代码
queueJob(effect)

也就是说:

❗组件更新 = 被放进一个"待执行任务队列"

2、Vue 的三种核心队列

队列 存什么 什么时候执行
queue 组件更新任务 主要渲染阶段
pendingPreFlushCbs watchEffect / watch (flush:'pre') 渲染前
pendingPostFlushCbs watch(..., { flush:'post' }) DOM 更新后

(1)、调度总入口:queueJob()

源码逻辑(简化):

typescript 复制代码
const queue: Job[] = []
let isFlushing = false

export function queueJob(job) {
  if (!queue.includes(job)) {   // ⭐ 去重
    queue.push(job)
    queueFlush()
  }
}

关键点 1:去重

同一个组件不管触发多少次,只会保留一个更新任务。

(2)、queueFlush():开启"异步更新"

typescript 复制代码
function queueFlush() {
  if (!isFlushing) {
    isFlushing = true
    Promise.resolve().then(flushJobs)
  }
}

关键点 2:微任务异步执行

Vue 使用的是:

typescript 复制代码
Promise.then → 微任务

这就解释了为什么:

typescript 复制代码
state.count++
console.log(dom) // 旧 DOM
await nextTick()
console.log(dom) // 新 DOM

因为 DOM 更新在 本轮同步代码执行完后才发生。

(3)、真正执行更新:flushJobs()

typescript 复制代码
function flushJobs() {
  try {
    flushPreFlushCbs()   // 1️⃣ 执行 pre 队列
    queue.sort(sortJob)  // 2️⃣ 排序(父 → 子)
    
    for (job of queue) { // 3️⃣ 执行组件更新
      job()
    }
  } finally {
    flushPostFlushCbs()  // 4️⃣ 执行 post 队列
    resetSchedulerState()
  }
}

执行顺序非常重要!

  • 先执行 pre 队列
  • 再执行组件更新(render + patch)
  • 最后执行 post 队列
①、先执行 pre 队列

用于:

typescript 复制代码
watchEffect(fn)        // 默认 flush: 'pre'
watch(source, fn)

此时 DOM 还没更新,适合做:

  • 读取旧 DOM 状态
  • 计算下一步逻辑
②、再执行组件更新(render + patch)

就是我们熟悉的:

typescript 复制代码
effect → render → patch

这一步才真正改 DOM。

③、最后执行 post 队列

用于:

typescript 复制代码
watch(source, fn, { flush: 'post' })

此时 DOM 已经是最新的。

适合:

  • 访问更新后的 DOM
  • 操作第三方库

3、为什么还要排序?

typescript 复制代码
queue.sort((a, b) => getId(a) - getId(b))

组件创建时有递增 id:

typescript 复制代码
父组件 id < 子组件 id

排序的意义是:

  • ✅ 保证父组件先更新,子组件后更新

否则可能出现:

  • 子组件基于旧 props 更新
  • 父组件晚更新导致数据错乱

四、调度机制解决了哪些问题?

问题 Vue 怎么解决
多次数据变更频繁重渲染 队列去重 + 批量执行
更新顺序错乱 按组件创建顺序排序
DOM 访问时机混乱 pre / post 队列分离
同步更新阻塞 UI 微任务异步更新

五、nextTick 本质是什么?(⭐️⭐️⭐️⭐️⭐️)

typescript 复制代码
export function nextTick(fn?) {
  return fn ? Promise.resolve().then(fn) : Promise.resolve()
}

它只是:

  • 等待当前这轮"组件更新批处理"结束。

所以:

typescript 复制代码
state.count++
await nextTick()
// 这里 DOM 一定是新的

六、整个调度流程串起来

typescript 复制代码
响应式数据改变
        │
        ▼
trigger()
        │
        ▼
queueJob(组件effect)
        │
        ▼
Promise.then (微任务)
        │
        ▼
flushJobs()
   │         │
   ▼         ▼
pre队列   组件render+patch
              │
              ▼
          post队列
相关推荐
excel8 小时前
研究 Vue 3 源码的收获
前端·vue.js
vx-程序开发16 小时前
【计算机毕设】51300童装超市管理系统的设计与实现
java·vue.js·spring boot·源码·课程设计·毕设·大作业
ym hyd 11116 小时前
题库建立与考试系统源码 Java+SpringBoot+Vue3 前后分离
java·开发语言·vue.js·spring boot·毕设
YZ12255218 小时前
【Docker专题】使用Docker部署Vue-Flask项目前后端分离版【前端Docker部署】
前端·vue.js·docker
专业程序开发源19 小时前
springboot古城景区管理系统88564-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·php·课程设计
小蒜学长19 小时前
基于SSM+VUE的电影售票平台的设计与实现(代码+数据库+LW)
java·vue.js·spring boot·后端·ssm框架·电影售票平台
Q264336502320 小时前
【有源码】基于Spring Boot+Vue的骑行爱好者平台的设计与实现-计算机毕设选题推荐
java·vue.js·spring boot·毕业设计·课程设计·数据可视化·源代码
FYKJ_201020 小时前
SpringSecurity教室智能预约系统73972-计算机课程设计、毕业设计
java·vue.js·spring boot·python·mysql·spring·django
专业程序开发源20 小时前
springboot酒店会员点餐系统22339-计算机课程设计、毕业设计
java·javascript·vue.js·spring boot·后端·php·课程设计
FYKJ_201020 小时前
springboot游泳馆管理系统79222-计算机课程设计、毕业设计
vue.js·spring boot·后端·python·mysql·django·课程设计