乐于分享
好东西不私藏

Vue 3 源码解读 -- 4. 响应式系统(下)

Vue 3 源码解读 -- 4. 响应式系统(下)

04

PART

响应式系统(下)

REACTIVITY · 面向开发者的响应式 API

本章概览

上篇讲了响应式内核,本篇聚焦开发者天天在用的 APIreactiverefcomputedwatch,以及 toRef/toRefs/proxyRefs 的解构技巧。

理解这些 API 的内部实现,能帮你避开「为什么我改了值视图不更新」「为什么 reactive 解构后失效」这类典型坑。

4.1 reactive — 深层响应式代理

reactive 是 Vue 3 最核心的响应式 API,它通过 Proxy 拦截对象的所有操作:

...javascript

// 第 1756 行

function reactive(target) {

 if (isReadonly(target)) return target;

 return createReactiveObject(

  target, false,

  mutableHandlers, mutableCollectionHandlers,

  reactiveMap

 );

}

createReactiveObject — 第 1798 行

整个流程分五步,缓存类型分发是性能关键:

1

非对象直接返回(reactive(1) 无效,应使用 ref)

2

检查 __v_raw(已是代理)和 __v_skip(标记跳过)

3

从 proxyMap 缓存查找,避免重复代理同一个对象

4

判断类型:普通对象用 baseHandlers,Map/Set/WeakMap/WeakSet 用 collectionHandlers

5

创建 new Proxy(target, handlers) 并缓存

Proxy Handler 体系

Vue 3 使用继承链组织 Proxy Handler,最终落地为四个具体 handler:

mutableHandlers

BaseReactiveHandler → MutableReactiveHandler(shallow=false),普通对象可写代理

shallowReactiveHandlers

(shallow=true)只代理一层,不深层响应,性能更好

readonlyHandlers

ReadonlyReactiveHandler(shallow=false),只读代理,set 时告警

shallowReadonlyHandlers

(shallow=true)只读 + 浅层,适合 props 透传

get trap — 第 1365 行

读取属性时依次处理内部标记(__v_isReactive 等)、数组方法拦截(arrayInstrumentations)、收集依赖(track)、shallow 短路返回、Ref 自动解包、对象自动深层代理。这是「访问即追踪」的核心。

...javascript

get(target, key, receiver) {

 const isReadOnly2 = this._isReadonly;

 const isShallow2 = this._isShallow;

 // 1. 处理内部标记(__v_isReactive 等)

 if (key === "__v_isReactive") return !isReadOnly2;

 if (key === "__v_isReadonly") return isReadOnly2;

 if (key === "__v_raw") return target;

 // 2. 数组方法查找 arrayInstrumentations

 const targetIsArray = isArray(target);

 if (!isReadOnly2 && targetIsArray && hasOwn(arrayInstrumentations, key)) {

  return Reflect.get(arrayInstrumentations, key, receiver);

 }

 // 3. 获取值

 const res = Reflect.get(target, key, receiver);

 // 4. 收集依赖

 if (!isReadOnly2) track(target, "get", key);

 // 5. shallow 模式直接返回

 if (isShallow2) return res;

 // 6. Ref 自动解包

 if (isRef(res)) return isArray(target) && isIntegerKey(key) ? res : res.value;

 // 7. 对象自动深层代理

 if (isObject(res)) return isReadOnly2 ? readonly(res) : reactive(res);

 return res;

}

set trap(第 1423 行)负责派发更新,它需要区分“新增 key”和“修改已有 key”,分别触发 "add" 和 "set" 类型的通知。

4.2 ref — 基本值响应式

reactive 只能代理对象,而 ref 可以包装任意类型的值:

...javascript

// 第 1881 行

class RefImpl {

 constructor(value, shallow) {

  this.dep = new Dep(); // 独立的 Dep 实例

  this.__v_isRef = true;

  this._rawValue = shallow ? value : toRaw(value);

  this._value = shallow ? value : toReactive(value);

  this.__v_isShallow = shallow;

 }

 get value() {

  this.dep.track(); // 收集依赖

  return this._value;

 }

 set value(newValue) {

  const oldValue = this._rawValue;

  if (hasChanged(newValue, oldValue)) {

   this._rawValue = toRaw(newValue);

   this._value = toReactive(newValue);

   this.dep.trigger(); // 派发更新

  }

 }

}

reactive vs ref

  • reactive 适合对象/数组
  • ref 适合基本类型(string/number/boolean)
  • reactive 访问属性无需 。value
  • ref 需要 。value(模板中自动解包)
  • reactive 不能替换整个对象
  • ref。value = newObj 可以替换

toRaw 与 toReactive

  • toRaw(value):剥除 Proxy 获取原始对象,用于存储 _rawValue
  • toReactive(value):如果是对象则调用 reactive() 深层代理,否则直接返回
  • 这保证了 ref({a: 1}) 的 。value 是响应式的

4.3 computed — 计算属性

computed 是 Vue 中最巧妙的 API。它既是消费者(依赖上游响应式数据),又是生产者(被下游 effect 依赖):

...javascript

// 第 2047 行

class ComputedRefImpl {

 constructor(getter, setter, isSSR) {

  this.fn = getter;

  this.setter = setter;

  this.dep = new Dep(this); // 注意:传入 this

  this.flags = 16; // DIRTY

  this.globalVersion = globalVersion - 1;

 }

 get value() {

  this.dep.track(); // 被下游依赖收集

  refreshComputed(this); // 检查是否需要重算

  if (this.deps) {

   // 更新所有 link 的 version

   let link = this.deps;

   while (link) {

    link.version = link.dep.version;

    link = link.nextDep;

   }

  }

  return this._value;

 }

}

computed 的懒求值特性

computed 不会在依赖变化时立即重新计算,而是在被访问时才检查是否需要重算(通过 version 对比)。如果没有任何地方读取这个 computed,即使它依赖的数据变了也不会重新计算。这就是“懒求值”——只在需要时才做工作。

4.4 watch 与 watchEffect

watch 和 watchEffect 本质上都是创建一个 ReactiveEffect,但通过 scheduler 控制执行时机

API
回调查找变化
懒执行
用法
watchEffect
否(自动收集依赖)
否(立即执行)
副作用逻辑,不关心旧值
watch(source, cb)
是(对比新旧值)
是(默认不立即执行)
需要旧值做逻辑判断
watchPostEffect
DOM 更新后执行
watchSyncEffect
同步执行(不进队列)

watch 的 source 支持多种形式:

...javascript

// Ref

watch(countRef, (newVal, oldVal) => { ... })

// Getter 函数

watch(() => state.count, (newVal) => { ... })

// Reactive 对象(自动深层)

watch(reactiveState, (newState) => { ... })

// 多源数组

watch([ref1, ref2, () => state.prop], ([v1, v2, v3]) => { ... })

4.5 toRef / toRefs / proxyRefs

这三个工具函数用于响应式对象的“解构”场景:

...javascript

const state = reactive({ name: 'Vue', version: 3 });

// toRef:为单个属性创建 ref

const nameRef = toRef(state, 'name');

// toRefs:为所有属性创建 ref 对象

const { name, version } = toRefs(state);

// proxyRefs:自动解包 ref 的代理

// (setupState 使用此机制,模板中无需 .value)

proxyRefs(第 1949 行)创建一个 Proxy,在 get 时自动解包 ref(isRef ? ref.value : value),在 set 时自动设置 ref 的 .value。这就是为什么在 setup() 中返回 { count }(ref),模板中可以直接用 {{ count }} 而不需要 {{ count.value }}

我是安南,专注于前端框架源码拆解与分享

需要完整相关资料的可以打个:‘学习666‘,找我拿

如果你觉得今天这篇有收获,欢迎点赞、在看、转发三连,我们下篇见