Vue 3 的响应式系统基于 Proxy 提供了四种不同深度的 API,用于控制数据的响应式行为和读写权限。
reactive 深层响应式
在 Vue 3 中,reactive 是一个用于创建深度响应式对象的 API。它接收一个普通对象(或数组、Map、Set 等),并返回该对象的 Proxy 代理。通过这个代理,可以像操作普通对象一样修改数据,所有变更都会被 Vue 自动追踪,并在数据变化时触发视图更新。
🍒 响应式对象
- 直接修改对象属性
- 新值属性
- 删除某个属性
- 批量更新。使用
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);
});
【示例】为什么点击修改 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);
}
);
🍒 响应式数组(重写了27个方法)
在 Vue 3 中,reactive 创建的响应式数组,直接使用原生数组方法(push、pop、splice 等)或 通过索引 直接修改元素或修改 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 包裹,在执行前暂停依赖收集并开启批量更新,执行完原生方法后恢复追踪并合并为一次更新。
- 避免循环依赖导致的无限循环。例如 push 方法会改变 length 属性,length属性变化又会触发副作用执行,以此会进入无限循环。
- 性能优化,避免不必要的依赖收集。例如
push是写操作,不应该收集任何依赖。 - 批量更新优化。例如
push操作会触发多次更新(设置新元素、更新length),批量更新可以合并这些更新。 - 保持响应式语义。例如 虽然暂停了依赖收集,但
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);
}
};
获取迭代器方法
遍历第一个元素
遍历第二个元素
对象类型,会包装成响应式返回
到最后,遍历结束
迭代器方法(3个)重写原因?
Symbol.iterator、values、entries
原生迭代器遍历响应式数组时,返回的是原始存储的值(例如 Ref 对象、普通对象),而用户期望在遍历中获得自动解包后的值(Ref 变成 .value)或深度响应式代理(方便后续修改)。重写后,迭代器会对每个元素调用 toWrapped(this, item),确保遍历结果与直接通过索引访问的行为一致。
- 确保迭代操作能够被正确追踪(依赖收集)。
- 确保迭代返回的元素是响应式代理。
- 解决 Ref 自动解包问题。响应式数组中如果包含 Ref 对象,迭代时应该自动解包。
- 处理代理/原始值混合遍历的正确性问题。当数组中同时包含代理对象和原始对象时,迭代方法需要正确处理这种混合场景。
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);
};
数组的values返回一个数组迭代器对象.
next方法(如果元素是对象会进行响应式包装后返回)。_next方法(返回原始元素)。
示例 数组查找元素
const reactiveCopyArr = reactive([
"item1",
{
list: ["child1", "child2"],
},
"item3",
]);
const flag = reactiveCopyArr.includes("item2");
console.log("flag", flag);
查找方法 (3个)重写原因?
includes、indexOf、lastIndexOf
响应式数组存储的是原始值(通过 toRaw 可从代理中获得),但用户调用 includes 时可能传入原始对象或代理对象。由于 === 比较下 Proxy ≠ 原始对象,原生方法会查找失败。Vue 重写后,通过 searchProxy 将参数和数组元素都 toRaw 后再比较,保证语义正确。
- 解决代理与原始对象的比较问题。
源码 searchProxy
- 优先使用原始参数:保持响应式语义,允许用户传递响应式代理进行搜索
- 失败后重试:如果搜索失败且参数是代理,解包后重新搜索
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);
示例 数组迭代
const result = reactiveCopyArr.find((item) => typeof item === "object");
console.log("result", result);
迭代方法(9个)重写原因?
forEach、findLastIndex、findLast、findIndex、find、filter、every、map、some
这些方法会遍历数组并将每个元素传给用户回调。为了保证回调中拿到的是响应式代理(便于后续修改或读取深层属性)以及Ref 自动解包,需要将每个原始元素通过 toWrapped 包装后再传入。此外,filter、map 返回的新数组应该也是响应式的,因此需要额外对结果数组进行整体包装。
- 确保迭代操作能够被正确追踪(依赖收集)。
- 确保回调函数接收的元素是响应式代理。当迭代响应式数组时,传递给回调函数的元素也应该是响应式的,这样才能保持嵌套响应性。
- 修复回调函数的第三个参数(数组引用)。数组迭代方法的回调函数通常有三个参数:(item, index, array)。第三个参数 array 应该是响应式代理,而不是原始数组。
- 处理返回值的响应式包装。某些方法(如 filter、find)返回的结果需要保持响应式。
- 兼容用户扩展数组。
示例 数组 reduce
const sum = reactiveCopyArr.reduce((acc, item) => {
console.log("acc", acc);
console.log("item", item);
return typeof item === "string" ? acc + item : acc;
}, "");
重写原因?
与遍历方法类似,回调中的 item 需要是响应式版本。归约方法的回调签名多了一个累计参数,因此使用专门的 reduce 辅助函数,同样对每个元素调用 toWrapped。
- 正确追踪依赖(数组变化时触发视图更新)
- 保持响应式传递(回调函数接收的元素是响应式代理)
- 修复回调参数(确保第四个参数是响应式数组引用)
示例 数组 concat
非破坏性方法(5个)
concat、join、toReversed、toSorted、toSpliced
这些方法返回一个新数组,该数组的元素应该是响应式代理(如果元素是对象)或经过解包的 Ref。直接调用原生方法会得到普通数组,丢失响应性。Vue 通过 reactiveReadArray 将结果数组转换为响应式数组
join 只需要将元素转为字符串,不涉及响应式依赖或比较问题。直接调用原始数组执行即可,避免多余的代理层开销。使用 reactiveReadArray 获取内部原始数组后调用原生 join。
- 确保迭代操作能够被正确追踪(依赖收集)
- 确保返回的新数组元素保持响应式
- 处理参数中的嵌套响应式数组。concat 方法需要处理参数中可能包含的响应式数组。
- 兼容 ES2023 新增方法。toReversed、toSorted、toSpliced 是 ES2023 新增的数组方法,需要为它们提供响应式支持。
数组没有重写的方法 reverse、sort
不会引起无限循环的问题。
🍒 依赖收集
示例 副作用函数访问数组索引,会触发依赖收集
收集的条件:
- 开启可追踪依赖
- 当前有正在执行的副作用函数
effect(() => {
// 副作用函数,会收集依赖
console.log("effect 值", reactiveArr[0]);
});
reactive 响应式数组涉及的 dep 依赖,会存储在 targetMap 缓存映射表中。
示例 副作用函数迭代数组,会触发依赖收集
获取迭代器函数,这里不收集依赖,直接获取的是函数。
收集依赖
🍒 示例 利用 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);
🍒 示例 利用 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);
});
示例 has
const reactiveMap = reactive(
new Map([
["key1", { age: 20 }],
["key2", { age: 30 }],
])
);
effect(() => {
const hasKey1 = reactiveMap.has("key1");
console.log("hasKey1", hasKey1);
});
示例 get
const reactiveMap = reactive(
new Map([
["key1", { age: 20 }],
["key2", { age: 30 }],
])
);
effect(() => {
const key1 = reactiveMap.get("key1");
console.log("key1", key1);
});
🍒 响应式 Set
const info = reactive(new Set(["a", "b", "c"]));
const handleClick = () => {
info.add("d");
};
effect(() => {
console.log("info", info.has("a"));
});
执行 info.has("a")
执行info.add("d")
若新添加的 value 已存在集合中,直接返回。
🍒 reactive 使用注意事项
- 不能直接重新赋值整个对象。
reactive返回一个 Proxy 代理对象,重新赋值会使原代理对象被丢弃,新对象不再是响应式的。
let state = reactive({ count: 0 })
// ❌ 错误:重新赋值会断开响应式引用
state = reactive({ count: 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++ // 触发更新
- 只能用于对象/数组等引用类型,不能用于基本类型。由于proxy不能代理基本类型。
// ❌ reactive 不能包装 string、number、boolean 等
let str = reactive('hello') // 警告,且无效
- 在组合式函数中返回响应式对象时,建议使用
toRefs。 - 直接监听 reactive 对象时,默认深度监听,回调中新旧值相同(指向同一对象引用)。
- 响应式对象的属性值如果是
ref,会自动解包。 - 懒递归,只在访问深层属性时才将其转换为响应式,初始化时不会递归遍历整个对象,性能更优。
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++;
};
【示例】修改深处属性,不会响应式更新
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。当前没有活跃的副作用函数。
修改 shallowObj.info 会触发 trigger。
【示例】修改深层对象,不会响应式更新
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);
});
示例 修改 shallowState.arr.push(4)
先执行 get方法 shallowState.arr
使用场景?
- 性能优化 - 大型数据结构
- 不可变数据模式
- 缓存/快照数据。对于不需要响应式的缓存数据
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);
};
示例 作用于ref 对象
传入一个 ref,返回一个只读的 ref 代理。修改 .value 会触发警告,但原 ref 仍然可写。
const count = ref(0);
const readOnlyCount = readonly(count);
const handleClick = () => {
readOnlyCount.value++;
};
执行readOnlyCount.value++ ,会先获取 readOnlyCount.value的值。因为 readOnlyCount 是只读的,不会进行依赖收集。
获取 value 后进行自增操作,会发出警告。
示例 作用于 reactive 对象
传入一个 reactive 对象,返回一个深度只读的代理。原对象依然可写,但对只读代理写操作会警告。
原来的 reactive 对象任何修改 ,任何依赖深度只读代理 的副作用函数也会执行。
const reactiveObj = reactive({
count: 0,
name: "effectB",
age: 20,
extra: {
a: 1,
b: 2,
},
});
const readOnlyObj = readonly(reactiveObj);
示例 作用于已有 readonly 对象
对一个已经只读的对象再次调用 readonly,返回原对象本身(幂等性)。不会产生新的代理嵌套。
const count = ref(0);
const readOnlyCount = readonly(count);
const readOnlyObj = readonly(readOnlyCount);
使用场景?
- 组合式函数暴露只读接口
- 保护配置对象
shallowReadonly
只将对象的第一层属性变为只读(禁止修改),更深层的属性保持原样——既不会强制变为只读,也不会递归转换为响应式。
示例 作用于普通对象
const shallowReadOnlyObj = shallowReadonly({
count: 0,
name: "effectB",
age: 20,
extra: {
a: 1,
b: 2,
},
});
const handleClick = () => {
// shallowReadOnlyObj.extra 返回的是普通对象
console.log("click", shallowReadOnlyObj.extra);
};
作用于 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);
};
const handleClick = () => {
console.log("click", shallowReadOnlyObj.extra.a++);
};
获取 shallowReadOnlyObj.extra.a属性, shallowReadOnlyObj.extra是 浅层。
由于没有活跃的副作用,因此不会触发。
源码 createReactiveObject
reactive 、readonly 、shallowReactive、 shallowReadonly 都由 createReactiveObject 函数来实现。
Objact和Array的对象由 基本类型对象代理处理器实现- 集合类型(
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);
});
const handleClick = () => {
// 获取 datas.value 不会收集依赖,因为没有正在执行的副作用
// 执行push 操作,是reactive对象,会触发响应式
datas.value.push(6);
};
注意事项
- 脚本中必须使用
.value。在<script>或<script setup>中,访问或修改ref的内容时必须通过.value,除非在某些自动解包的场景(如reactive对象属性)。 - 模板中顶层 ref 自动解包。
- 解构会丢失响应性。
- 整体替换 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
- 仅监听 .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++;
};
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
}
【示例】
const projects = ref({
id: 'xx',
desc: '项目描述'
})
console.log('projects',projects,)
console.log('projects.value',projects.value)
console.log('toRaw projects.value', toRaw(projects.value))
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
将可能是 ref、computed 或 getter 函数的值统一转换为其底层的原始值。
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
}