Vue 3响应式原理源码解析

19 阅读15分钟

在查看源码后,发现Vue 3的响应式系统代码主要位于packages/reactivity/src目录下,包含以下核心文件:

  1. reactive.ts - 创建响应式对象的主要API
  2. effect.ts - 副作用函数的实现
  3. ref.ts - ref相关API的实现
  4. dep.ts - 依赖收集系统
  5. baseHandlers.ts - 基本类型的代理处理器
  6. computed.ts - 计算属性实现

让我们一步步分析Vue 3响应式系统的核心原理:

Vue3的响应式和vue2的原理一致:采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()/Proxy来劫持各个属性的setter、getter,在数据变动时发布消息给订阅者,触发相应的监听回调

vue3 同vue2 重点不同的内容在于:

  1. 使用 Proxy 替代了 Object.defineProperty
  2. 使用 effect 函数创建响应式副作用,它在内部执行时会自动收集依赖,当依赖变化时自动重新执行;对应的作用类似vue2的watcher
  3. 使用了 Options API 的写法,将响应式相关的逻辑分散在多个文件中,而不是像 Vue 2 那样集中在一个文件中

Vue 3响应式原理时序图与部分原理源码记录如下:

Vue 3 响应式原理时序图

sequenceDiagram
    participant Developer as 开发者
    participant Vue as Vue应用
    participant Reactive as reactive API
    participant Proxy as Proxy对象
    participant Effect as 副作用函数(effect)
    participant Track as 依赖收集(track)
    participant Trigger as 触发更新(trigger)
    participant Component as 组件(Component)
    
    Developer->>Vue: 创建Vue应用并提供响应式数据
    
    Note over Vue: 初始化阶段
    
    Vue->>Reactive: 调用reactive(data)
    Note over Reactive: 【核心点1】<br/>使用Proxy创建响应式代理<br/>而非Object.defineProperty
    
    Reactive->>Proxy: 创建Proxy代理对象
    Proxy-->>Vue: 返回代理后的响应式对象
    
    Vue->>Component: 组件挂载,开始渲染
    
    Component->>Effect: 创建渲染effect
    
    Note over Effect,Track: 【核心点2】依赖收集阶段
    
    Effect->>Effect: activeEffect = 当前effect
    
    loop 模板中使用的每个响应式属性
        Component->>Proxy: 读取属性值(触发get trap)
        Proxy->>Track: 调用track()函数
        Track->>Track: 收集当前activeEffect作为依赖
        Note over Track: 使用WeakMap+Map+Set结构存储依赖
    end
    
    Effect->>Effect: activeEffect = null
    
    Note over Developer,Trigger: 【核心点3】数据更新阶段
    
    Developer->>Proxy: 修改数据(如:state.message = "新值")
    Proxy->>Trigger: 触发set trap,调用trigger()函数
    
    Note over Trigger: 通知所有相关effect更新
    
    Trigger->>Effect: 调度执行(scheduler)
    
    Note over Effect: 【核心点4】<br/>自动批处理更新

    Effect->>Component: 重新执行组件渲染函数

官网对响应式的说明: cn.vuejs.org/guide/extra…

现在让我们开始分析源码实现:

二、创建响应式对象 (reactive.ts)

从源码中可以看到,Vue 3提供了几种创建响应式对象的API:

  1. reactive - 深度响应式
  2. shallowReactive - 浅响应式
  3. readonly - 只读响应式
  4. shallowReadonly - 浅只读响应式

它们都是通过createReactiveObject函数实现的,核心代码如下:

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>,
) {
  // 如果目标不是对象,则直接返回
  if (!isObject(target)) {
    return target
  }
  
  // 如果目标已经是Proxy,直接返回
  if (target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {
    return target
  }
  
  // 已经有对应的Proxy,则直接返回
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  
  // 创建新的Proxy
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
  )
  proxyMap.set(target, proxy)
  return proxy
}

这里最关键的部分是使用了JavaScript的Proxy对象来创建响应式对象,它允许我们拦截对象的基本操作。不同类型的响应式对象使用不同的handlers来处理对象操作。

让我们来看看handlers是如何实现的:刚刚我查看了Vue 3响应式系统的核心文件reactive.tsbaseHandlers.ts。现在让我继续为您解析Vue 3响应式原理的源码:

三、Proxy处理器实现(baseHandlers.ts)

从源码中可以看到,Vue 3使用了不同的处理器来处理不同类型的响应式对象:

  1. mutableHandlers - 可变响应式处理器
  2. readonlyHandlers - 只读响应式处理器
  3. shallowReactiveHandlers - 浅层可变响应式处理器
  4. shallowReadonlyHandlers - 浅层只读响应式处理器

这些处理器都继承自BaseReactiveHandler,关键的实现有:

1. get 拦截器

get(target: Target, key: string | symbol, receiver: object): any {
  // ...特殊标志处理...
  
  // 非只读模式下跟踪依赖
  if (!isReadonly) {
    track(target, TrackOpTypes.GET, key)
  }
  
  // 浅层模式直接返回
  if (isShallow) {
    return res
  }
  
  // 自动解包ref对象
  if (isRef(res)) {
    return targetIsArray && isIntegerKey(key) ? res : res.value
  }
  
  // 对象类型递归转换
  if (isObject(res)) {
    return isReadonly ? readonly(res) : reactive(res)
  }
  
  return res
}

这里的关键点是:

  • 通过track函数收集依赖
  • 自动解包ref类型
  • 对于对象类型值,递归转换为响应式

2. set 拦截器

set(target: Record<string | symbol, unknown>, key: string | symbol, value: unknown, receiver: object): boolean {
  let oldValue = target[key]
  // ...处理value...
  
  const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key)
  const result = Reflect.set(target, key, value, receiver)
  
  // 触发更新
  if (target === toRaw(receiver)) {
    if (!hadKey) {
      // 新增属性
      trigger(target, TriggerOpTypes.ADD, key, value)
    } else if (hasChanged(value, oldValue)) {
      // 设置属性
      trigger(target, TriggerOpTypes.SET, key, value, oldValue)
    }
  }
  
  return result
}

关键点是:

  • 使用trigger函数在属性变化时触发更新
  • 区分ADD和SET操作

让我们继续看依赖收集和触发更新的实现:接下来,让我们继续分析Vue 3响应式系统的核心源码:

四、依赖收集和触发更新(dep.ts)

dep.ts文件中,我们可以看到Vue 3响应式系统的依赖收集和触发更新的具体实现:

1. 核心数据结构

// 存储依赖关系的核心WeakMap
export const targetMap: WeakMap<object, KeyToDepMap> = new WeakMap()

这个结构非常关键,它使用WeakMap来存储所有的依赖关系:

  • key: 响应式对象
  • value: 该对象的依赖Map(key -> dep)

2. 依赖收集方法 track

export function track(target: object, type: TrackOpTypes, key: unknown): void {
  if (shouldTrack && activeSub) {
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = new Dep()))
      dep.map = depsMap
      dep.key = key
    }
    dep.track()
  }
}

这个函数在属性被访问时调用,进行依赖收集:

  1. 获取目标对象的依赖Map,不存在则创建
  2. 获取特定key的Dep实例,不存在则创建
  3. 调用dep.track()将当前活动的effect收集为依赖

3. 触发更新方法 trigger

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>,
): void {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    return
  }

  // 根据不同的操作类型触发相应的依赖更新
  if (type === TriggerOpTypes.CLEAR) {
    // 清空集合,触发所有依赖
    depsMap.forEach(run)
  } else {
    // 处理SET、ADD、DELETE操作
    if (key !== void 0) {
      run(depsMap.get(key))
    }
    
    // 特殊处理数组和集合类型
    // ...
  }
}

这个函数在属性被修改时调用,触发依赖更新:

  1. 获取目标对象的依赖Map
  2. 根据操作类型(ADD/SET/DELETE/CLEAR)找到相关的依赖
  3. 调用这些依赖执行更新

接下来,让我们看一下effect的实现,这是整个响应式系统的另一个核心部分:现在,我来继续分析Vue 3响应式系统的核心源码:

五、副作用函数 (effect.ts)

effect.ts文件包含了Vue 3响应式系统中副作用函数的实现,这是整个响应式系统的核心。

1. effect函数

export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions,
): ReactiveEffectRunner<T> {
  // 创建ReactiveEffect实例
  const e = new ReactiveEffect(fn)
  
  // 合并选项
  if (options) {
    extend(e, options)
  }
  
  // 首次执行
  try {
    e.run()
  } catch (err) {
    e.stop()
    throw err
  }
  
  // 返回绑定了this的runner函数
  const runner = e.run.bind(e) as ReactiveEffectRunner
  runner.effect = e
  return runner
}

这个函数:

  1. 创建一个ReactiveEffect实例,传入要执行的函数
  2. 立即执行一次该函数(e.run()),触发依赖收集
  3. 返回一个runner函数,可以手动触发该effect

2. ReactiveEffect类

export class ReactiveEffect<T = any> implements Subscriber, ReactiveEffectOptions {
  // ...

  constructor(public fn: () => T) {
    if (activeEffectScope && activeEffectScope.active) {
      activeEffectScope.effects.push(this)
    }
  }

  run(): T {
    if (!(this.flags & EffectFlags.ACTIVE)) {
      return this.fn()
    }

    this.flags |= EffectFlags.RUNNING
    cleanupEffect(this)
    prepareDeps(this)
    const prevEffect = activeSub
    const prevShouldTrack = shouldTrack
    activeSub = this
    shouldTrack = true

    try {
      return this.fn()
    } finally {
      cleanupDeps(this)
      activeSub = prevEffect
      shouldTrack = prevShouldTrack
      this.flags &= ~EffectFlags.RUNNING
    }
  }

  notify(): void {
    if (
      this.flags & EffectFlags.RUNNING &&
      !(this.flags & EffectFlags.ALLOW_RECURSE)
    ) {
      return
    }
    if (!(this.flags & EffectFlags.NOTIFIED)) {
      batch(this)
    }
  }

  trigger(): void {
    if (this.flags & EffectFlags.PAUSED) {
      pausedQueueEffects.add(this)
    } else if (this.scheduler) {
      this.scheduler()
    } else {
      this.runIfDirty()
    }
  }
}

核心方法是run(),它会:

  1. 设置当前活动的副作用函数为自身
  2. 允许依赖追踪
  3. 执行包装的函数,在执行过程中会访问响应式数据,从而触发依赖收集
  4. 恢复之前的环境状态

为什么需要 const prevEffect = activeSub 存储上一个activeSub??

在vue.js 设计与实现这本书上有以下说明:

这是在副作用函数嵌套的时候使用的一个全局参数,拿 vue.js 来说,实际上Vue.js 的渲染函数就是一个effect中执行的:

const Foo = {
  render() {
    return ......
  }
}

// 其实上面的代码对应的如下:
effect(()=> {
  Foo.render()
})

组件是嵌套的,那么就会遇到下面的情况:

const Bar = {
  render() {
    return 'xxx'
  }
}
// 组件内使用了子组件
const Foo = {
  render() {
    return <Bar />
  }
}

这样就产生effect嵌套,它相当于:

effect(()=>{
  Foo.render()
  effect(()=>{
    Bar.render()
  })
})

单一个全局的activeEffect(源码中是 activeSub)所存储的副作用函数只能是一个,当副作用函数发生嵌套的时候,内层的副作用函数的执行会覆盖 activeEffect 的值,并且永远不会恢复到原来的状态。覆盖之后,外层effect如果再有响应式数据进行依赖收集,就会拿到内层的副作用函数,这是问题所在 照理来说,每个组件有自己的effect,当某个组件内有依赖发生变化的时候,只触发该组件的渲染effect,而不会影响其他组件。

为了解决这个问题,需要一个副作用函数栈 effectStack,在收集依赖前保存外层effect,收集完成之后,恢复到原状态。这样就能做到 一个响应式数据只会收集直接读取其值的副作用函数

书中说明的原理非常简单

而源码是实现依靠的是 const prevEffect = activeSub 保存当前活跃的副作用函数(effect),以便在当前effect执行完毕后能正确恢复上下文环境

在ReactiveEffect的run方法中,执行流程是:

  1. 保存当前活跃的effect(prevEffect = activeSub)
  2. 设置当前effect为活跃effect(activeSub = this)
  3. 执行当前effect的函数体(this.fn())
  4. 在finally中恢复之前保存的effect(activeSub = prevEffect)

这种设计确保了即使在嵌套的Effect的情况下,每个Effect执行完毕后都能正确恢复到之前的执行上下文,避免副作用追踪混乱


看看源码哪里出现的嵌套Effect:

源码中嵌套组件就会产生如此的情况:vue中renderEffect的创建在:packages/runtime-core/src/renderer.ts 的 setupRenderEffect 中,里面会执行下面代码去创建渲染 effect

function componentUpdateFn = ()=>{
  //...
  const subTree = (instance.subTree = renderComponentRoot(instance))

  patch(
    null, 
    subTree,
    container,
    anchor,
    instance,
    parentSuspense,
    namespace,
  )
}
// ...

 const effect = (instance.effect = new ReactiveEffect(componentUpdateFn))

而componentUpdateFn中又会调用 renderComponentRoot 生成组件子树 vNode ,然后调用patch渲染子组件。对于子组件,会重复上面的过程,创建新的渲染effect,嵌套的Effect就出现了

关于 Vue.js 设计与实现 书中提到的 无限循环问题

在《vue.js 设计与实现》一书中有提到下面的例子

const data = reactive({
  count: 0
})

effect(()=>{
  data.count++
})

这段代码中,effect 中的回调函数会先读取data.count的值,然后触发 track 操作将当前副作用函数收集到targetMap中。接着将data中的count值加1,此时会触发proxy中 set 拦截器里面的 trigger 操作,把当前副作用函数从targetMap中拿出来执行。当前副作用函数未有执行完成,就又触发了track操作,如果处理不当,就会导致无限循环,最终栈溢出爆错

因为分析问题发现,读取和设置操作都是在同一个副作用函数中执行的。此时无论是收集依赖的track操作,还是触发更新的trigger操作,获取的全局唯一变量 activeEffect 都是同一个。基于此,可以添加守卫条件:如果trigger触发执行的副作用函数与当前执行的副作用函数相同,则不触发执行

书中的解决方案是:

function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if(!depsMap) return
  const effects = depsMap.get(key)

  const effectsToRun = new Set()
  effects && effects.forEach(effect => {
    // 如果trigger触发执行的副作用函数与当前执行的副作用函数相同,则不触发执行,其中全局变量activeEffect是(在源码中是activeSub)当前正在执行的副作用函数
    if(effect !== activeEffect) {
      effectsToRun.add(effect)
    }
  })

  effectsToRun.forEach(effect => effect())
}

原理非常简单,那么对应源码中是如何实现的呢?


当响应式对象的属性被修改时(例如 data.count++),会触发以下流程:

  1. 首先调用 packages/reactivity/src/baseHandlers.ts 中的 set 拦截器

  2. set 拦截器会调用 trigger(target, TriggerOpTypes.SET, key, value, oldValue) 方法

  3. packages/reactivity/src/dep.ts 中的 trigger 函数执行以下步骤:

    export function trigger(
      target: object,
      type: TriggerOpTypes,
      key?: unknown,
      newValue?: unknown,
      oldValue?: unknown,
      oldTarget?: Map<unknown, unknown> | Set<unknown>,
    ): void {
      const depsMap = targetMap.get(target)
      if (!depsMap) {
        // 如果没有被跟踪,直接返回
        globalVersion++
        return
      }
    
      const run = (dep: Dep | undefined) => {
        if (dep) {
          // 调用每个依赖对象的 trigger 方法
          dep.trigger()
        }
      }
    
      // ... 根据不同的操作类型(ADD/SET/DELETE等)找到相关的依赖
      
      // 对于 SET 操作,会调用:
      run(depsMap.get(key))
      
      // ... 其他相关依赖的处理
    }
    
  4. 每个依赖(Dep 实例)的 trigger 方法会执行:

    trigger(debugInfo?: DebuggerEventExtraInfo): void {
      this.version++         // 更新版本号
      globalVersion++        // 更新全局版本号
      this.notify(debugInfo) // 调用 notify 方法通知所有订阅者
    }
    
  5. Dep.notify 方法会遍历所有订阅该依赖的订阅者(即 effect),并调用它们的 notify 方法:

    notify(debugInfo?: DebuggerEventExtraInfo): void {
      startBatch()
      try {
        // ... 开发环境下的调试代码
        
        // 遍历所有订阅者并调用它们的 notify 方法
        for (let link = this.subs; link; link = link.prevSub) {
          if (link.sub.notify()) {
            // 如果是计算属性,还需要通知计算属性的依赖
            (link.sub as ComputedRefImpl).dep.notify()
          }
        }
      } finally {
        endBatch()
      }
    }
    
  6. 最终会调用到 ReactiveEffect 实例的 notify 方法,这里就是关键的避免递归循环的逻辑:

    notify(): void {
      if (
        this.flags & EffectFlags.RUNNING &&
        !(this.flags & EffectFlags.ALLOW_RECURSE)
      ) {
        return; // 如果当前effect正在运行且不允许递归,直接返回
      }
      if (!(this.flags & EffectFlags.NOTIFIED)) {
        batch(this);
      }
    }
    
  7. 如果 effect 没有被标记为 RUNNING 或被允许递归(ALLOW_RECURSE),则会通过 batch 函数将其加入到批处理队列中等待执行

  8. batch 函数将 effect 标记为 NOTIFIED 并加入到批处理队列:

    export function batch(sub: Subscriber, isComputed = false): void {
      sub.flags |= EffectFlags.NOTIFIED
      if (isComputed) {
        sub.next = batchedComputed
        batchedComputed = sub
        return
      }
      sub.next = batchedSub
      batchedSub = sub
    }
    
  9. 在批处理结束时(通过 endBatch),所有收集的 effect 会被触发:

    export function endBatch(): void {
      // ... 处理计算属性
    
      let error: unknown
      while (batchedSub) {
        let e: Subscriber | undefined = batchedSub
        batchedSub = undefined
        while (e) {
          const next: Subscriber | undefined = e.next
          e.next = undefined
          e.flags &= ~EffectFlags.NOTIFIED
          if (e.flags & EffectFlags.ACTIVE) {
            try {
              // ACTIVE flag is effect-only
              (e as ReactiveEffect).trigger()
            } catch (err) {
              if (!error) error = err
            }
          }
          e = next
        }
      }
    
      if (error) throw error
    }
    
  10. 最终会调用 effect 的 trigger 方法,这个方法会根据情况决定如何触发 effect:

    trigger(): void {
      if (this.flags & EffectFlags.PAUSED) {
        pausedQueueEffects.add(this)
      } else if (this.scheduler) {
        this.scheduler()
      } else {
        this.runIfDirty()
      }
    }
    

总结来说,当你执行 data.count++ 操作时:

  1. 触发 set 拦截器
  2. 调用 trigger 函数
  3. trigger 函数找到与 count 相关的所有依赖(Dep)
  4. 对每个依赖调用 dep.trigger(),增加版本号并调用 dep.notify()
  5. dep.notify() 遍历所有订阅此依赖的 effect,调用它们的 notify() 方法
  6. 在 effect 的 notify() 方法中,检查 effect 是否正在运行且不允许递归
    • 如果正在运行且不允许递归,直接返回,阻止无限循环
    • 否则将 effect 加入批处理队列
  7. 批处理结束后触发所有排队的 effect

关键就在第6步,如果 effect 正在运行(RUNNING标志)且没有被允许递归(ALLOW_RECURSE标志),就会跳过当前的通知,从而避免无限递归循环

各种位运算,真是让头头大;其实结果就两种,0 和 1,0 表示当前effect未运行,1 表示当前effect正在运行

六、计算属性实现 (computed.ts)

现在让我们看看计算属性的实现:

计算属性是Vue中非常重要的特性,从源码中可以看到它的实现原理:

export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false,
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T> | undefined

  // 支持函数形式和对象形式
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  // 创建ComputedRefImpl实例
  const cRef = new ComputedRefImpl(getter, setter, isSSR)
  
  return cRef as any
}

核心是ComputedRefImpl类的实现:

export class ComputedRefImpl<T = any> implements Subscriber {
  _value: any = undefined
  readonly dep: Dep = new Dep(this)
  readonly __v_isRef = true
  
  // 依赖收集相关属性
  deps?: Link = undefined
  depsTail?: Link = undefined
  flags: EffectFlags = EffectFlags.DIRTY
  
  constructor(
    public fn: ComputedGetter<T>,
    private readonly setter: ComputedSetter<T> | undefined,
    isSSR: boolean,
  ) {
    this[ReactiveFlags.IS_READONLY] = !setter
    this.isSSR = isSSR
  }
  
  get value(): T {
    // 收集当前计算属性作为依赖
    const link = this.dep.track()
    // 重新计算值(如果脏了)
    refreshComputed(this)
    // 同步版本
    if (link) {
      link.version = this.dep.version
    }
    return this._value
  }
  
  set value(newValue) {
    if (this.setter) {
      this.setter(newValue)
    } else if (__DEV__) {
      warn('Write operation failed: computed value is readonly')
    }
  }
}

计算属性的核心实现:

  1. 创建一个特殊的Ref对象,带有getter和可选的setter
  2. 当访问value属性时,收集依赖并返回计算结果
  3. 当依赖项变化时,标记计算属性为"脏"
  4. 下次访问时重新计算值

七、Ref实现(ref.ts)

让我们来看看ref的实现:## 七、Ref实现(ref.ts)

让我们看看Vue 3响应式系统中ref的实现:

export function ref(value?: unknown) {
  return createRef(value, false)
}

function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

核心是RefImpl类的实现:

class RefImpl<T = any> {
  _value: T
  private _rawValue: T
  dep: Dep = new Dep()
  public readonly [ReactiveFlags.IS_REF] = true

  constructor(value: T, isShallow: boolean) {
    this._rawValue = isShallow ? value : toRaw(value)
    this._value = isShallow ? value : toReactive(value)
    this[ReactiveFlags.IS_SHALLOW] = isShallow
  }

  get value() {
    this.dep.track()
    return this._value
  }

  set value(newValue) {
    const oldValue = this._rawValue
    // 处理新值
    newValue = useDirectValue ? newValue : toRaw(newValue)
    if (hasChanged(newValue, oldValue)) {
      this._rawValue = newValue
      this._value = useDirectValue ? newValue : toReactive(newValue)
      this.dep.trigger()
    }
  }
}

从源码可以看出:

  1. ref是一个包装对象,提供了.value属性的读写访问
  2. 在访问.value时会调用track收集依赖
  3. 在设置.value时会调用trigger触发更新
  4. 如果值是对象,会被转换为响应式对象(非浅层模式)

八、响应式系统的工作流程总结

现在,让我们综合上面的源码分析,梳理出Vue 3响应式系统的工作流程:

1. 创建响应式对象

  • 通过reactiveref等API创建响应式对象
  • reactive使用Proxy拦截对象操作
  • ref则使用getter/setter实现

2. 依赖收集

当组件渲染或计算属性计算时:

  • 创建effect实例
  • 执行组件渲染或计算函数
  • 在过程中访问响应式对象的属性
  • 触发Proxy的get或ref的get value
  • 通过track函数收集当前effect作为依赖

3. 更新触发

当响应式数据变更时:

  • 触发Proxy的set或ref的set value
  • 通过trigger函数找到相关依赖
  • 将这些依赖加入更新队列
  • 调度执行这些依赖

4. 批量更新优化

  • 使用startBatchendBatch函数来批量处理更新
  • 避免同一个effect被多次触发
  • 确保更新顺序正确

九、Vue 3响应式系统的设计亮点

从源码分析中,我们可以看到Vue 3响应式系统的几个亮点:

  1. 使用Proxy替代Object.defineProperty

    • 可以监听整个对象,包括属性的添加和删除
    • 可以监听数组的变化,不需要特殊处理
  2. 分包设计

    • 将响应式系统作为独立的包@vue/reactivity
    • 可以单独使用,不依赖于Vue的其他部分
  3. 更精确的依赖收集

    • 使用WeakMap + Map的数据结构存储依赖关系
    • 对于数组和集合类型有特殊优化
  4. 更好的类型支持

    • 使用TypeScript编写,提供了完善的类型定义
    • 支持泛型,提高了代码的类型安全性

十、Vue 3的响应式系统与Vue 2的对比

Vue 3 响应式系统相比Vue 2有很多改进:

  1. 性能更好

    • Proxy比Object.defineProperty更高效
    • 更精确的依赖收集减少了不必要的更新
  2. 功能更强

    • 可以监听对象属性的添加和删除
    • 可以正确监听数组索引和长度的变化
  3. API更丰富

    • 提供了reactiverefcomputedeffect等更丰富的API
    • 支持readonlyshallowReactive等更多选项
  4. 更好的类型支持

    • 使用TypeScript编写,提供了完善的类型定义
    • 支持泛型,提高了代码的类型安全性

总结

源码复杂,以此为记,持续学习