阅读视图

发现新文章,点击刷新页面。

Vue3架构设计——调度系统

调度本义是指控制一系列任务的执行顺序/编排规划。Vue3 的调度系统使其能够做到**“批量更新、不重复渲染、任务执行顺序可控”** 。

Vue 的调度系统 = 副作用执行顺序 + 去重 + 批量刷新

所有响应式变化,最终都不会“立刻执行”,而是被“调度”

一、Vue 为什么需要调度系统?

如果没有调度,会发生什么?

state.a++
state.b++
state.c++

如果每次 set 都立即触发:

render()
render()
render()

造成后果:

  • 性能问题
  • 顺序不可控
  • DOM 不断更改,页面抖动

所以,Vue 的目标是:

state.a++
state.b++
state.c++
↓
render()  // 只执行一次(Scheduler 存在的意义)

二、调度系统的数据结构

源码中的位置:packages/runtime-core/src/scheduler.ts

运行时(runtime)调度,对 effect 进行 “统一执行管理”。

调度系统不关心数据,只关心:

2.1 Job 的本质

type SchedulerJob = Function & {
  id?: number
  flags?: number
}
  • 没有 id,直接 push 进队列
  • 有 id,按照顺序通过二分查找插入到合适的位置

Job ≈ effect.run / component update

2.2 核心队列

const queue: SchedulerJob[] = []

所有待执行任务,都会进这个队列。

2.3 任务去重

const queued = new Set<SchedulerJob>()

同一个 job,一个 flush 周期只会进队一次

三、调度入口:queueJob

export function queueJob(job: SchedulerJob) {
  if (!queued.has(job)) {
    queued.add(job)
    queue.push(job)
    queueFlush()
  }
}
  1. 去重(比如说 count++ 多次,最终的更新只需要一次)
  2. 入队
  3. 触发 flush

四、flush:真正执行的地方

function queueFlush() {
  if (!isFlushing) {
    isFlushing = true
    resolvedPromise.then(flushJobs)
  }
}

Vue 的调度基于 microtask(Promise.then)

所以:

同步代码 → 全跑完
↓
flushJobs(统一执行)

五、flushJobs 的核心逻辑

function flushJobs() {
  try {
    // 批量执行 所有 job 集中执行一次
    for (let i = 0; i < queue.length; i++) {
      const job = queue[i]
      job()
    }
  } finally {
    queue.length = 0
    queued.clear()
    isFlushing = false
  }
}

六、组件更新的调度

每个组件都有一个 render effect

const effect = new ReactiveEffect(componentUpdateFn)

scheduler 被设置为:

scheduler = () => queueJob(update) // UI 更新
state change
 ↓
trigger
 ↓
component render effect.scheduler
 ↓
queueJob(update)
 ↓
flushJobs(异步更新)
 ↓
update() → render()

七、computed / watch 在调度系统中的位置

7.1 computed 的 scheduler

scheduler = () => {
  dirty = true
  trigger(computed.dep)
}

computed 的任务调度不进 scheduler 队列(queueJob),只影响依赖它的 effect

7.2 watch 的 scheduler

scheduler = () => {
  queueJob(job)
}

watch 直接进入调度系统(具体进入哪个优先层级取决于 flush ,默认为 queueJob)

八、flush: pre / post / sync

Vue 的调度系统 不是一个队列,而是三个层级

三种 flush 模式

8.1 pre 队列(默认 watch)

queuePreFlushCb(job)

用于:

  • watch
  • beforeUpdate

8.2 post 队列(DOM 后)

queuePostFlushCb(job)

用于:

  • watch(flush: 'post')
  • onMounted / onUpdated

8.3 执行顺序

flushPreFlushCbs
 ↓
flushJobs(组件更新)
 ↓
flushPostFlushCbs

九、nextTick 的本质

export function nextTick(fn?) {
  return fn
    ? resolvedPromise.then(fn)
    : resolvedPromise
}

所以 nextTick 本质是:等当前调度周期 flush 完(在原本调度系统 Promise.then(调度任务队列) 的后面又拼接了一个 .then(nextTick任务)

DOM 更新会在原本的调度系统中,所以 nextTick 在开发中一般用于获取最新的 DOM 。

十、简单示例

watch(state, () => console.log('watch'))

state.count++

console.log('sync')

nextTick(() => console.log('tick'))

执行顺序:

sync
watch
render
tick

十一、为什么 Vue 不用 setTimeout / requestAnimationFrame?

Vue 的目标是:“同步代码结束后,立刻统一刷新”

Vue3 响应式系统——ref 和 reactive

一、Vue3 响应式系统概述

Vue3 响应式包 @vue/reactivity,核心由三部分构成:

数据 (Proxy Object)  —— 依赖收集 Track  —— 触发更新 Trigger  ——  Effect 执行更新

核心目标:

  • 拦截读取和设置操作
  • 收集依赖
  • 在数据变化时重新触发相关副作用

主要实现 API:

二、reactive() 执行机制

2.1 核心逻辑(核心源码)

function reactive(target) {
  return createReactiveObject(target, false, mutableHandlers)
}

function createReactiveObject(target, isReadonly, baseHandlers) {
  if (!isObject(target)) {
    return target
  }
  if (target already has proxy) return existing proxy
  const proxy = new Proxy(target, baseHandlers)
  cache proxy
  return proxy
}

Vue3 用 Proxy 拦截对象操作,比 Vue2 的 Object.defineProperty 更强(能监听属性增删)。

2.2 reactive 的 handler(简化)

const mutableHandlers = {
  get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver)
    track(target, key)
    return isObject(res) ? reactive(res) : res
  },
  set(target, key, value, receiver) {
    const oldValue = target[key]
    const result = Reflect.set(target, key, value, receiver)
    if (oldValue !== value) {
      trigger(target, key)
    }
    return result
  }
}

三、依赖收集和触发更新:track()trigger()

Vue 内部维护一个 全局的 activeEffect

let activeEffect = null

function effect(fn) {
  activeEffect = wrappedEffect(fn)
  fn() // 执行一次用于收集依赖
  activeEffect = null
}

每次读取(get)响应式数据时:

function track(target, key) {
  if (!activeEffect) return
  const depsMap = targetMap.get(target) || new Map()
  const dep = depsMap.get(key) || new Set()
  dep.add(activeEffect)
}

当数据被设置(set)时:

function trigger(target, key) {
  const depsMap = targetMap.get(target)
  const dep = depsMap?.get(key)
  dep?.forEach(effect => effect())
}
  • track 只在 读取时收集依赖
  • trigger 只在 数据修改时触发 effect 重新执行

四、ref() 的设计与区别

4.1 ref 是什么?

ref() 主要用于包装 基本类型 (对于对象引用类型内部直接调用上面的 reactive()):

const count = ref(0)

其结构本质上是:

interface RefImpl {
  value: T
}

源码核心:

function ref(rawValue) {
  return createRef(rawValue)
}

function createRef(rawValue) {
  const refImpl = { 
    _value: convert(rawValue), 
    dep: new Set(), // 区别于reactive引用类型复杂的多层嵌套数据结构封装dep,ref这里直接在实例中存放一个dep来实现
    get value() {
      trackRefValue(refImpl)
      return refImpl._value
    },
    set value(newVal) {
      if (hasChanged(newVal, refImpl._value)) {
        refImpl._value = convert(newVal)
        triggerRefValue(refImpl)
      }
    }
  }
  return refImpl
}

4.2 ref vs reactive 的本质区别

五、template / setup 中的自动 unwrap

在 Vue 模板中:

<p>{{ count }}</p>

如果 count 是一个 ref,它会 自动解包,模板中不需要写 .value。这是由编译阶段的 transform 实现的。

六、响应式系统执行流程图(简化)

reactive/ref 数据 -> Proxy getter -> track
                           │
                        effect 注册
                           │
                    数据 setter -> trigger
                           ↓
                    重新执行 effect
❌