Vue 生态(二):响应式系统 · 构建坚实的数据基石

第二章:响应式系统 · 构建坚实的数据基石

摘要: 欢迎来到 Vue 3 的引擎室。本章,我们将解构 Vue 的核心——响应式系统,它是一切动态交互的基石。我们将超越“数据变,视图也变”的表面现象,深入探讨其背后的设计哲学与工程实践。我们将从响应式系统的底层原理出发,理解其演进;接着从响应式数据的“原子” ref 开始,建立统一的数据处理范式;探索 reactive 代理模式的应用场景;最后,我们将锻造处理衍生状态与副作用的“瑞士军刀”—— computedwatch。本章将以 TypeScript 作为我们的“蓝图语言”,让你从一开始就以构建健壮、可预测应用的角度,去思考和掌控数据。


2.1. 响应式原理:从 Object.definePropertyProxy 的革命

在深入 API 之前,理解其“为何如此”至关重要。Vue 的响应式系统并非凭空而来,而是经历了一次从 Vue 2 到 Vue 3 的重大架构升级。

2.1.1. Vue 2 的基石:Object.defineProperty

Vue 2 的响应式系统依赖于 ES5 的 Object.defineProperty。它通过遍历一个对象的所有属性,并为每个属性设置 gettersetter 来实现数据劫持。

  • getter: 当你读取属性时触发,Vue 在此时进行 依赖收集 (Track)
  • setter: 当你修改属性时触发,Vue 在此时 触发更新 (Trigger)

然而,这种方式存在一些天然的、无法绕过的局限性:

  1. 无法侦测新增属性: 对于一个已经响应化的对象,后续动态添加的新属性不会被 defineProperty 劫持,因此不是响应式的。Vue 2 必须通过 Vue.set (或 this.$set) 这一特殊 API 来解决。
  2. 无法侦测数组索引和长度的变化: 直接通过索引修改数组项 (arr[0] = ...) 或修改数组长度 (arr.length = 0) 无法被 setter 捕获。Vue 2 不得不重写数组的 push, pop, shift 等方法来“曲线救国”。

2.1.2. Vue 3 的引擎:ES6 Proxy

为了从根本上解决这些问题,Vue 3 拥抱了 ES6 的 ProxyProxy 是一种元编程能力,它允许我们创建一个对象的“代理”,从而可以拦截并自定义该对象上的各种操作。

特性对比Object.defineProperty (Vue 2)Proxy (Vue 3)
拦截目标对象的 属性整个对象
拦截能力只能拦截 getset 等少数操作可拦截多达 13 种操作 (如 get, set, has, deleteProperty)
新增属性无法 自动侦测,需 Vue.set可以,代理是针对整个对象的
数组操作无法 侦测索引赋值,需重写数组方法可以getset 拦截器能处理索引访问
性能初始化时需 深度递归 遍历所有属性惰性初始化,仅在访问属性时才进行代理

Proxy 的引入,让 Vue 3 的响应式系统变得更加 完整、强大且高效,开发者不再需要记忆那些特例和边界情况。


2.2. ref:响应式数据的“原子”与首选范式

在 Vue 3 中,我们有两种创建响应式状态的方式。但在构建大型、可维护的应用时,我们必须建立一套统一、可预测的范式。因此,我们提出本指南的第一个核心架构原则:“万物皆 ref (Ref-First)”

这意味着,无论我们处理的是原始值还是对象,我们都 优先使用 ref 来包裹它。reactive 则作为处理特定场景(如大型、非替换性的聚合状态)的补充。我们将在后续内容中揭示为何这一原则能从根本上提升代码的健壮性和一致性。

在深入理论之前,我们先通过一个实验来感受“非响应式”世界的痛点。请修改 src/App.vue,尝试用原生 JavaScript 变量实现一个计数器。

文件路径: src/App.vue (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup lang="ts">
// 一个普通的 JavaScript 变量
let count = 0;

function increment() {
count++;
console.log('变量 count 的值:', count);
}
</script>

<template>
<button @click="increment">
Count is: {{ count }}
</button>
</template>

你会发现,无论你如何点击按钮,控制台里 count 的值确实在增加,但页面上的 “Count is: 0” 却 纹丝不动

2.2.1. 核心痛点:为何需要“信号”——从 JavaScript 变量到响应式状态

这就是问题的根源。对于 let count = 0,JavaScript 引擎对 count 的修改是“静默”的。Vue 的渲染系统对此一无所知,因为它没有收到任何需要更新视图的“信号”。

我们需要一种机制,将一个普通变量包装成一个“响应式信源”。当这个信源的值发生变化时,它能主动向 Vue 的调度系统发出通知:“我已变更,请更新所有订阅我的部分!” 这个信源,就是 Vue 提供的 ref

2.2.2. 创建响应式引用:ref 的基本用法

现在,我们用 ref 来赋予数据“生命”。

文件路径: src/App.vue (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup lang="ts">
// 1. 从 'vue' 中导入 ref 函数
import { ref } from 'vue';

// 2. 使用 ref() 将 0 “包裹”成一个响应式引用对象
const count = ref(0);

function increment() {
// 3. 必须通过 .value 属性来访问和修改其内部值
count.value++;
console.log('ref count 的 .value 是:', count.value);
}
</script>

<template>

<button @click="increment">
<!-- 模板语法糖:在 <template> 中,Vue 编译器会自动为我们“解包”。当你写 {{ count }} 时,它在编译时已被智能地转换为了
{{ count.value }},这是为了提升开发体验,也是为了遵循最佳规范,不要在 template 中写过多的变量嵌套引用。 -->
Count is: {{ count }}
</button>
</template>

点击按钮,页面与控制台同步更新。魔法发生了。


2.2.3. 封装与意图:.value 背后的设计哲学

为什么必须通过 .value?这并非繁琐,而是一种精妙的设计。const count = ref(0) 创建的 count 并非数字 0 本身,而是一个包含了值的 容器对象Ref 对象)。这个容器是实现响应性的关键。

  • 读取值 (count.value): 当你访问 .value 时,Vue 底层的 getter 被触发。它会记录下来:“哦,当前这个渲染上下文 依赖count 这个信源。” (此为 依赖收集/Track)。
  • 修改值 (count.value++): 当你修改 .value 时,Vue 底层的 setter 被触发。它检测到这个动作,并说:“count 信源已更新,我要 通知 所有依赖它的地方进行更新。” (此为 触发更新/Trigger)。

.value 的存在,使得对值的 读写操作 都成为可被拦截的“钩子”。它让每一次数据访问和修改都变得 意图明确,并且为 Vue 提供了执行其响应式魔法的必要入口。

2.2.4. (TS 实战) 类型即契约:用 Ref<T> 为数据“立规矩”

在专业开发中,我们必须为数据建立严格的契约。TypeScript 的泛型是实现这一点的利器,在 TypeScript 的世界里,你将频繁遇到像 <T> 这样的尖括号语法,它被称为 泛型。你可以把它想象成一个“类型的变量”或“类型占位符”。当我们写 Ref<number> 时,就是在告诉 TypeScript:“我创建的这个 ref 是一个专门用来装 number 类型数据的容器。” 这个 <T> 赋予了我们创造灵活且类型安全的“模具”的能力,是 TypeScript 中最强大的特性之一。在本指南中,我们将通过一个个实战场景,让你逐步领会它的妙用。

文件路径: src/App.vue (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup lang="ts">
import { ref } from 'vue';
// 显式导入 Ref 类型,是良好的编码习惯
import type { Ref } from 'vue';

// TS 会从初始值 `0` 推断出 count 的类型是 Ref<number>
const count = ref(0);

// 我们可以显式地注解,这在定义复杂或联合类型时至关重要
const message: Ref<string> = ref('Hello, Prorise!');

// 场景:一个可能存在的用户信息,初始为 null
// 明确定义了 user 这个“容器”只能装 `string` 或 `null`
const user = ref<string | null>(null);

// 任何违反类型契约的行为,都会在编码阶段被 TypeScript 捕获
// message.value = 123; // 错误:不能将类型“number”分配给类型“string”。
</script>

使用 Ref<T> 泛型进行类型注解,是 Vue + TypeScript 项目的 黄金标准。它将 TypeScript 的静态类型检查能力与 Vue 的响应式系统无缝结合,让你的代码在运行前就具备极高的健壮性。


2.3. reactive:对象整体响应式的“代理”模式

当处理复杂的、拥有多个属性的 JavaScript 对象时(例如表单状态),为每个属性都创建一个 ref 会显得冗长。为此,Vue 提供了 reactive,它采用了一种不同的策略。

2.3.1. 创建响应式代理:reactive 的基本用法

reactive 接收一个对象或数组,并返回其基于 Proxy深度响应式 版本。

文件路径: src/App.vue (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script setup lang="ts">
import { reactive } from 'vue';

// 1. 使用 reactive 创建一个响应式代理对象
const state = reactive({
user: 'Prorise',
isLoggedIn: false,
permissions: ['read', 'write'],
});

function login() {
// 2. 直接像操作普通对象一样修改属性,无需 .value
state.isLoggedIn = true;
state.permissions.push('delete'); // 深度响应:对数组的修改也能被侦测到
}
</script>

<template>
<div>
<p v-if="state.isLoggedIn">Welcome, {{ state.user }}!</p>
<p v-else>Please log in.</p>
<button @click="login">Login</button>
<p>Permissions: {{ state.permissions.join(', ') }}</p>
</div>
</template>

ref 的核心差异与设计权衡:

  • 访问方式: reactive 返回的是代理对象本身,操作直观,无需 .value
  • 适用类型: reactive 只能 接收对象或数组作为参数。reactive(0) 是无效的。
  • 赋值替换: 你不能直接替换整个 reactive 对象,否则会使其与原始的响应式连接断开。例如 state = { ... } 是错误的,而 ref 可以通过 count.value = ... 整体替换。

2.3.2. (TS 实战) 定义数据蓝图:interfacetype 的力量

对于 reactive 对象,使用 interfacetype 来定义其结构,是保障代码质量的基石。

文件路径: src/App.vue (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<script setup lang="ts">
import { reactive } from 'vue';

// 1. 使用 interface 定义一个用户状态对象的“蓝图”
interface UserState {
name: string;
age: number;
isOnline: boolean;
hobbies?: string[]; // '?' 代表该属性是可选的
}

// 2. 将 UserState 作为类型注解应用到 reactive 对象上
const user: UserState = reactive({
name: 'Prorise',
age: 3,
isOnline: false,
});

function addHobby(hobby: string) {
// TS 能够理解可选链和类型收窄
if (!user.hobbies) {
user.hobbies = [];
}
user.hobbies.push(hobby);
}

// 尝试访问一个不存在的属性,TS 会在编译时就发出警告
// console.log(user.email); // 错误:类型“UserState”上不存在属性“email”。
</script>

通过 interface,我们为 user 对象建立了一个严格的“契约”。这为我们带来了三大好处:精准的智能提示编译时的类型检查,以及 代码即文档 的可读性。

2.3.3. 响应性的“黑洞”:解构 reactive 的陷阱与原理

reactive 的魔法完全依赖于那个“代理”对象。一个常见的、致命的错误是使用 ES6 解构赋值。

1
2
3
4
5
6
7
8
9
10
11
import { reactive } from 'vue';

const state = reactive({ count: 0, message: 'hello' });

// 致命错误!
let { count, message } = state;

count++; // 这里的 count 只是一个值为 0 的普通 number,与代理无关
message = 'world'; // 这里的 message 也只是一个普通 string

// state.count 和 state.message 的值纹丝不动,视图绝不会更新

核心原理:解构赋值 let { count } = state 实质上是 let count = state.count。这个操作直接获取了代理对象内部的 原始值,从而绕过了代理的 getset 拦截器,导致得到的变量与响应式系统 完全断开连接

那么,如果我们需要将响应式对象的属性作为独立的、可传递的单元,同时保持其响应性,该怎么办?

2.4. toRefs & toRef:保持响应性连接的桥梁

toRefs 是解决上述问题的标准工具。它能够将一个 reactive 对象的所有属性,转换为一个普通对象,但该对象的每个属性都是一个指向原始对象相应属性的 ref

文件路径: src/App.vue (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<script setup lang="ts">
import { reactive, toRefs } from 'vue';

const state = reactive({
count: 0,
message: 'Hello',
});

// 使用 toRefs 安全地解构
// count 现在是 Ref<number>
// message 现在是 Ref<string>
const { count, message } = toRefs(state);

function increment() {
// 因为 count 是一个 ref,所以需要通过 .value 来操作
count.value++;
}

function updateMessage() {
message.value = 'Hello Prorise!';
}

// 即使操作的是解构后的 ref,原始的 state 也会同步更新
// watch(() => state.count, (newVal) => console.log('State count changed:', newVal))
</script>

<template>
<div>
<p>Count: {{ count }}</p>
<p>Message: {{ message }}</p>
<button @click="increment">Increment Count</button>
<button @click="updateMessage">Update Message</button>
</div>
</template>

toRefs 是编写可组合函数 (Composables) 时的关键工具,它确保了从函数中返回的响应式状态在被外部解构使用时,依然能保持与源状态的连接。toRef 则是它的单数形式,用于为单个属性创建 ref

2.5. 衍生与响应:computedwatch 的应用场景

除了直接定义和修改状态,我们还需要更高级的工具来处理两种核心场景:

  1. 衍生状态:一个状态的值由其他状态计算而来 (computed)
  2. 副作用:当状态变化时,需要执行一些额外的逻辑,如 API 请求、DOM 操作等(watch)

2.5.1. computed:智能的衍生状态与缓存之美

computed 用于创建一个计算属性 ref。其核心价值在于它的 声明性缓存机制

文件路径: src/App.vue (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup lang="ts">
import { ref, computed } from 'vue';
import type { ComputedRef } from 'vue';

const firstName = ref('John');
const lastName = ref('Doe');

// 创建一个计算属性 fullName,其类型为 ComputedRef<string>
const fullName: ComputedRef<string> = computed(() => {
// 这个计算函数只有在 firstName 或 lastName 变化时才会重新执行
console.log('Computing full name...');
return `${firstName.value} ${lastName.value}`;
});
</script>

<template>
<div>
<input v-model="firstName" placeholder="First Name" />
<input v-model="lastName" placeholder="Last Name" />
<p>Full Name: {{ fullName }}</p>
<p>Full Name again: {{ fullName }}</p> <!-- 不会触发重新计算 -->
</div>
</template>

在模板中多次访问 fullName,控制台的 “Computing…” 只会打印一次。这是因为 computed 会缓存其结果,只有当其依赖项(firstNamelastName)发生变化时,缓存才会失效并重新计算。这是一种重要的性能优化手段,也让模板代码更具可读性。


2.5.2. watchwatchEffect:洞察数据变化的“哨兵”

当需要在数据变化时执行具有副作用的操作时,侦听器是我们的不二之选。

watch:精确制导的侦听器

watch 是一个 精确制导 的侦听器,你需要明确指定侦听的目标、选项和回调。

特点:

  • 懒执行:默认情况下,回调只在侦听源变化后执行。
  • 目标明确:必须显式指定要侦听的一个或多个数据源。
  • 访问新旧值:回调函数能接收到变化前后的值,便于进行比较或逻辑判断。
  • 更强的控制力:通过 deep (深度侦听) 和 immediate (立即执行) 选项进行精细控制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<script setup lang="ts">
import { ref, watch, reactive, type Ref } from 'vue';

const question: Ref<string> = ref('');
const answer: Ref<string> = ref('问题需要包含一个问号 (?)');
const formState = reactive({ search: '' });

// 1. 精确侦听 `question` 这个 ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) {
answer.value = 'Thinking...';
await new Promise(r => setTimeout(r, 1500)); // 模拟API
answer.value = '我无法回答这个问题。';
}
});

// 2. 侦听 reactive 对象的属性,需要使用 getter 函数
watch(() => formState.search, (newSearch) => {
console.log(`Search term is now: ${newSearch}`);
});

// 3. 使用 immediate 选项,让侦听器在创建时就立即执行一次
watch(question, (val) => {
console.log(`Question is: ${val} (logged immediately)`);
}, { immediate: true });
</script>
<template>
<div>
<p>问一个 yes/no 问题: <input v-model="question" /></p>
<p>{{ answer }}</p>
</div>
</template>

watchEffect:自动追踪的侦听器

watchEffect 是一个 自动追踪 的侦听器,它会响应其“势力范围”内任何依赖的变化。

特点:

  • 立即执行:创建时会立即执行一次回调,以进行依赖收集。
  • 自动追踪:无需指定侦听源,它会自动将回调函数中用到的所有响应式数据作为依赖。
  • 更简洁:适用于不关心旧值,只关心“当依赖变化时重新运行这段逻辑”的场景。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script setup lang="ts">
import { ref, watchEffect, type Ref } from 'vue';

interface UserData { name: string; }

const userId: Ref<number> = ref(1);
const userData: Ref<UserData | undefined> = ref();

// 立即执行,并自动将 userId 作为依赖
watchEffect(async () => {
// 当 userId.value 变化时,这个匿名函数会重新执行
console.log(`获取新用户: ${userId.value}`);
userData.value = undefined; // 清空旧数据
// 模拟 API 请求
const response = await new Promise(r => setTimeout(r, 500, { name: `User ${userId.value}` }));
// 类型断言:添加UserData可以保证类型安全
userData.value = response as UserData;
});
</script>
<template>
<div>
<button @click="userId++">获取下一个用户 (ID: {{ userId }})</button>
<p v-if="userData">用户数据: {{ userData.name }}</p>
<p v-else>Loading...</p>
</div>
</template>

2.6. 响应式工具与性能优化

除了核心 API,Vue 还提供了一些工具来应对特定场景,尤其是在性能优化方面。

2.6.1. readonly:创建不可变的响应式状态

readonly 接收一个响应式对象(由 refreactive 创建)并返回一个只读的代理。任何对其的修改尝试都会在开发环境下产生警告。

1
2
3
4
5
6
7
8
import { reactive, readonly } from 'vue';

const original = reactive({ count: 0 });
const copy = readonly(original);

original.count++; // OK

// copy.count++; // 运行时警告: Set operation on key "count" failed: target is readonly.

核心用途:在父组件中,当你需要向子组件传递一个响应式状态,但又不希望子组件意外修改它时,readonly 是实现单向数据流的绝佳工具。

2.6.2. shallowRef & shallowReactive:性能优化的利器

默认情况下,refreactive 都是“深度”响应的。如果你有一个层级很深、数据量庞大的对象,对其所有属性进行深度代理可能会带来不必要的性能开销,这个用的比较少,简单了解一下即可

  • shallowRef: 只对 .value赋值 操作是响应式的。它不会自动将 .value 的内容转换为深度响应式对象。
  • shallowReactive: 只对对象的 第一层属性 是响应式的。嵌套对象内部的属性变化不会被追踪。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { shallowReactive, shallowRef, triggerRef } from 'vue';

// shallowRef 示例
const state = shallowRef({ nested: { count: 0 } });

// 不会触发更新,因为只修改了嵌套对象的属性
state.value.nested.count++;

// 会触发更新,因为替换了整个 .value
state.value = { nested: { count: 1 } };
// 如果确实需要强制更新,可以使用 triggerRef(state);

// shallowReactive 示例
const shallowState = shallowReactive({
foo: 1,
nested: { bar: 2 }
});

// 会触发更新
shallowState.foo++;

// 不会触发更新
shallowState.nested.bar++;

核心用途:当处理大型、不可变的数据结构(如从后端获取的复杂列表),或者需要手动控制更新时机以进行极致性能优化时,应考虑使用 shallow API。


2.7. 响应式工具箱:深入底层的“诊断”与“手术”工具

除了构建响应式数据的核心 API,Vue 的响应式模块还为我们提供了一套如同“精密仪器”般的工具函数。它们允许我们对响应式对象进行精确的 诊断(例如,判断其类型),甚至执行一些深入底层的 “手术”(例如,获取原始数据或阻止某个对象被代理)。掌握这些工具,能让你在处理复杂状态、集成第三方库或进行极致性能优化时游刃有余。


2.7.1. 诊断工具:isRefisProxy

在编写通用的组合式函数或工具函数时,我们常常需要根据传入参数的“身份”来决定不同的处理逻辑。

  • isRef(value): 精确地判断一个值是否为 ref 对象。这在你想编写一个既能接受 ref 又能接受普通值的函数时非常有用。

  • isProxy(value): 检查一个对象是否是由 reactivereadonly 创建的代理。这有助于我们识别一个对象是否已经被 Vue 的响应式系统所“接管”。

1
2
3
4
5
6
7
8
9
10
11
12
13
import { ref, reactive, readonly, isRef, isProxy } from 'vue';

const count = ref(0);
const state = reactive({ name: 'Prorise' });
const readOnlyState = readonly(state);
const plainObject = { id: 1 };

console.log(isRef(count)); // -> true
console.log(isRef(state.name)); // -> false (reactive 对象的属性是原始值)

console.log(isProxy(state)); // -> true
console.log(isProxy(readOnlyState));// -> true
console.log(isProxy(plainObject)); // -> false

2.7.2. 解包与转换:unreftoRaw

这两组工具允许我们“穿透”响应式包装,获取其内部的原始数据。

  • unref(value): 这是一个非常实用的“语法糖”。如果参数是一个 ref,它会返回其 .value;如果参数不是 ref,它会直接返回参数本身。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import { ref, unref } from 'vue';

    const countRef = ref(10);
    const normalNumber = 20;

    // 假设有一个工具函数,它需要的是原始值
    function processValue(value: number) {
    console.log(value * 2);
    }

    // 使用 unref,无需关心传入的是否是 ref
    processValue(unref(countRef)); // -> 20
    processValue(unref(normalNumber)); // -> 40

    unref 是编写 MaybeRefOrGetter 模式(我们在第四章会深入探讨)时的核心工具之一,它极大地提升了函数的灵活性。

  • toRaw(proxy): 这是一个更深入的“逃生舱”机制。它会返回由 reactivereadonly 创建的代理对象的 原始目标对象

    核心使用场景: 当你需要对一个响应式对象进行某些操作,但又不希望这些操作被 Vue 的响应式系统追踪(即不触发读/写依赖)时,toRaw 是你的选择。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import { reactive, toRaw } from 'vue';

    const state = reactive({ count: 0 });
    const rawState = toRaw(state);

    // 直接操作原始对象,不会触发任何更新
    rawState.count++;

    console.log(state.count); // -> 1 (数据确实变了)
    // 但是,任何依赖 state.count 的视图或 watch 都不会更新!

    警告: toRaw 是一把双刃剑。请只在充分理解其后果的情况下使用它,例如在需要将响应式状态传递给一个不应触发更新的外部库时,或在某些性能敏感的读取操作中。常规业务开发中应极力避免使用 toRaw


2.7.3. 性能优化与边界控制:markRaw

markRaw(object) 是一个强大的性能优化工具,它的作用是:将一个对象显式地标记为“永远不会”被转换为代理。Vue 的响应式系统在遇到被 markRaw 标记过的对象时,会直接跳过它。

核心使用场景:

  1. 嵌入不可变数据: 当你有一个非常大的、层级很深且不会改变的数据结构(例如,从后端获取的静态配置),将其 markRaw 可以避免 Vue 在初始化时对其进行深度代理,从而节省大量的性能开销。
  2. 集成第三方库: 许多第三方库的实例(如 ECharts 实例、地图库实例)内部有复杂的状态和方法,不应该被 Vue 代理。将它们 markRaw 后再存入 refreactive 是标准做法。
  3. 避免组件代理: Vue 组件对象本身也不应被代理。虽然 Vue 内部有处理,但手动 markRaw 是更保险的做法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { reactive, markRaw, isProxy } from 'vue';

// 假设这是一个复杂的、不应被代理的第三方库实例
class ThirdPartyLibrary {
// ...
}
const libInstance = new ThirdPartyLibrary();

const state = reactive({
id: 1,
// 直接嵌入,libInstance 会被代理
// lib: libInstance,

// 使用 markRaw,libInstance 将保持其原始状态
lib: markRaw(libInstance)
});

console.log(isProxy(state.lib)); // -> false

通过使用 markRaw,我们向 Vue 明确传达了我们的意图:“这部分数据,请你不要插手”,从而实现了更精细的性能控制和更安全的外部库集成。