Vue3响应式原理:Effect与Computed的协作机制解析

作者:南栀时间:2026-10-23 11:53:56浏览:656

本文深入解析Vue3响应式系统中Effect与Computed的内在联系。通过测试用例揭示Effect作为副作用收集器的核心作用,阐述track与trigger如何协同工作。文章逐步推导Computed基于Lazy Effect的实现逻辑,并解析targetMap、deps等关键数据结构,帮助开发者理解Vue3响应式底层原理。

Effect在响应式系统中的角色

在深入研读Vue3响应式机制后,一个核心疑问始终萦绕:在getter与setter之间穿梭的effect究竟承担何种角色?若仅实现基础响应式,似乎无需引入effect。那么,effect存在的根本意义何在?

通过偶然发现的effect测试代码用例,这一谜团得以解开。


it('should observe basic properties', () => {
 let dummy
 const counter = reactive({ num: 0 })
 effect(() => (dummy = counter.num))

 expect(dummy).toBe(0)
 counter.num = 7
 expect(dummy).toBe(7)
})

以下是对该段测试代码的详细解析。

  • 首先声明一个普通变量dummy,在effect回调函数中,将响应式对象counter的num属性值赋给dummy。
  • 接着进行断言判断,验证dummy的初始值是否等于0。
  • 当将counter.num重新赋值为7时,dummy的值也随之更新为7。

这一现象引发了我的深入思考。

这是否意味着computed的实现原理与此相关?

随即查阅computed的测试用例以验证猜想。


const value = reactive<{ foo?: number }>({})
const cValue = computed(() => value.foo)
expect(cValue.value).toBe(undefined)
value.foo = 1
expect(cValue.value).toBe(1)

观察结果令人兴奋。

进一步确认了之前的推测。

逻辑链条完全闭合。

这种豁然开朗的感觉令人振奋。

事实证明猜想完全正确。至此,effect的本质得以厘清:顾名思义,effect意为“副作用”,它是响应式系统的副产品。每次触发getter时收集对应的effect,每次触发setter时则执行这些effect。利用这一机制,可以在响应式数据变化时执行额外操作,例如实现计算属性computed。

基于Effect实现Computed

为了更清晰地理解,我们可以尝试用effect来手动实现一个computed。

此处省略复杂的边界判断逻辑,以便更直观地展示核心实现。


function computed (fn) {
 let value = undefined
 const runner = effect(fn, {
  // 如果lazy不置为true的话,每次创建effect的时候都会立即执行一次
  // 而我们要实现computed显然是不需要的
  lazy: true
 })
 // 为什么要使用对象的形式,是因为我们最后需要得到computed的值
 // 如果不用对象的 get 方法的话我们就需要手动再调用一次 computed() 
 return {
  get value() {
   return runner()
  }
 }
}

// 使用起来是这样的

const value = reactive({})
const cValue = computed(() => value.foo)
value.foo = 1

console.log(cValue.value) // 1

实现看似简单,但effect的具体运行机制究竟如何?

Effect内部逻辑与数据结构

不急,我们先梳理整体逻辑脉络。

  1. 首先,若effect回调中访问了已响应对象的getter,该effect必须被存储起来。
  2. 其次,需要一个存储effect的容器。在调用effect函数时,需将当前effect放入该容器。Vue内部使用数组activeReactiveEffectStack = []来维护这一栈结构。
  3. 再次,每个target触发时可能关联多个effect,因此每个target需对应一个依赖收集器deps。在setter触发时,遍历deps并执行对应的effect()。
  4. 然而,deps不能直接挂载在target对象上,否则会导致数据冗余且不简洁。因此,Vue采用Map集合来存储target与deps的映射关系,该集合在源码中称为targetMap。

接下来明确几个核心概念。

track(追踪器):在getter调用时执行,负责建立target、key与effect之间的依赖关系。


// 比如 const react = reactive({a: { b: 2 })
// react.a 时 target -> {a: { b: 2 } key -> a 
// targetMap 储存了 target --> Map --> key --> Set --> dep --> effect 
// 当调用 react.a.b.c.d.e 时 depsMap
// {"a" => Set(1)} --> Set --> effect
// {"b" => Set(1)}
// {"c" => Set(1)}
// {"d" => Set(1)}
// {"e" => Set(1)}
export function track(target: any, key: string) {
 const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1];
 if (effect) {
  let depsMap = targetMap.get(target);
  if (depsMap === void 0) {
   targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key!);
  if (!dep) {
   depsMap.set(key!, (dep = new Set()));
  }
  if (!dep.has(effect)) {
   dep.add(effect);
   effect.deps.push(dep);
  }
 }
}

trigger(触发器):逻辑相对直观,通过target和key获取对应的所有effect,并遍历执行它们。


export function trigger(target: any, key?: string | symbol) {
 const depsMap: any = targetMap.get(target);
 const effects: any = new Set()
 if (depsMap && depsMap.get(key)) {
  depsMap.get(key).forEach((dep: any) => {
   effects.add(dep)
  });
  effects.forEach((e: any) => e())
 }
}

以下是effect函数的具体实现代码。


// 暴露的 effect 函数
export function effect(
 fn: Function,
 options: any = EMPTY_OBJ
): any {
 if ((fn as any).isEffect) {
  fn = (fn as any).raw
 }
 const effect = createReactiveEffect(fn, options)
 // 如果不是 lazy,则会立即执行一次
 if (!options.lazy) {
  effect()
 }
 return effect
}

// 创建 effect
function createReactiveEffect(
 fn: Function,
 options: any
): any {
 const effect = function effect(...args: any): any {
  return run(effect as any, fn, args)
 } as any
 effect.isEffect = true
 effect.active = true
 effect.raw = fn
 effect.scheduler = options.scheduler
 effect.onTrack = options.onTrack
 effect.onTrigger = options.onTrigger
 effect.onStop = options.onStop
 effect.computed = options.computed
 effect.deps = []
 return effect
}

// 执行函数,执行完之后会将储存的 effect 删除
// 这是函数 effect 的所有执行,所经历的完整的声明周期
function run(effect: any, fn: Function, args: any[]): any {
 if (!effect.active) {
  return fn(...args)
 }
 if (activeReactiveEffectStack.indexOf(effect) === -1) {
  try {
   activeReactiveEffectStack.push(effect)
   return fn(...args)
  } finally {
   activeReactiveEffectStack.pop()
  }
 }
}

回顾全文,总结学习源码的方法:若对源码某部分感到无从下手,建议从测试用例入手。测试用例能清晰展示函数预期效果,进而推导设计意图及实现思路,结合源码与个人思考,能更高效地掌握知识。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

作者最新文章