25-10-22-学习日志-VUE的响应式原理


目标:理解 VUE2 与 VUE3 的响应式原理,深入 VUE3 的响应式源码。

第一部分:阅读相关文章,理解原理

回填:这篇文章讲把 Vue3 响应式原理讲得挺细致的,也是真有源码解读部分。如果能读懂的话,”Vue3 部分”可以只看这篇:浅谈 Vue3 响应式原理与源码解读

深入解析 Vue2 和 Vue3 响应式原理这篇文章讲解了 VUE2 与 VUE3 的响应式基本实现。文中提到的“VUE3 使用到在 ES6 一并新增的 Reflect,与 Proxy 一同实现响应式”,我对其中 Reflect 的概念有所疑惑。Reflect 基本复刻了 Object 的方法,在复刻方法的前提下添加了“Object 对象的一些明显属于语言内部的方法(比如 Object.defineProperty)”、修改了部分方法的返回值、让 Object 的操作都变成函数行为。具体可以看下面这篇文章,讲述了 Reflect 的产生背景与定义、用法:ECMAScript 6 入门-Reflect

Vue3 响应式原理揭秘:从源码层面理解 Composition API 的底层机制这篇文章的“第二章和第三章”质量蛮高的,主题是 VUE3 组合式 API 下的”响应式系统&性能优化”。里面提到了关键概念——“副作用 effect“。

副作用函数可以这样定义:如果一个函数引用了外部的数据,这个函数会受到外部数据改变的影响,我们就说这个函数存在副作用,也就是我们所说的副作用函数。(引用自浅谈 Vue3 响应式原理与源码解读

阅读完上面三篇文章,我大致理解了“VUE3 响应式原理”是如何实现的。就是通过 Proxy 与 Reflect 的共同作用下代理对象,并维护一个“依赖收集-触发更新”的列表。这个列表是通过全局 WeakMap 的多层映射维护的(第一层:全局 WeakMap,由”原始对象”映射到对象的 “属性 - 副作用” 映射(Map);第二层:Map, 键是对象的属性名,值是依赖该属性的所有副作用函数(Set,避免重复))。

上面这段表述,ChatGPT 给了 9.5/10 分,这里也展示下当前版本 ChatGPT 修改后的文本,填充了“‘依赖收集’是在 effect 触发时收集”这个细节:
Vue3 通过 ProxyReflect 拦截对响应式对象的访问与修改,在副作用函数(effect)执行时进行依赖收集,并在数据变更时触发相应的副作用函数,从而实现响应式更新。
依赖关系存储在一个全局的 WeakMap(即 targetMap)中,层级结构如下:

  • 第一层:WeakMap,键为原始对象(target),值为该对象的 depsMap
  • 第二层:Map,键为属性名(key),值为依赖该属性的所有副作用函数集合(Set<ReactiveEffect>),以防重复收集。

源码链接:
Vue2:https://github.com/vuejs/vue/tree/v2.7.14/src/core/observer中的 observer.js(数据劫持核心,Observer 类实现)、dep.js(依赖管理器 Dep 类)、watcher.js(观察者 Watcher 类)、array.js(数组响应式处理)
Vue3:https://github.com/vuejs/core/tree/v3.3.4/packages/reactivity/src中的 reactive.ts(reactive 函数与 Proxy 代理实现)、effect.ts(副作用 effect 与依赖追踪核心)、baseHandlers.ts(Proxy 拦截器的具体实现)、collectionHandlers.ts(集合类型 Map/Set 的响应式处理)

个人通过豆包 AI 了解 Vue2 的源码实现(似懂非懂,勉强了解了大致概念与事件的链路)。 Vue2 就是通过对对象的每个属性设置 Dep.notify()与其中的 Watcher 列表,让 watcher 触发更新逻辑。

第二部分:针对性提问

Vue3 的 trigger 与 Vue2 的 trigger 核心差异解析

Q:VUE3 的 trigger 和 VUE2 的 trigger 有什么不同吗?就是一个通过副作用直接触发,一个通过 observer 监听变化进而触发?

要理解 Vue3 的 trigger 与 Vue2 的 trigger(实际对应 Vue2 中 Dep.notify() 主导的更新触发逻辑)的不同,需从“触发前提、依赖关联方式、执行逻辑、功能覆盖范围”四个维度拆解——核心差异并非简单的“副作用直接触发”与“Observer 监听触发”,而是由两者响应式底层机制(Proxy 代理 vs Object.defineProperty 劫持)差异衍生的完整链路区别。

一、先明确:Vue2 中没有“trigger 函数”,但有等效的“更新触发逻辑”

首先需澄清一个概念:Vue2 的源码中没有名为 trigger 的独立函数,其“数据变化触发更新”的核心逻辑由 Dep 类的 notify() 方法实现(可理解为 Vue2 版的“trigger”);而 Vue3 中 trigger 是独立的核心函数,统一负责“数据变化后触发依赖更新”。

两者的本质目标一致——当数据变化时,找到依赖该数据的“更新逻辑”并执行,但因底层响应式机制不同,实现路径和能力存在显著差异。

二、核心差异:从“依赖关联”到“执行逻辑”的全链路对比

结合你提到的“副作用”“Observer 监听”,以及文档中 Vue3 的 Proxy、Vue2 的 Object.defineProperty 逻辑,两者的差异可通过下表清晰呈现:

对比维度 Vue2(Dep.notify() 主导) Vue3(trigger 函数)
1. 触发前提:数据变化的监听方式 基于 Object.defineProperty单个属性setter 劫持,仅能监听“属性值修改”(无法监听新增/删除属性、数组索引修改)。 基于 Proxy整个对象的拦截,可监听“属性修改、新增、删除”“数组索引修改、原型方法调用”等几乎所有对象操作。
2. 依赖关联:“数据-更新逻辑”的绑定方式 依赖 Dep(每个属性对应一个 Dep)和 Watcher(封装更新逻辑):
- 每个响应式属性绑定一个 Dep,存储依赖该属性的 Watcher
- Watcher 初始化时执行更新逻辑(如 render),触发属性 getter,通过 Dep.depend() 完成“属性-Dep-Watcher”的绑定。
依赖 targetMap(全局依赖映射)和 effect(副作用函数):
- targetMapWeakMap<原对象, Map<属性名, Set<effect>>>,直接绑定“对象-属性-effect”;
- effect 执行时访问 Proxy 对象,触发 get 拦截,通过 track() 函数将 effect 加入 targetMap 对应位置。
3. 执行逻辑:触发更新时的核心流程 1. 属性值修改触发 setter
2. 调用属性对应的 Dep.notify()
3. 遍历 Dep 中的 Watcher,调用每个 Watcherupdate() 方法;
4. Watcher 最终执行更新逻辑(如重新 render、计算 computed)。
1. 对象操作触发 Proxy 拦截(如 set/deleteProperty);
2. 调用 trigger() 函数,从 targetMap 中找到该“对象-属性”对应的所有 effect
3. 遍历 effect 集合:
- 若 effectscheduler(调度器,如 watchflush: 'post'),则执行 scheduler
- 无 scheduler 则直接执行 effect 函数(如组件更新逻辑)。
4. 功能覆盖:支持的更新场景 局限性强:
- 无法触发“对象新增/删除属性”的更新(需手动调用 this.$set/this.$delete);
- 无法触发“数组索引修改”的更新(需调用重写的 push/splice 等方法);
- 数组的 Dep 绑定在数组对象本身,调用重写方法时触发数组 Dep.notify(),无法精准定位“修改的元素对应的依赖”。
场景全覆盖:
- 天然支持“对象新增/删除属性”“数组索引修改”的更新,无需手动调用额外 API;
- 数组操作(如 arr[0] = 1delete arr[0])直接触发 Proxy 拦截,trigger() 可精准找到依赖该索引的 effect,避免不必要的更新。
5. 性能优化:避免重复/冗余更新 依赖 Watcherid 去重(Depsubs 数组通过 id 避免重复添加 Watcher),但数组更新时会触发整个数组的 Dep.notify(),可能导致冗余更新。 1. targetMap 中用 Set 存储 effect,天然去重;
2. Proxy 拦截精准定位修改的属性/索引,仅触发依赖该位置的 effect,减少冗余更新;
3. 支持 scheduler 自定义执行时机(如批量更新、延迟更新),进一步优化性能。

三、关键补充:你疑问中的“副作用直接触发”与“Observer 监听触发”的准确解读

你提到的“一个通过副作用直接触发,一个通过 Observer 监听触发”,需要修正并细化:

1. Vue2:并非“Observer 监听触发”,而是“属性 setter 触发 Dep 通知 Watcher”

Vue2 中的 Observer 类仅负责“初始化数据劫持”——递归遍历对象,为每个属性添加 Object.definePropertygetter/setter,它本身不直接触发更新,只是“准备监听条件”。
真正的更新触发逻辑是:属性修改 → 触发 setter → 调用 Dep.notify() → 通知 Watcher 执行更新,核心是“属性-Dep-Watcher”的联动。

2. Vue3:并非“副作用直接触发”,而是“Proxy 拦截触发 trigger 执行 effect”

Vue3 中的 effect 是“副作用函数”的封装(如组件 render、watch 回调),但 effect 不会“直接触发”——必须通过 Proxy 拦截数据变化后,由 trigger 函数从 targetMap 中找到对应的 effect 并执行。
核心逻辑是:**对象操作 → 触发 Proxy 拦截 → 调用 trigger() → 执行对应 effect**,effect 是“更新逻辑的载体”,而非“触发者”。

四、总结:差异的本质是“响应式底层机制的升级”

Vue2 的“更新触发”(Dep.notify())是“属性级劫持”的必然结果——因 Object.defineProperty 只能针对单个属性,不得不通过 Dep 作为中间层关联 Watcher,导致功能局限和链路冗余;
Vue3 的 trigger 是“对象级代理”的优化产物——Proxy 能拦截整个对象的操作,配合 targetMap 直接绑定“对象-属性-effect”,不仅解决了 Vue2 的所有局限性,还通过 scheduler 等设计提升了灵活性和性能。

简单来说:Vue2 是“属性变了,找依赖该属性的 Watcher 更”;Vue3 是“对象变了,找依赖该对象属性的 effect 更”——后者的链路更短、覆盖场景更广、性能更优。

Vue3 的 effect 与 Vue2 的 Watcher 之间的差别

Q:那 Vue2 就是通过对对象的每个属性设置 Dep.notify()与其中的 Watcher 列表,让 watcher 触发更新逻辑? watcher 和 effect 的效果是一样的吗?

你的理解基本正确。Vue2 中确实通过为每个属性关联 Dep(依赖管理器),并在 Dep 中维护 Watcher 列表,当属性变化时通过 Dep.notify() 触发 Watcher 执行更新逻辑;而 Vue3 的 effect 与 Vue2 的 Watcher 核心作用相似——都是“封装需要响应数据变化的逻辑”,但两者在设计和能力上存在显著差异。

一、Vue2 的更新触发逻辑:DepWatcher 的协作

Vue2 中,每个响应式属性会对应一个 Dep 实例,Dep 内部通过 subs 数组存储所有依赖该属性的 Watcher。当属性变化时:

  1. 触发 setter 拦截,调用 Dep.notify()
  2. Dep.notify() 遍历 subs 数组,调用每个 Watcherupdate() 方法;
  3. Watcher 最终执行自身封装的更新逻辑(如视图渲染、计算属性重新计算、watch 回调等)。

核心链路属性变化 → setter → Dep.notify() → Watcher.update() → 执行更新逻辑

二、Watchereffect 的对比:作用相似,设计不同

两者的核心目标一致——将“依赖数据”与“更新逻辑”绑定,数据变化时自动执行逻辑,但实现方式和灵活性有明显区别:

维度 Vue2 的 Watcher Vue3 的 effect
形态 类实例(new Watcher(...) 函数(effect(fn) 包装的副作用函数)
依赖关联方式 双向关联:Watcher 记录依赖的 DepDep 记录依赖的 Watcher(通过 id 去重)。 临时关联:通过全局 activeEffect 变量,在 effect 执行时临时标记当前副作用,收集依赖后自动解除。
更新触发方式 Dep.notify() 调用 Watcher.update()Watcher 内部通过队列(nextTick)优化执行时机。 trigger 函数直接调用 effect 或其 scheduler(调度器),支持自定义执行时机(如 flush: 'pre'/'post'/'sync')。
适用场景 需手动实例化,按场景分为渲染 Watcher、计算属性 Watcherwatch Watcher 等,场景绑定较固定。 函数式封装,无需区分场景,可直接包裹任意逻辑(组件渲染、计算、自定义回调等),更灵活。
与数据的绑定粒度 依赖“属性级 Dep”,一个 Watcher 可能依赖多个属性的 Dep 依赖“对象-属性”的全局映射(targetMap),一个 effect 直接关联它访问过的所有“对象-属性”。
停止响应式 需手动调用 Watcher.teardown() 清理 DepWatcher 的双向关联,逻辑复杂。 调用 effect 返回的停止函数即可,内部自动清理 effecttargetMap 中的关联,逻辑简洁。

三、关键结论

  1. 核心效果一致Watchereffect 都是“响应式更新的载体”,负责在数据变化时执行关联的逻辑(如视图更新、回调函数等)。
  2. 设计理念不同
    • Watcher 是 Vue2 中“面向对象”的设计,通过类实例管理依赖和更新,逻辑较重但职责明确;
    • effect 是 Vue3 中“函数式”的设计,通过纯函数封装副作用,配合全局变量和弱引用映射简化依赖管理,更灵活轻量。
  3. 能力边界差异effect 支持自定义调度器、自动清理依赖等特性,解决了 Watcher 在复杂场景下的局限性(如停止响应式的繁琐、更新时机定制困难等)。

简单来说,effect 可以看作是 Watcher 的“进化版”——保留了核心功能,同时通过设计优化提升了灵活性和易用性。

Vue3 的 Ref 是如何实现的?

Q:先前你说的都是 reactive 的创建,能否结合 ref 定义源码谈谈 ref 的? https://github.com/vuejs/core/blob/v3.3.4/packages/reactivity/src/ref.ts

结合提供的 ref.ts 源码,我们可以从定义、实现原理和核心特性三个方面解析 Vue 的 ref 实现:

一、ref 的核心定义与类型

ref 是 Vue 中用于将基本类型值(或对象)包装为响应式对象的方法,其核心是通过 Ref 接口定义的对象,包含一个 .value 属性用于访问和修改内部值。

源码中 Ref 接口定义如下:

export interface Ref<T = any, S = T> {
get value(): T; // 读取内部值
set value(_: S); // 设置内部值
[RefSymbol]: true; // 用于类型区分的标记(内部使用)
}
  • [RefSymbol] 是一个私有 Symbol,用于在 isRef 方法中判断是否为 ref 对象,避免与普通对象混淆。
  • 泛型 T 表示 .value 的返回类型,S 表示设置 .value 时允许的类型。

二、ref 的创建流程(refshallowRef

refshallowRef 均通过 createRef 函数创建,核心区别在于是否对值进行深度响应式转换

1. 入口函数
// 普通 ref:对值进行深度响应式处理
export function ref(value?: unknown) {
return createRef(value, false);
}

// 浅层 ref:仅监听 .value 的替换,不处理内部属性
export function shallowRef(value?: unknown) {
return createRef(value, true);
}
2. 核心创建逻辑(createRef
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue; // 如果已经是 ref,直接返回
}
return new RefImpl(rawValue, shallow); // 否则创建 RefImpl 实例
}
  • 若传入的 rawValue 已经是 ref 对象,则直接返回(避免重复包装)。
  • 否则通过 RefImpl 类实例化一个新的 ref 对象。

三、RefImplref 的底层实现类

RefImplref 的实际实现类,负责管理响应式依赖追踪和触发更新。

1. 构造函数(初始化值)
class RefImpl<T = any> {
_value: T; // 处理后的值(可能是响应式对象)
private _rawValue: T; // 原始值(未处理的原始数据)
dep: Dep = new Dep(); // 依赖管理容器
public readonly [ReactiveFlags.IS_REF] = true; // 标记为 ref
public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false; // 是否为浅层 ref

constructor(value: T, isShallow: boolean) {
this._rawValue = isShallow ? value : toRaw(value); // 原始值存储(非浅层时取原始对象)
this._value = isShallow ? value : toReactive(value); // 处理后的值(非浅层时转为响应式)
this[ReactiveFlags.IS_SHALLOW] = isShallow;
}
}
  • **_rawValue**:存储原始值(未经过响应式转换),用于后续对比值是否变化。
  • **_value**:存储处理后的值:
    • 若为 shallow: false(普通 ref),则通过 toReactive(value) 将对象转为深度响应式(等价于 reactive)。
    • 若为 shallow: trueshallowRef),则直接存储原始值,不做响应式转换。
2. .value 的 getter:依赖追踪

当读取 ref.value 时,会触发依赖追踪,收集当前活跃的副作用(effect):

get value() {
if (__DEV__) {
this.dep.track({ // 开发环境:带详细信息的追踪
target: this,
type: TrackOpTypes.GET,
key: 'value'
})
} else {
this.dep.track() // 生产环境:简化追踪
}
return this._value // 返回处理后的值
}
  • 通过 dep.track() 将当前 effect 收集到 dep 容器中,后续值变化时会触发这些 effect
3. .value 的 setter:触发更新

当修改 ref.value 时,会检查值是否变化,若变化则更新值并触发依赖更新:

set value(newValue) {
const oldValue = this._rawValue
// 决定是否直接使用新值(浅层 ref 或新值是浅层/只读对象时)
const useDirectValue = this[ReactiveFlags.IS_SHALLOW] || isShallow(newValue) || isReadonly(newValue)
newValue = useDirectValue ? newValue : toRaw(newValue) // 非浅层时取新值的原始对象

if (hasChanged(newValue, oldValue)) { // 检查值是否真的变化(使用 Vue 内部的 hasChanged 比较)
this._rawValue = newValue // 更新原始值
this._value = useDirectValue ? newValue : toReactive(newValue) // 更新处理后的值
// 触发依赖更新
if (__DEV__) {
this.dep.trigger({
target: this,
type: TriggerOpTypes.SET,
key: 'value',
newValue,
oldValue
})
} else {
this.dep.trigger()
}
}
}
  • 值变化检查:通过 hasChanged 比较新旧值(支持对象、数组等引用类型的深层比较)。
  • 触发更新:若值变化,调用 dep.trigger() 通知所有收集的 effect 重新执行。

四、ref 的关键辅助函数

  1. **isRef**:判断一个值是否为 ref 对象

    export function isRef(r: any): r is Ref {
    return r ? r[ReactiveFlags.IS_REF] === true : false;
    }

    通过检查对象是否有 ReactiveFlags.IS_REF 标记(true)来判断。

  2. **unref**:获取 ref 的原始值(若不是 ref 则直接返回自身)

    export function unref<T>(ref: MaybeRef<T> | ComputedRef<T>): T {
    return isRef(ref) ? ref.value : ref;
    }

    常用于简化对 ref 值的访问(如 unref(countRef) 等价于 countRef.value)。

  3. **triggerRef**:强制触发 shallowRef 的更新

    export function triggerRef(ref: Ref): void {
    if ((ref as unknown as RefImpl).dep) {
    (ref as unknown as RefImpl).dep.trigger(...)
    }
    }

    由于 shallowRef 不追踪内部属性变化,当手动修改其内部属性后,可通过此方法强制触发依赖更新。

  4. **customRef**:自定义 ref 逻辑

    export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
    return new CustomRefImpl(factory) as any;
    }

    允许开发者自定义 getset 逻辑,例如实现防抖、节流等功能(需手动调用 tracktrigger 管理依赖)。

五、refreactive 的核心区别

  1. 包装类型ref 可包装基本类型(如 numberstring)和对象,而 reactive 仅能包装对象。
  2. 访问方式ref 通过 .value 访问/修改值,reactive 直接访问属性。
  3. 响应式深度
    • 普通 ref 对对象进行深度响应式转换(等价于 reactive)。
    • shallowRef 仅追踪 .value 的替换,不处理内部属性。
  4. 适用场景
    • 基本类型值的响应式处理(必用 ref)。
    • 需要明确控制响应式深度时(shallowRef)。
    • 自定义响应式逻辑时(customRef)。

通过以上源码分析可见,ref 的核心是通过 RefImpl 类封装值,并利用 getter/setter 实现依赖追踪和更新触发,同时提供灵活的辅助函数满足不同场景需求。


文章作者: Qijia Huang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Qijia Huang !
  目录