zhangdizhangdi

调度器 scheduler

负责的任务

  • 批量:同一事件循环里多次 set → 只更新一次。
  • 去重:同一个 job(比如同一组件实例的更新)只入队一次。
  • 阶段顺序:
    • pre:渲染前的回调(watch(…, { flush: ‘pre’ })、onBeforeUpdate)。
    • jobs:组件渲染更新 job(由渲染 effect 的 scheduler 入队)。
    • post:渲染后的回调(watch(…, { flush: ‘post’ })、onUpdated、nextTick 之后看到的 DOM)。
  • 时机:统一在 微任务(Promise.resolve().then(…))里 一次性 flush。

主要文件

  • runtime-core/scheduler.ts
    • queueJob:看 isFlushing / isFlushPending 如何决定“什么时候安排 flush”
    • flushJobs:看 flushIndex 的推进与 排序/去重 逻辑
    • queuePostFlushCb:看相同回调如何去重
    • nextTick:返回 currentFlushPromise
  • runtime-core/renderer.ts(或 createRenderer.ts)
    • 组件的渲染 effect 被创建时,会传入一个 scheduler,其实现是 queueJob(componentUpdateJob)。
  • runtime-core/apiWatch.ts
    • doWatch 里给 watch/watchEffect 的 effect 配置 scheduler(根据 flush 选择入哪个阶段)。
    • 找到 doWatch,看 const job = () => { … } 与 scheduler 如何把 job 放到 pre/post/queue
  • reactivity/computed.ts
    • computed 的内部 effect 有自己的 scheduler(不入队渲染,只是标记 dirty 并触发依赖)。

关键概念

  • job:一次可执行任务(函数)。组件更新 job 通常带 job.id = instance.uid,用于稳定排序与去重。
  • 三类队列:
    • preFlushCbs(去重 Set/队列):渲染前回调。
    • queue:组件更新等“主队列”。
    • postFlushCbs:渲染后回调。
  • 状态位:isFlushing、isFlushPending、flushIndex(当前执行到哪)、currentFlushPromise(nextTick 用)。

流程

js
state.xxx = newVal
trigger(target, key)
对应 effect触发
 effect scheduler则调用 scheduler(job/runner)
         - 组件更新queueJob(updateComponentJob)
         - watch 'pre'queuePreFlushCb(cb)
         - watch 'post'queuePostFlushCb(cb)
启动一次微任务 flushqueueFlush
微任务阶段
flushPreFlushCbs()
flushJobs()( job.id 排序去重依次执行渲染
flushPostFlushCbs()

💡 案例分析

你会在控制台看到的大致顺序(点击「连续 +3」):

  1. [watch sync] …(同步,立即打印 3 次)
  2. 微任务 flush 开始
    • 2.1 pre:[watch pre] …(只执行 1 次,取最终值)
    • 2.2 jobs:子组件渲染更新(onBeforeUpdate → 重新 render)
    • 2.3 post:onUpdated 等
  3. 再点击「msg 变化」:
    • 先打印点击日志
    • flush 时:先跑可能存在的 pre → 跑 jobs(渲染)→ 跑 post([watch post])
    • await nextTick() 之后的日志在 post 之后

断点提示:

  • 点「连续 +3」后,断点在 queueJob 可看到只入队一次的组件更新 job;
  • flushJobs 里能看到 排序(按 job.id)和 去重;
  • queuePostFlushCb 在修改 msg 时会被命中。

  • computed:内部 effect 的 scheduler 不入队,只做:

    1. 标记 dirty = true
    2. triggerRefValue(this) 通知依赖它的外部 effect/渲染在下一次访问再重新计算 → 因此它是“拉取式(lazy)”更新。
  • watch / watchEffect:由 doWatch 为 effect 设置 scheduler:

    1. flush: ‘pre’ → queuePreFlushCb
    2. flush: ‘post’ → queuePostFlushCb
    3. flush: ‘sync’ → 直接运行(无队列)

flags

ts
export enum SchedulerJobFlags {
  QUEUED = 1 << 0, // 二进制: 0001 十进制: 1
  PRE = 1 << 1, //  二进制: 0010 十进制: 2
  ALLOW_RECURSE = 1 << 2, // 二进制: 0100 十进制: 4
  DISPOSED = 1 << 3, // 二进制: 1000 十进制: 8
}

QUEUED

作用: 标识任务已经被添加到调度队列中
用途: 防止同一个任务被重复添加到队列
场景: 确保任务去重,避免重复调度

PRE

作用: 标识任务是一个预处理任务
用途: 预处理任务可以在任务执行之前执行,比如在任务执行之前更新状态,或者提前计算一些数据
场景:

  • 指令的 beforeUpdate 钩子
  • flush: “pre” 的 watch 回调
  • 组件更新前的准备工作

ALLOW_RECURSE

作用: 允许任务递归执行
场景:

  • 组件更新函数
  • watch 回调

DISPOSED

作用: 标识任务已被销毁/释放
场景:

  • 组件卸载时清理相关任务
  • effect 作用域停止时