vue3.x 响应式系统:核心 API

0 阅读22分钟

Vue 3 的响应式系统基于 Proxy 提供了四种不同深度的 API,用于控制数据的响应式行为和读写权限。

reactive 深层响应式

在 Vue 3 中,reactive 是一个用于创建深度响应式对象的 API。它接收一个普通对象(或数组、MapSet 等),并返回该对象的 Proxy 代理。通过这个代理,可以像操作普通对象一样修改数据,所有变更都会被 Vue 自动追踪,并在数据变化时触发视图更新。

🍒 响应式对象

  1. 直接修改对象属性
  2. 新值属性
  3. 删除某个属性
  4. 批量更新。使用 Object.assign 批量更新多个属性(保持同一响应式对象)
import { reactive, watch, watchEffect } from "vue";
const obj = reactive({
  count: 0,
  age: 18,
  name: "张云",
});

const handleClick = () => {
  // 1、直接修改单个属性
  obj.count++;
  // 2、新增属性
  obj.extra = "extra";
  // 3、删除属性
  delete obj.age;
  // 4、批量修改属性
  // 使用 Object.assign 批量更新多个属性(保持同一响应式对象)
  Object.assign(obj, {
    count: 100,
    name: "李四",
    date: "2026-01-01",
  });
};
</script>

【示例】直接监听 reactive 对象时,默认深度监听,回调中新旧值相同(指向同一对象引用)。

watch(obj, (newVal, oldVal) => {
  console.log("watch obj 值", newVal, oldVal);
});

image.png

【示例】为什么点击修改 reactive 对象时 watch 监听没有执行回调?

watch(
  () => obj,
  (newVal, oldVal) => {
    console.log("watch obj 值", newVal, oldVal);
  }
);

source 是一个函数,默认是浅层监听。

  • reactive 对象是响应式的,但其引用是稳定的
  • 浅层监听的行为:只监听对象的引用变化,而不监听对象内部属性的变化
  • 因此,修改 obj.count 时,obj 的引用没有改变,所以浅层监听不会触发回调

【示例】添加 deep 配置,会执行回调。

回调中新旧值相同(指向同一对象引用)。

watch(
  () => obj,
  (newVal, oldVal) => {
    console.log("watch obj 值", newVal, oldVal);
  },
  {
    deep: true,
  }
);

【示例】 监听 reactive

const reactiveObj = reactive({
  count: 0, // 响应式
  nested: {
    value: 1,
  },
  arr: [1, 2, 3],
});

const handleClick = () => {
  // 会触发 watch 监听,因为 count 是响应式的
  reactiveObj.count++;
};

const handleClick2 = () => {
  // 会触发 watch 监听,数组是响应式的,但引用没有改变
  // reactiveObj.arr[4] = 4;

  // 会触发,数组的引用没有发生改变
  // reactiveObj.arr.push(4);

  // 会触发,数组的引用发生了改变
  reactiveObj.arr = [4, 5, 6];
};

watch(
  () => reactiveObj.count,
  (newVal, oldVal) => {
    console.log("watch reactiveObj.count 值", newVal, oldVal);
  }
);
watch(reactiveObj, (newVal, oldVal) => {
  // 新旧值 是同一个引用
  console.log("watch reactiveObj 值", newVal, oldVal);
});

watch(reactiveObj.arr, (newVal, oldVal) => {
  // 新旧值 是同一个引用
  console.log("1、watch reactiveObj.arr 值", newVal, oldVal);
});

// 通过 getter 函数返回数组引用,Vue 默认只比较引用是否改变
watch(
  () => reactiveObj.arr,
  (newVal, oldVal) => {
    // 新旧值 是不同的引用
    console.log("2、getter watch reactiveObj.arr 值", newVal, oldVal);
  }
);

image.png

🍒 响应式数组(重写了27个方法)

在 Vue 3 中,reactive 创建的响应式数组,直接使用原生数组方法(pushpopsplice 等)或 通过索引 直接修改元素或修改 length 属性都会 自动触发视图更新

示例 数组操作

const reactiveArr = reactive(["item1", "item2", "item3"]);

const handleClick = () => {
  // 批量更新,update 一次
  // reactiveArr.push("item4");
  // reactiveArr.push("item5");

  // 更新
  reactiveArr[0] = "索引修改item0";

  // 更新
  reactiveArr.length = 0;
};

// reactive默认深度监听
watch(reactiveArr, (newVal, oldVal) => {
  // 新旧值是同一个引用
  console.log("watch reactiveArr 值", newVal, oldVal);
});
操作方法(5个)重写原因?

push、pop、shift、unshift、splice

这些方法会修改原数组,会导致多次触发 Proxy 的 set 陷阱(例如 push 先设置新索引,再改 length)。如果没有优化,一次 push 就会触发多次更新,造成性能浪费和潜在的错误。Vue 通过 noTracking 包裹,在执行前暂停依赖收集开启批量更新,执行完原生方法后恢复追踪合并为一次更新

  1. 避免循环依赖导致的无限循环。例如 push 方法会改变 length 属性,length属性变化又会触发副作用执行,以此会进入无限循环。
  2. 性能优化,避免不必要的依赖收集。例如 push 是写操作,不应该收集任何依赖。
  3. 批量更新优化。例如 push 操作会触发多次更新(设置新元素、更新 length),批量更新可以合并这些更新。
  4. 保持响应式语义。例如 虽然暂停了依赖收集,但 push 仍然会触发响应式更新。
┌─────────────────────────────────────────────────────────────────────┐
│                    arr.push(item) 执行流程                          │
├─────────────────────────────────────────────────────────────────────┤
│  1. 访问 arr.push                                                  │
│     └─→ 触发 Proxy get 拦截器                                     │
│     └─→ 返回 arrayInstrumentations.push(重写后的方法)            │
│     └─→ ❌ 这里不会触发依赖收集(因为 push 是方法名,不是数据属性)   │
│                                                                     │
│  2. 调用 push(item)                                                │
│     └─→ 进入 noTracking(this, 'push', [item])                      │
│                                                                     │
│  3. pauseTracking()                                                │
│     └─→ 设置 shouldTrack = false(暂停依赖收集)                    │
│                                                                     │
│  4. (toRaw(self) as any)['push'].apply(self, [item])              │
│     ├─→ toRaw(self) 获取原始数组                                    │
│     ├─→ 在原始数组上调用 push 方法                                  │
│     ├─→ this 绑定到响应式数组(确保某些操作正确执行)                │
│     ├─→ push 内部会访问 length、设置索引                            │
│     └─→ ❌ 这些操作都不会触发依赖收集(因为 shouldTrack = false)   │
│                                                                     │
│  5. resetTracking()                                                │
│     └─→ 恢复 shouldTrack = true                                    │
│                                                                     │
│  6. 返回 push 的返回值(新数组长度)                                  │
└─────────────────────────────────────────────────────────────────────┘
push 能触发响应式更新的根本原因?

Vue 3 中 push 触发更新的根本原因是 Proxy 对索引和 length 赋值的拦截以及随后的依赖通知机制

示例 数组迭代器遍历 for ...of

const reactiveCopyArr = reactive([
  "item1",
  {
    list: ["child1", "child2"],
  },
  "item3",
]);

const handleClick = () => {
  for (const item of reactiveCopyArr) {
    console.log("item", item);
  }
};

image.png

获取迭代器方法

image.png

遍历第一个元素

image.png

遍历第二个元素

对象类型,会包装成响应式返回

image.png

image.png

到最后,遍历结束

image.png

迭代器方法(3个)重写原因?

Symbol.iterator、values、entries

原生迭代器遍历响应式数组时,返回的是原始存储的值(例如 Ref 对象、普通对象),而用户期望在遍历中获得自动解包后的值(Ref 变成 .value)或深度响应式代理(方便后续修改)。重写后,迭代器会对每个元素调用 toWrapped(this, item),确保遍历结果与直接通过索引访问的行为一致。

  1. 确保迭代操作能够被正确追踪(依赖收集)。
  2. 确保迭代返回的元素是响应式代理。
  3. 解决 Ref 自动解包问题。响应式数组中如果包含 Ref 对象,迭代时应该自动解包。
  4. 处理代理/原始值混合遍历的正确性问题。当数组中同时包含代理对象和原始对象时,迭代方法需要正确处理这种混合场景。
keys 方法不需要重写的原因?

keys 方法只返回一个迭代器,它仅读取 length 属性确定迭代范围,在真正遍历之前不会读取具体索引。而 length 本来就是响应式的(Vue 3 的 Proxy 会拦截 length 的读取),因而不需要特殊包装。

示例 数组遍历 values
const reactiveCopyArr = reactive([
  "item1",
  {
    list: ["child1", "child2"],
  },
  "item3",
]);

const handleClick = () => {
  const result = reactiveCopyArr.values();
  console.log("result", result);
  
  let res;
  do {
    res = result.next();
    console.log(res);
  } while (!res.done);
};

image.png

数组的values返回一个数组迭代器对象.

  • next方法(如果元素是对象会进行响应式包装后返回)。
  • _next方法(返回原始元素)。

image.png

示例 数组查找元素

const reactiveCopyArr = reactive([
  "item1",
  {
    list: ["child1", "child2"],
  },
  "item3",
]);

 const flag = reactiveCopyArr.includes("item2");
 console.log("flag", flag);

image.png

image.png

查找方法 (3个)重写原因?

includes、indexOf、lastIndexOf

响应式数组存储的是原始值(通过 toRaw 可从代理中获得),但用户调用 includes 时可能传入原始对象代理对象。由于 === 比较下 Proxy ≠ 原始对象,原生方法会查找失败。Vue 重写后,通过 searchProxy 将参数和数组元素都 toRaw 后再比较,保证语义正确。

  1. 解决代理与原始对象的比较问题。
源码 searchProxy
  1. 优先使用原始参数:保持响应式语义,允许用户传递响应式代理进行搜索
  2. 失败后重试:如果搜索失败且参数是代理,解包后重新搜索
function searchProxy(
  self: unknown[],
  method: keyof Array<any>,
  args: unknown[],
) {
  const arr = toRaw(self) as any
  track(arr, TrackOpTypes.ITERATE, ARRAY_ITERATE_KEY)

  const res = arr[method](...args)

  if ((res === -1 || res === false) && isProxy(args[0])) {
    args[0] = toRaw(args[0])
    return arr[method](...args)
  }

  return res
}

示例 数组迭代

const result = reactiveCopyArr.find((item) => typeof item === "string");
console.log("result", result);

image.png

image.png

示例 数组迭代
const result = reactiveCopyArr.find((item) => typeof item === "object");
console.log("result", result);

image.png

迭代方法(9个)重写原因?

forEach、findLastIndex、findLast、findIndex、find、filter、every、map、some

这些方法会遍历数组并将每个元素传给用户回调。为了保证回调中拿到的是响应式代理(便于后续修改或读取深层属性)以及Ref 自动解包,需要将每个原始元素通过 toWrapped 包装后再传入。此外,filtermap 返回的新数组应该也是响应式的,因此需要额外对结果数组进行整体包装。

  1. 确保迭代操作能够被正确追踪(依赖收集)。
  2. 确保回调函数接收的元素是响应式代理。当迭代响应式数组时,传递给回调函数的元素也应该是响应式的,这样才能保持嵌套响应性。
  3. 修复回调函数的第三个参数(数组引用)。数组迭代方法的回调函数通常有三个参数:(item, index, array)。第三个参数 array 应该是响应式代理,而不是原始数组。
  4. 处理返回值的响应式包装。某些方法(如 filter、find)返回的结果需要保持响应式。
  5. 兼容用户扩展数组。

示例 数组 reduce

const sum = reactiveCopyArr.reduce((acc, item) => {
  console.log("acc", acc);
  console.log("item", item);
  return typeof item === "string" ? acc + item : acc;
}, "");

image.png

image.png

重写原因?

与遍历方法类似,回调中的 item 需要是响应式版本。归约方法的回调签名多了一个累计参数,因此使用专门的 reduce 辅助函数,同样对每个元素调用 toWrapped

  1. 正确追踪依赖(数组变化时触发视图更新)
  2. 保持响应式传递(回调函数接收的元素是响应式代理)
  3. 修复回调参数(确保第四个参数是响应式数组引用)

示例 数组 concat

非破坏性方法(5个)

concat、join、toReversed、toSorted、toSpliced

这些方法返回一个新数组,该数组的元素应该是响应式代理(如果元素是对象)或经过解包的 Ref。直接调用原生方法会得到普通数组,丢失响应性。Vue 通过 reactiveReadArray 将结果数组转换为响应式数组

join 只需要将元素转为字符串,不涉及响应式依赖或比较问题。直接调用原始数组执行即可,避免多余的代理层开销。使用 reactiveReadArray 获取内部原始数组后调用原生 join

  1. 确保迭代操作能够被正确追踪(依赖收集)
  2. 确保返回的新数组元素保持响应式
  3. 处理参数中的嵌套响应式数组。concat 方法需要处理参数中可能包含的响应式数组。
  4. 兼容 ES2023 新增方法。toReversed、toSorted、toSpliced 是 ES2023 新增的数组方法,需要为它们提供响应式支持。
数组没有重写的方法 reverse、sort

不会引起无限循环的问题。

🍒 依赖收集

示例 副作用函数访问数组索引,会触发依赖收集

收集的条件:

  1. 开启可追踪依赖
  2. 当前有正在执行的副作用函数
effect(() => {
  // 副作用函数,会收集依赖
  console.log("effect 值", reactiveArr[0]);
});

image.png

image.png

reactive 响应式数组涉及的 dep 依赖,会存储在 targetMap 缓存映射表中。

image.png

示例 副作用函数迭代数组,会触发依赖收集

获取迭代器函数,这里不收集依赖,直接获取的是函数。

image.png

收集依赖

image.png

🍒 示例 利用 toRefs 解构

import { reactive, toRefs } from 'vue'

const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state)

// 现在 count 和 name 是 ref 对象,需要通过 .value 访问和修改
count.value++       // 视图更新
name.value = 'Vue3' // 视图更新

【示例】解构数组

const testArr = reactive(["item1", "item2", "item3"]);
const { 0: item1 } = toRefs(testArr);

image.png

🍒 示例 利用 toRef 逐个解构

import { reactive, toRef } from 'vue'

const state = reactive({ count: 0 })
const countRef = toRef(state, 'count')
countRef.value++  // 响应式更新

🍒 响应式 Map

集合类型的所有可观察操作(读、写、删除、清空、迭代等)都是通过属性访问触发的

示例 获取 size

const reactiveMap = reactive(
  new Map([
    ["key1", { age: 20 }],
    ["key2", { age: 30 }],
  ])
);

effect(() => {
  console.log("reactiveMap", reactiveMap.size);
});

image.png

image.png

示例 has

const reactiveMap = reactive(
  new Map([
    ["key1", { age: 20 }],
    ["key2", { age: 30 }],
  ])
);

effect(() => {
  const hasKey1 = reactiveMap.has("key1");
  console.log("hasKey1", hasKey1);
});

image.png

image.png

示例 get

const reactiveMap = reactive(
  new Map([
    ["key1", { age: 20 }],
    ["key2", { age: 30 }],
  ])
);

effect(() => {
  const key1 = reactiveMap.get("key1");
  console.log("key1", key1);
});

image.png

image.png

🍒 响应式 Set

const info = reactive(new Set(["a", "b", "c"]));

const handleClick = () => {
  info.add("d");
};
effect(() => {
  console.log("info", info.has("a"));
});

执行 info.has("a")

image.png

执行info.add("d")

image.png

若新添加的 value 已存在集合中,直接返回。

image.png

🍒 reactive 使用注意事项

  1. 不能直接重新赋值整个对象。reactive 返回一个 Proxy 代理对象,重新赋值会使原代理对象被丢弃,新对象不再是响应式的。
let state = reactive({ count: 0 })
// ❌ 错误:重新赋值会断开响应式引用
state = reactive({ count: 1 })
  1. 不能解构,解构会丢失响应性。解构需要使用 toRefs 包装
const state = reactive({ count: 0, name: 'vue' })
// ❌ 错误:解构后的 count、name 是普通值,不具响应性
let { count, name } = state
count++ // 不会触发更新

// ✅ 正确:使用 toRefs 包装
import { toRefs } from 'vue'
const { count, name } = toRefs(state)
count.value++ // 触发更新
  1. 只能用于对象/数组等引用类型,不能用于基本类型。由于proxy不能代理基本类型。
// ❌ reactive 不能包装 string、number、boolean 等
let str = reactive('hello') // 警告,且无效
  1. 在组合式函数中返回响应式对象时,建议使用 toRefs
  2. 直接监听 reactive 对象时,默认深度监听,回调中新旧值相同(指向同一对象引用)。
  3. 响应式对象的属性值如果是 ref,会自动解包。
  4. 懒递归,只在访问深层属性时才将其转换为响应式,初始化时不会递归遍历整个对象,性能更优。

shallowReactive

shallowReactive 返回一个浅层响应式代理,只拦截对象第一层属性的读取和修改操作。对于深层嵌套的属性,它们不会被转换为响应式代理,而是直接返回原始值。因此,修改深层属性不会触发响应式更新。

示例

const shallowObj = shallowReactive({
  count: 0,
  name: "shallowObj",
  list: [1, 2, 3],
  info: {
    age: 20,
    list: [4, 5, 6],
  },
});

const info = toRef(shallowObj, "info");

const handleClick = () => {
  // 利用解构操作、不会触发响应式更新
  info.value.age++;
};

image.png

【示例】修改深处属性,不会响应式更新

const handleClick = () => {
  // 可以修改,但是不会触发响应式更新
  shallowObj.info.age++;
};

【示例】修改顶层属性,会响应式更新

const handleClick = () => {
  // shallowObj.info 是顶层属性,修改会触发响应式更新
  shallowObj.info = {
    age: 30,
    list: [7, 8, 9],
  };
};

effect(() => {
  console.log("shallowObj", shallowObj.info);
});

获取 shallowObj.info ,不会触发 track。当前没有活跃的副作用函数。

image.png

修改 shallowObj.info 会触发 trigger。

image.png

【示例】修改深层对象,不会响应式更新

const shallowObj = shallowReactive({
  count: 0,
  name: "shallowObj",
  list: [1, 2, 3],
  info: {
    age: 20,
    list: [4, 5, 6],
  },
});
const handleClick = () => {
  // 先访问 shallowObj.info,得到普通对象
  // 然后修改该普通对象的 list 属性,赋值为新数组
  // 由于 info 不是响应式代理,这个修改不会触发任何响应式更新。而且 list 也不是响应式数组
  shallowObj.info.list = [7, 8, 9];
};

示例

const shallowState = shallowReactive({
  count: 0, // 响应式
  nested: {
    // nested 本身是响应式(第一层),但其内部的 value 不是
    value: 1,
  },
  arr: [1, 2, 3], // arr 是响应式(第一层),但数组内的元素不是响应式
});

console.log("shallowState", shallowState);

const handleClick = () => {
  // 会触发 watch 监听,因为 count 是响应式的
  shallowState.count++;
};

const handleClick2 = () => {
  // 不会触发 watch 监听,因为数组元素不是响应式的
  // shallowState.arr[4] = 4;

  // 不会触发,数组的引用没有发生改变
  // shallowState.arr.push(4);

  // 会触发,数组的引用发生了改变
  shallowState.arr = [4, 5, 6];
};

watch(
  () => shallowState.count,
  (newVal, oldVal) => {
    // 新旧值 是同一个引用
    console.log("watch shallowState.count 值", newVal, oldVal);
  }
);

watch(shallowState, (newVal, oldVal) => {
  // 新旧值 是同一个引用
  console.log("watch shallowState 值", newVal, oldVal);
});


watch(
  () => shallowState.arr,
  (newVal, oldVal) => {
    console.log("watch shallowState.arr 值", newVal, oldVal);
  }
);
// ❌ 直接监听 shallowState.arr 会发出警告、因为数组元素不是响应式的
watch(shallowState.arr, (newVal, oldVal) => {
  console.log("watch shallowState.arr 值", newVal, oldVal);
});

image.png

image.png

image.png

示例 修改 shallowState.arr.push(4)

先执行 get方法 shallowState.arr

image.png

image.png

使用场景?

  1. 性能优化 - 大型数据结构
  2. 不可变数据模式
  3. 缓存/快照数据。对于不需要响应式的缓存数据

readonly

readonly 是 Vue 3 提供的 API,用于创建一个深度只读的响应式代理。任何尝试修改其属性的操作都会在控制台发出警告(开发模式),并且修改无效。

示例 作用于普通对象

传入一个普通对象,返回一个深度只读的代理。修改顶层或嵌套属性都会警告。

const readOnlyObj = readonly({
  count: 0,
  name: "effectB",
  age: 20,
  extra: {
    a: 1,
    b: 2,
  },
});

const handleClick = () => {
  // readOnlyObj.extra 返回的也是一个只读代理 
  console.log("click", readOnlyObj.extra);
};

image.png

示例 作用于ref 对象

传入一个 ref,返回一个只读的 ref 代理。修改 .value 会触发警告,但原 ref 仍然可写。

const count = ref(0);
const readOnlyCount = readonly(count);

image.png

const handleClick = () => {
  readOnlyCount.value++;
};

执行readOnlyCount.value++ ,会先获取 readOnlyCount.value的值。因为 readOnlyCount 是只读的,不会进行依赖收集。

image.png

获取 value 后进行自增操作,会发出警告。

image.png

image.png

示例 作用于 reactive 对象

传入一个 reactive 对象,返回一个深度只读的代理。原对象依然可写,但对只读代理写操作会警告。

原来的 reactive 对象任何修改 ,任何依赖深度只读代理 的副作用函数也会执行。

const reactiveObj = reactive({
  count: 0,
  name: "effectB",
  age: 20,
  extra: {
    a: 1,
    b: 2,
  },
});
const readOnlyObj = readonly(reactiveObj);

image.png

示例 作用于已有 readonly 对象

对一个已经只读的对象再次调用 readonly,返回原对象本身(幂等性)。不会产生新的代理嵌套。

const count = ref(0);
const readOnlyCount = readonly(count);

const readOnlyObj = readonly(readOnlyCount);

image.png

image.png

使用场景?

  1. 组合式函数暴露只读接口
  2. 保护配置对象

shallowReadonly

只将对象的第一层属性变为只读(禁止修改),更深层的属性保持原样——既不会强制变为只读,也不会递归转换为响应式。

示例 作用于普通对象

const shallowReadOnlyObj = shallowReadonly({
  count: 0,
  name: "effectB",
  age: 20,
  extra: {
    a: 1,
    b: 2,
  },
});

const handleClick = () => {
  // shallowReadOnlyObj.extra 返回的是普通对象
  console.log("click", shallowReadOnlyObj.extra);
};

image.png

作用于 reactive 对象

const reactiveObj = reactive({
  count: 0,
  name: "effectB",
  age: 20,
  extra: {
    a: 1,
    b: 2,
  },
});
const shallowReadOnlyObj = shallowReadonly(reactiveObj);

const handleClick = () => {
  console.log("click", shallowReadOnlyObj.extra);
};

image.png

const handleClick = () => {
  console.log("click", shallowReadOnlyObj.extra.a++);
};

获取 shallowReadOnlyObj.extra.a属性, shallowReadOnlyObj.extra是 浅层。

image.png

image.png

由于没有活跃的副作用,因此不会触发。

image.png

源码 createReactiveObject

reactive 、readonly 、shallowReactive、 shallowReadonly 都由 createReactiveObject 函数来实现。

  • ObjactArray 的对象由 基本类型对象代理处理器实现
  • 集合类型(Map 、Set、WeakMap、WeakSet )的对象 由 集合代理处理器实现
/**
 * 负责创建响应式对象(reactive)或只读响应式对象(readonly)
 * @param target 要转换为响应式的目标对象,可以是普通对象、数组、Map、Set 等
 * @param isReadonly 指示是否创建只读的响应式对象
 * @param baseHandlers 基本类型对象(普通对象、数组)的代理处理器,包含 get、set、deleteProperty 等陷阱(trap)
 * @param collectionHandlers 集合类型对象(Map、Set、WeakMap、WeakSet)的代理处理器
 * @param proxyMap 存储原始对象与代理对象的映射关系
 * @returns
 */
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>,
) {
  if (!isObject(target)) {
    // 在开发环境下,发出警告,提示不能将非对象转换为响应式
    if (__DEV__) {
      warn(
        `value cannot be made ${isReadonly ? 'readonly' : 'reactive'}: ${String(
          target,
        )}`,
      )
    }
    return target
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (
    // 检查目标是否已经是代理对象(具有 ReactiveFlags.RAW 标志)
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // only specific value types can be observed.
  const targetType = getTargetType(target) // 获取目标对象的类型
  // 如果类型无效(如 symbol、function 等),直接返回目标
  if (targetType === TargetType.INVALID) {
    return target
  }
  // target already has corresponding Proxy
  // 如果目标对象已经存在对应的代理对象,直接返回
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }

  // 创建代理对象
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
  )
  // 将代理对象与原始对象关联起来
  proxyMap.set(target, proxy)
  return proxy
}

ref

在 Vue 3 的组合式 API(Composition API)中,ref 是最基础、最常用的响应式 API 之一。它用于在组件中创建响应式的状态,无论是基本类型还是对象类型,都可以通过 ref 包装后获得响应式能力。

ref 是 reference(引用)的缩写。它返回一个包含 value 属性的对象,这个 value 属性具有响应性。通过 .value 访问或修改内部的值时,Vue 能够追踪变化并触发相应的更新。

简单来说,ref 将一个普通值包装成一个响应式引用,使得这个值的变化能够被 Vue 的响应式系统感知。

示例 基本类型

【示例】基本使用

const count = ref(0);

const handleClick = () => {
  count.value++;
};

// 监听单个 ref
watch(count, (newVal, oldVal) => {
  console.log("info.count 值", newVal, oldVal);
});

【示例】批量更新

const count = ref(0);

const handleClick = () => {
  // 批量更新,触发 watch 一次
   count.value++;
   count.value++;
   count.value++;

};

watch(count, (newVal, oldVal) => {
  console.log("watch count 值", newVal, oldVal);
});

【示例】批量更新

const count = ref(0);
const firstCount = ref(0);

const handleClick = () => {
  // 批量更新,update 一次
  count.value++;
  firstCount.value++;
};

watch(count, (newVal, oldVal) => {
  console.log("watch count 值", newVal, oldVal);
});

watch(firstCount, (newVal, oldVal) => {
  console.log("watch firstCount 值", newVal, oldVal);
});

onUpdated(() => {
  console.log("-----onUpdated ---");
});

【示例】异步更新

const count = ref(0);

const handleClick = () => {
  // 异步更新,触发 watch 两次
  count.value++;
  setTimeout(() => {
    count.value++;
  }, 1000);
};

watch(count, (newVal, oldVal) => {
  console.log("watch count 值", newVal, oldVal);
});

示例 数组

const datas = ref([3, 4, 5]);

effect(() => {
  console.log("datas", datas.value);
});

image.png

const handleClick = () => {
  // 获取 datas.value 不会收集依赖,因为没有正在执行的副作用
  // 执行push 操作,是reactive对象,会触发响应式
  datas.value.push(6);
};

注意事项

  1. 脚本中必须使用 .value。在 <script> 或 <script setup> 中,访问或修改 ref 的内容时必须通过 .value,除非在某些自动解包的场景(如 reactive 对象属性)。
  2. 模板中顶层 ref 自动解包。
  3. 解构会丢失响应性。
  4. 整体替换 ref 变量会断开引用

源码 RefImpl

vue3-core/packages/reactivity/src/ref.ts

class RefImpl<T = any> {
  _value: T
  private _rawValue: T

  dep: Dep = new Dep()

  public readonly [ReactiveFlags.IS_REF] = true
  public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false

  constructor(value: T, isShallow: boolean) {
    // 初始化原始值和值
    this._rawValue = isShallow ? value : toRaw(value)
    // 如果参数是引用类型,用 reactive
    this._value = isShallow ? value : toReactive(value)
    // 标记是否浅层
    this[ReactiveFlags.IS_SHALLOW] = isShallow
  }

  get value() {
    // 记录依赖追踪信息
    if (__DEV__) {
      this.dep.track({
        target: this,
        type: TrackOpTypes.GET,
        key: 'value',
      })
    } else {
      // 收集依赖
      this.dep.track()
    }
    return this._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)) {
      this._rawValue = newValue
      this._value = useDirectValue ? newValue : toReactive(newValue)
      if (__DEV__) {
        this.dep.trigger({
          target: this,
          type: TriggerOpTypes.SET,
          key: 'value',
          newValue,
          oldValue,
        })
      } else {
        // 触发依赖更新(dep.trigger())
        this.dep.trigger()
      }
    }
  }
}

shallowRef

  1. 仅监听 .value 替换,内部属性修改不触发更新

【示例】基本类型

const count = shallowRef(0);

const handleClick = () => {
// 
  count.value++;
};

effect(() => {
  console.log("count", count.value);
});

【示例】普通对象

const objShallow = shallowRef({ a: 1 });

const handleClick = () => {
  // objShallow.value 获取的是 普通对象,非响应式
  // 直接修改普通对象的属性,不会响应式更新
  objShallow.value.a++;
};

image.png

triggerRef

强制触发依赖于一个浅层 ref 的副作用,这通常在对浅引用的内部值进行深度变更后使用。

const objShallow = shallowRef({ a: 1 });

const handleClick = () => {
  objShallow.value.a++;
};

const handleClick2 = () => {
  triggerRef(objShallow);
};
effect(() => {
  console.log("effect副作用", objShallow.value.a);
});

vue3-core/packages/reactivity/src/ref.ts

function triggerRef(ref: Ref): void {
  // ref may be an instance of ObjectRefImpl
  // dep 属性检查,确保 ref 是响应式的
  if ((ref as unknown as RefImpl).dep) {
    if (__DEV__) {
      ;(ref as unknown as RefImpl).dep.trigger({
        target: ref,
        type: TriggerOpTypes.SET,
        key: 'value',
        newValue: (ref as unknown as RefImpl)._value,
      })
    } else {
      // 直接调用了 ref 内部的 dep.trigger() 方法,绕过了 set value 的值变化检查
      ;(ref as unknown as RefImpl).dep.trigger()
    }
  }
}

响应式工具API

toRaw

toRaw 是专门为 reactive(以及 readonly)这类响应式代理对象设计的,用来获取其原始对象(即代理的内部目标 target)。

reactive 返回的是一个 Proxy 对象,对它的操作会被拦截并转发给原始对象。但有时你需要绕过 Proxy 直接操作原始对象(例如性能优化、避免不必要的响应式更新、或与不希望接收 Proxy 的第三方库交互),这时就可以用 toRaw

  • 仅对响应式代理有效:如果传入一个普通对象,toRaw 会直接返回该对象本身(不做任何处理)。
  • 不会触发响应式:直接修改 toRaw 返回的原始对象不会触发视图更新,因此通常只在必要时使用。
  • 可嵌套获取toRaw 只会剥离一层代理,如果原始对象内部还包含其他响应式代理,需要递归调用。
function toRaw<T>(observed: T): T {
  const raw = observed && (observed as Target)[ReactiveFlags.RAW]
  return raw ? toRaw(raw) : observed
}

image.png

【示例】

  const projects = ref({
    id: 'xx',
    desc: '项目描述'
  })

  console.log('projects',projects,)
  console.log('projects.value',projects.value)
  console.log('toRaw projects.value', toRaw(projects.value))

image.png

toReadonly

toReadonly 函数是一个自定义的辅助工具,它的作用是将一个值“安全地”转换为只读形式

  • 如果 value 是对象,则调用 Vue 的 readonly 函数,返回一个深层只读代理
  • 如果 value 不是对象,则原样返回,但通过类型断言告诉 TypeScript 这个值已经是 DeepReadonly<T>
 const toReadonly = <T extends unknown>(value: T): DeepReadonly<T> =>
  isObject(value) ? readonly(value) : (value as DeepReadonly<T>)

toReactive

toReactive 是一个辅助工具函数,它的作用是:如果传入的值是对象,则将其转换为 reactive 响应式代理;否则原样返回

const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value

toRef

function toRef(
  source: Record<string, any> | MaybeRef,
  key?: string,
  defaultValue?: unknown,
): Ref {
  // 如果 source 是一个 ref 对象,直接返回
  if (isRef(source)) {
    return source
    
    // 如果 source 是一个函数,创建一个 getter ref 对象
  } else if (isFunction(source)) {
    return new GetterRefImpl(source) as any

    // 如果 source 是一个对象且传入了至少两个参数(即指定了 key)
  } else if (isObject(source) && arguments.length > 1) {
    return propertyToRef(source, key!, defaultValue)
    
  } else {
    // 如果 source 是一个普通值,创建一个普通 ref 对象
    return ref(source)
  }
}

参数是函数时处理如下:

class GetterRefImpl<T> {
  public readonly [ReactiveFlags.IS_REF] = true
  public readonly [ReactiveFlags.IS_READONLY] = true
  public _value: T = undefined!

  // 接收参数 _getter 作为实例属性
  constructor(private readonly _getter: () => T) {}
  get value() {
    // 调用 _getter 函数获取值
    return (this._value = this._getter())
  }
}

参数是对象 且带有 key 时处理如下:

function propertyToRef(
  source: Record<string, any>,
  key: string,
  defaultValue?: unknown,
) {
  return new ObjectRefImpl(source, key, defaultValue) as any
}

toValue

将可能是 refcomputedgetter 函数的值统一转换为其底层的原始值。

function toValue<T>(source: MaybeRefOrGetter<T>): T {

  return isFunction(source) ? source() : unref(source)
}

unref

function unref<T>(ref: MaybeRef<T> | ComputedRef<T>): T {
  // 如果是 参数 ref 对象,返回其 value 属性
  // 否则,返回 参数 ref 本身
  return isRef(ref) ? ref.value : ref
}

markRaw

标记一个对象,使其永远不会被转换为响应式对象

function markRaw<T extends object>(value: T): Raw<T> {
  // 如果对象没有 ReactiveFlags.SKIP 标志,且是可扩展的
  if (!hasOwn(value, ReactiveFlags.SKIP) && Object.isExtensible(value)) {
    // 定义 ReactiveFlags.SKIP 标志,值为 true
    def(value, ReactiveFlags.SKIP, true)
  }
  return value
}

最后

  1. Map 集合WeakMap
  2. Set 集合WeakSet
  3. vue 响应式