Vue 3 响应式基础全面教学:从核心到进阶全面解析

欢迎来到这篇关于 Vue 3 响应式系统的全面教学文档!作为一名新手,你可能会觉得 Vue 的响应式系统(refreactive)有点复杂,但别担心!这篇文档将以通俗易懂的方式,结合详细的解释和实际代码示例,带你一步步掌握 Vue 3 的所有响应式 API。我们会从基础到高级,覆盖 refreactive 及其相关工具函数,确保内容全面、准确,并且适合初学者理解。

我们会按照以下结构来讲解:

  1. 响应式系统的核心概念:帮你理解 Vue 3 响应式系统的基本原理。
  2. 核心 API:详细讲解 refreactive 的用法。
  3. 工具函数:覆盖 isRefunreftoReftoRefs 等实用工具。
  4. 高级 API:深入探讨 shallowReftriggerRefcustomRef 等高级功能。
  5. 注意事项与常见问题:帮助新手避坑。
  6. 官方文档与参考资料:提供最新资源链接。

每部分都会包含:

  • 通俗解释:用生活化的比喻解释概念。
  • 代码示例:真实的、可运行的代码片段。
  • 注意事项:新手容易犯错的地方和最佳实践。

准备好了吗?让我们开始吧!


一、Vue 3 响应式系统的核心概念

1. 什么是响应式?

想象你有一个笔记本,上面记录了你的每日开销。当你添加一笔新的开销时,笔记本会“自动”更新你的总支出,并且页面上的数字也会实时变化。这就是 Vue 的 响应式:当数据发生变化时,界面会自动更新,而不需要你手动操作 DOM。

Vue 3 的响应式系统基于 Proxy(代理)和 Ref 两种机制:

  • ref:用于处理基本数据类型(如数字、字符串)或简单对象,包装成一个响应式对象。
  • reactive:用于处理复杂对象(如嵌套的对象或数组),让整个对象变成响应式的。

2. 为什么需要 refreactive

在 Vue 2 中,响应式是通过 Object.defineProperty 实现的,但它有局限性,比如无法检测对象属性的添加或删除。Vue 3 引入了 Proxy,让响应式系统更强大,同时提供了 refreactive 两种方式来满足不同场景的需求:

  • ref:适合单个值的响应式管理,简单直观。
  • reactive:适合复杂数据结构的响应式管理,比如多层嵌套的对象。

二、核心 API 详解

1. ref:响应式基本值

通俗解释

ref 就像一个魔法盒子,里面装着一个值(可以是数字、字符串、对象等)。你通过 .value 访问或修改盒子里的内容,当内容变化时,Vue 会自动通知界面更新。

用法

  • 创建:通过 ref 函数创建一个响应式引用。
  • 访问/修改:使用 .value 获取或设置值。
  • 适用场景:适合简单数据,如计数器、输入框的值等。

示例代码

javascript
import { ref } from 'vue';

export default {
  setup() {
    // 创建一个响应式的计数器
    const count = ref(0);

    // 定义一个增加计数器的方法
    const increment = () => {
      count.value++; // 修改 .value,触发界面更新
      console.log('当前计数:', count.value);
    };

    return { count, increment };
  }
};
html
<template>
  <div>
    <p>计数:{{ count }}</p>
    <button @click="increment">加 1</button>
  </div>
</template>

运行结果

  • 页面显示:计数:0
  • 点击按钮后,count 增加,页面自动更新为 计数:1计数:2 等。

注意事项

  1. 必须通过 .value 访问:在 JavaScript 代码中,ref 是一个对象,值存储在 .value 属性中。
  2. 模板中无需 .value:在 Vue 模板中,Vue 会自动解包 ref,直接写 即可。
  3. 适合基本类型ref 常用于数字、字符串等简单数据。如果是对象,建议考虑 reactive

2. reactive:响应式对象

通俗解释

reactive 就像一个智能文件夹,里面可以装很多文件(属性)。当你修改文件夹里的任何文件时,Vue 会自动感知并更新界面。

用法

  • 创建:通过 reactive 函数将对象变为响应式。
  • 访问/修改:直接操作对象的属性,无需 .value
  • 适用场景:适合复杂数据结构,如嵌套对象或数组。

示例代码

javascript
import { reactive } from 'vue';

export default {
  setup() {
    // 创建一个响应式的用户信息对象
    const user = reactive({
      name: '小明',
      age: 20
    });

    // 修改用户信息的函数
    const updateUser = () => {
      user.age++; // 直接修改属性,触发界面更新
      user.name = '小红';
    };

    return { user, updateUser };
  }
};
html
<template>
  <div>
    <p>姓名:{{ user.name }}</p>
    <p>年龄:{{ user.age }}</p>
    <button @click="updateUser">更新用户信息</button>
  </div>
</template>

运行结果

  • 初始显示:姓名:小明,年龄:20
  • 点击按钮后,更新为:姓名:小红,年龄:21

注意事项

  1. 只能用于对象reactive 不支持基本类型(如数字、字符串),这些需要用 ref
  2. 直接操作属性:不需要 .value,直接用 user.name 访问或修改。
  3. 不能重新赋值:不能用 user = { ... } 替换整个对象,否则会丢失响应式。需要修改已有属性或使用 Object.assign

三、工具函数详解

Vue 3 提供了一系列工具函数来增强 refreactive 的使用,下面逐一讲解。

1. isRef:判断是否为 ref

通俗解释

isRef 就像一个鉴定师,告诉你某个变量是不是一个 ref 魔法盒子。

用法

  • 作用:检查一个值是否为 ref 对象。
  • 返回值true(是 ref)或 false(不是 ref)。

示例代码

javascript
import { ref, reactive, isRef } from 'vue';

export default {
  setup() {
    const count = ref(0);
    const user = reactive({ name: '小明' });
    const normalValue = 42;

    console.log(isRef(count)); // true
    console.log(isRef(user)); // false
    console.log(isRef(normalValue)); // false

    return { count };
  }
};

注意事项

  • 用途:常用于调试或需要动态处理不同类型数据时。
  • 局限性:只检测 ref,不会检测 reactive

2. unref:获取 ref 的值

通俗解释

unref 就像打开魔法盒子,直接取出里面的值。如果不是 ref,就返回原值。

用法

  • 作用:如果传入的是 ref,返回其 .value;否则返回原值。
  • 适用场景:需要统一处理 ref 和非 ref 值时。

示例代码

javascript
import { ref, unref } from 'vue';

export default {
  setup() {
    const count = ref(10);
    const normalValue = 20;

    console.log(unref(count)); // 10
    console.log(unref(normalValue)); // 20

    // 动态处理函数
    const getValue = (val) => {
      return unref(val); // 自动解包 ref
    };

    console.log(getValue(count)); // 10
    console.log(getValue(normalValue)); // 20

    return { count };
  }
};

注意事项

  • 等价于 toValue:在 Vue 3.3+ 中,unreftoValue 的别名,功能完全相同。
  • 简化代码:避免手动判断是否为 ref 再用 .value

3. toRef:将对象属性转为 ref

通俗解释

toRef 就像从一个文件夹(reactive 对象)里拿出一页纸,单独装进一个魔法盒子(ref),但这页纸仍然与文件夹保持同步。

用法

  • 作用:从 reactive 对象的属性创建一个 ref,保持响应式连接。
  • 适用场景:需要单独操作对象的一个属性,但仍希望它与原对象同步。

示例代码

javascript
import { reactive, toRef } from 'vue';

export default {
  setup() {
    const user = reactive({
      name: '小明',
      age: 20
    });

    // 将 user.name 转为 ref
    const nameRef = toRef(user, 'name');

    // 修改 nameRef,user.name 也会同步变化
    const updateName = () => {
      nameRef.value = '小红';
      console.log(user.name); // 小红
    };

    // 修改 user.name,nameRef 也会同步变化
    user.name = '小刚';
    console.log(nameRef.value); // 小刚

    return { nameRef, updateName };
  }
};
html
<template>
  <div>
    <p>姓名:{{ nameRef }}</p>
    <button @click="updateName">更改姓名</button>
  </div>
</template>

注意事项

  1. 保持同步toRef 创建的 ref 与原 reactive 对象的属性是双向绑定的。
  2. 必须存在属性toRef(user, 'name') 要求 user 中有 name 属性,否则会报错。
  3. 性能优化:适合需要单独传递某个属性到子组件或函数时使用。

4. toRefs:将对象所有属性转为 ref

通俗解释

toRefs 就像把整个文件夹(reactive 对象)里的每一页纸都装进单独的魔法盒子(ref),但每个盒子仍然与文件夹保持同步。

用法

  • 作用:将 reactive 对象的每个属性转为单独的 ref
  • 适用场景:需要将 reactive 对象的属性解构后仍保持响应式。

示例代码

javascript
import { reactive, toRefs } from 'vue';

export default {
  setup() {
    const user = reactive({
      name: '小明',
      age: 20
    });

    // 将 user 的所有属性转为 ref
    const { name, age } = toRefs(user);

    // 修改 name,user.name 也会变化
    name.value = '小红';
    console.log(user.name); // 小红

    // 修改 user.age,age 也会变化
    user.age = 21;
    console.log(age.value); // 21

    return { name, age };
  }
};
html
<template>
  <div>
    <p>姓名:{{ name }}</p>
    <p>年龄:{{ age }}</p>
  </div>
</template>

注意事项

  1. 解构保持响应式toRefs 的核心作用是让 reactive 对象的属性在解构后仍然保持响应式。如果直接解构 const { name, age } = usernameage 会变成普通值,失去响应式。
  2. 批量转换toRefs 会将对象的所有可枚举属性转为 ref,包括动态添加的属性。
  3. 性能考虑:如果对象属性非常多,toRefs 会为每个属性创建一个 ref,可能增加内存开销,谨慎用于大型对象。
  4. toRef 的区别toRef 只针对单个属性,而 toRefs 针对整个对象,适合需要将所有属性传递给子组件或函数的场景。

5. toValue:统一获取值

通俗解释

toValue 就像一个“万能开箱器”,不管你给它的是 ref、函数、还是普通值,它都能帮你取出最终的值。如果是 ref,它返回 .value;如果是函数,它调用函数并返回结果;如果是普通值,就直接返回。

用法

  • 作用:统一处理 ref、函数或普通值,获取其值。
  • 适用场景:当你不确定传入的值是 ref、函数还是普通值,但需要一个确定的值时。
  • 注意toValue 是 Vue 3.3+ 引入的新 API,与 unref 功能类似,但更强大,因为它还能处理函数。

示例代码

javascript
import { ref, toValue } from 'vue';

export default {
  setup() {
    const count = ref(10);
    const normalValue = 20;
    const getValue = () => 30;

    // 使用 toValue 获取值
    console.log(toValue(count)); // 10
    console.log(toValue(normalValue)); // 20
    console.log(toValue(getValue)); // 30

    // 统一处理值的函数
    const displayValue = (val) => {
      console.log('值是:', toValue(val));
    };

    displayValue(count); // 值是:10
    displayValue(normalValue); // 值是:20
    displayValue(getValue); // 值是:30

    return { count };
  }
};
html
<template>
  <div>
    <p>计数:{{ count }}</p>
  </div>
</template>

注意事项

  1. unref 的区别unref 只处理 ref 和普通值,而 toValue 额外支持函数,推荐在 Vue 3.3+ 中优先使用 toValue
  2. 函数调用:如果传入的是函数,toValue 会执行函数,注意函数内部可能有副作用(如修改状态)。
  3. 性能优化:在需要统一处理多种类型数据的场景中,toValue 能简化代码逻辑。

四、高级 API 详解

1. shallowRef:浅层响应式引用

通俗解释

shallowRef 就像一个“只看表面”的魔法盒子,只有盒子里的直接值(.value)变化时才会触发更新。如果盒子里装的是对象,对象内部的属性变化不会触发响应式。

用法

  • 作用:创建一个浅层响应式 ref,只对 .value 的直接赋值操作响应。
  • 适用场景:适合需要优化性能的场景,比如处理大型对象或只需要监控顶层值的变化。

示例代码

javascript
import { shallowRef } from 'vue';

export default {
  setup() {
    // 创建一个 shallowRef,初始值是一个对象
    const state = shallowRef({
      name: '小明',
      age: 20
    });

    // 修改对象内部属性(不会触发更新)
    const updateInner = () => {
      state.value.name = '小红'; // 不会触发界面更新
      console.log('内部更新:', state.value);
    };

    // 替换整个对象(会触发更新)
    const updateOuter = () => {
      state.value = { name: '小刚', age: 21 }; // 触发界面更新
      console.log('整体替换:', state.value);
    };

    return { state, updateInner, updateOuter };
  }
};
html
<template>
  <div>
    <p>姓名:{{ state.name }}</p>
    <p>年龄:{{ state.age }}</p>
    <button @click="updateInner">修改内部</button>
    <button @click="updateOuter">替换整体</button>
  </div>
</template>

运行结果

  • 初始显示:姓名:小明,年龄:20
  • 点击“修改内部”:控制台打印更新,但界面不变化。
  • 点击“替换整体”:界面更新为 姓名:小刚,年龄:21

注意事项

  1. 仅监控 .valueshallowRef 只对 .value 的直接赋值(如 state.value = ...)触发响应式。
  2. 性能优化:适合处理大数据量对象,避免深层响应式带来的性能开销。
  3. 局限性:不适合需要监控对象内部属性变化的场景,这种情况应使用 refreactive

2. triggerRef:手动触发更新

通俗解释

triggerRef 就像一个“手动刷新按钮”,当你用 shallowRef 且修改了内部对象属性(默认不触发更新)时,可以用 triggerRef 强制通知 Vue 更新界面。

用法

  • 作用:手动触发 shallowRef 的响应式更新。
  • 适用场景:结合 shallowRef 使用,当内部属性变化需要强制更新界面时。

示例代码

javascript
import { shallowRef, triggerRef } from 'vue';

export default {
  setup() {
    const state = shallowRef({
      name: '小明',
      age: 20
    });

    // 修改内部属性并手动触发更新
    const updateInnerWithTrigger = () => {
      state.value.name = '小红'; // 默认不触发更新
      triggerRef(state); // 手动触发更新
      console.log('触发更新:', state.value);
    };

    return { state, updateInnerWithTrigger };
  }
};
html
<template>
  <div>
    <p>姓名:{{ state.name }}</p>
    <p>年龄:{{ state.age }}</p>
    <button @click="updateInnerWithTrigger">修改并触发</button>
  </div>
</template>

运行结果

  • 初始显示:姓名:小明,年龄:20
  • 点击按钮:界面更新为 姓名:小红,年龄:20

注意事项

  1. 仅用于 shallowReftriggerRef 主要为 shallowRef 设计,普通 ref 不需要手动触发。
  2. 谨慎使用:手动触发可能导致意外的界面更新,需确保逻辑清晰。
  3. 性能优化:适合在明确知道需要更新时减少不必要的响应式开销。

3. customRef:自定义响应式引用

通俗解释

customRef 就像一个“DIY魔法盒子”,你可以自定义盒子如何存储值、如何响应变化,甚至可以添加延迟、防抖等高级功能。

用法

  • 作用:通过工厂函数创建一个自定义的 ref,允许你控制值的获取(get)和设置(set)逻辑。
  • 适用场景:需要自定义响应式行为,比如防抖、节流、或与外部数据源同步。

示例代码(防抖输入)

javascript
import { customRef } from 'vue';

export default {
  setup() {
    // 自定义防抖 ref
    const debouncedText = customRef((track, trigger) => {
      let value = '';
      let timeout;

      return {
        get() {
          track(); // 追踪依赖
          return value;
        },
        set(newValue) {
          clearTimeout(timeout); // 清除之前的定时器
          timeout = setTimeout(() => {
            value = newValue; // 更新值
            trigger(); // 触发响应式更新
          }, 500); // 500ms 防抖
        }
      };
    });

    return { debouncedText };
  }
};
html
<template>
  <div>
    <input v-model="debouncedText" placeholder="输入内容" />
    <p>输入内容:{{ debouncedText }}</p>
  </div>
</template>

运行结果

  • 输入内容后,500ms 内连续输入不会立即更新,只有停止输入 500ms 后,界面才会显示最新值。

注意事项

  1. 必须调用 tracktrigger
    • track():在 get 中调用,告诉 Vue 追踪依赖。
    • trigger():在 set 中调用,通知 Vue 更新界面。
  2. 灵活但复杂customRef 功能强大,但逻辑复杂,适合高级场景。
  3. 性能优化:可以用来实现防抖、节流等优化,减少不必要的更新。

4. shallowReactive:浅层响应式对象

通俗解释

shallowReactive 就像一个“只管第一层”的智能文件夹。文件夹里的直接内容(顶层属性)变化会触发界面更新,但如果文件夹里还有子文件夹(嵌套对象),子文件夹里的内容变化不会触发更新。这种浅层响应式设计是为了优化性能,避免不必要的深层监听。

用法

  • 作用:创建一个浅层响应式对象,仅对对象的顶层属性变化触发响应式更新。
  • 适用场景:适合处理大型嵌套对象,只需要监控顶层属性的场景,比如配置对象或大数据量的状态管理。

示例代码

javascript
import { shallowReactive } from 'vue';

export default {
  setup() {
    // 创建一个浅层响应式对象
    const state = shallowReactive({
      user: {
        name: '小明',
        age: 20
      },
      count: 0
    });

    // 修改顶层属性(会触发更新)
    const updateTopLevel = () => {
      state.count++; // 触发界面更新
      console.log('顶层更新:', state.count);
    };

    // 修改嵌套属性(不会触发更新)
    const updateNested = () => {
      state.user.name = '小红'; // 不会触发界面更新
      console.log('嵌套更新:', state.user.name);
    };

    return { state, updateTopLevel, updateNested };
  }
};
html
<template>
  <div>
    <p>姓名:{{ state.user.name }}</p>
    <p>计数:{{ state.count }}</p>
    <button @click="updateTopLevel">更新计数</button>
    <button @click="updateNested">更新姓名</button>
  </div>
</template>

运行结果

  • 初始显示:姓名:小明,计数:0
  • 点击“更新计数”:界面更新为 计数:1姓名:小明 不变。
  • 点击“更新姓名”:控制台打印 小红,但界面不更新,姓名仍显示 小明

注意事项

  1. 仅监控顶层属性shallowReactive 只对对象的直接属性(如 state.count)变化触发响应式,嵌套对象(如 state.user.name)的变化不会触发。
  2. 性能优化:适合处理大型数据结构,避免深层 Proxy 监听的性能开销。
  3. 局限性:如果需要嵌套对象的响应式,使用 reactive 而不是 shallowReactive
  4. 不能直接替换对象:与 reactive 类似,不能用 state = { ... } 替换整个对象,否则会丢失响应式。

5. readonly:只读响应式对象

通俗解释

readonly 就像把你的智能文件夹(reactiveref)锁上,只允许查看里面的内容,但不能修改。任何尝试修改的操作都会被阻止,并抛出警告。这种只读特性非常适合保护数据,防止意外修改。

用法

  • 作用:将 refreactive 对象转为只读的响应式对象,禁止修改。
  • 适用场景:在组件间传递数据时,确保数据不被子组件或其他代码修改;或者用于状态管理的只读副本。

示例代码

javascript
import { reactive, readonly } from 'vue';

export default {
  setup() {
    // 创建一个响应式对象
    const original = reactive({
      name: '小明',
      age: 20
    });

    // 创建只读副本
    const readOnlyState = readonly(original);

    // 尝试修改只读对象
    const tryModify = () => {
      try {
        readOnlyState.name = '小红'; // 会抛出警告,修改无效
      } catch (e) {
        console.warn('无法修改只读对象!');
      }
      console.log('只读对象:', readOnlyState.name); // 仍为 小明
    };

    // 修改原始对象(会触发只读对象的更新)
    const updateOriginal = () => {
      original.name = '小刚'; // 触发界面更新
      console.log('原始对象更新:', readOnlyState.name); // 小刚
    };

    return { readOnlyState, tryModify, updateOriginal };
  }
};
html
<template>
  <div>
    <p>姓名:{{ readOnlyState.name }}</p>
    <p>年龄:{{ readOnlyState.age }}</p>
    <button @click="tryModify">尝试修改</button>
    <button @click="updateOriginal">更新原始对象</button>
  </div>
</template>

运行结果

  • 初始显示:姓名:小明,年龄:20
  • 点击“尝试修改”:控制台抛出警告,界面不变化。
  • 点击“更新原始对象”:界面更新为 姓名:小刚,年龄:20

注意事项

  1. 只读特性readonly 创建的对象无法直接修改,尝试修改会抛出警告(开发环境中)。
  2. 与原始对象同步readonly 对象是原始对象的代理,原始对象的变化会反映到只读对象上。
  3. 适用场景:常用于 Vuex/Pinia 的状态只读副本,或者防止子组件修改父组件传递的 props。
  4. 深层只读readonly 是深层的,嵌套对象的所有属性也都是只读的。

6. shallowReadonly:浅层只读响应式对象

通俗解释

shallowReadonly 就像只给智能文件夹的第一层加锁,顶层属性不能修改,但嵌套对象(子文件夹)的属性可以自由修改。它是 readonly 的浅层版本,适合需要部分保护的场景。

用法

  • 作用:创建一个浅层只读响应式对象,仅顶层属性不可修改,嵌套对象属性可修改。
  • 适用场景:需要保护顶层属性,但允许嵌套对象被修改的场景,比如配置对象的部分保护。

示例代码

javascript
import { reactive, shallowReadonly } from 'vue';

export default {
  setup() {
    // 创建一个响应式对象
    const original = reactive({
      user: {
        name: '小明',
        age: 20
      },
      count: 0
    });

    // 创建浅层只读对象
    const shallowReadOnly = shallowReadonly(original);

    // 尝试修改
    const tryModify = () => {
      try {
        shallowReadOnly.count = 1; // 抛出警告,修改无效
      } catch (e) {
        console.warn('无法修改顶层属性!');
      }
      shallowReadOnly.user.name = '小红'; // 可以修改嵌套属性
      console.log('嵌套属性更新:', shallowReadOnly.user.name); // 小红
    };

    // 修改原始对象
    const updateOriginal = () => {
      original.count++; // 触发界面更新
      console.log('原始对象更新:', shallowReadOnly.count);
    };

    return { shallowReadOnly, tryModify, updateOriginal };
  }
};
html
<template>
  <div>
    <p>姓名:{{ shallowReadOnly.user.name }}</p>
    <p>计数:{{ shallowReadOnly.count }}</p>
    <button @click="tryModify">尝试修改</button>
    <button @click="updateOriginal">更新原始对象</button>
  </div>
</template>

运行结果

  • 初始显示:姓名:小明,计数:0
  • 点击“尝试修改”:嵌套属性更新为 姓名:小红,但 count 不变,控制台抛出警告。
  • 点击“更新原始对象”:界面更新为 计数:1,姓名保持 小红

注意事项

  1. 仅顶层只读shallowReadonly 只保护顶层属性,嵌套对象属性可自由修改。
  2. 性能优化:比 readonly 更轻量,适合只需要保护顶层属性的场景。
  3. 与原始对象同步:修改原始对象的顶层或嵌套属性会反映到 shallowReadonly 对象上。
  4. 适用场景:适合需要部分只读保护的场景,比如只保护配置对象的某些字段。

7. markRaw:标记为非响应式

通俗解释

markRaw 就像给一个对象贴上“禁止响应式”的标签,告诉 Vue 不要将它转为响应式对象。不管是放在 refreactive 还是其他响应式对象中,这个对象都不会被 Proxy 包装。

用法

  • 作用:标记一个对象为非响应式,防止 Vue 自动将其转为 reactiveref
  • 适用场景:需要引入第三方库的对象(如 Three.js、Chart.js)或不需要响应式的复杂数据时,优化性能。

示例代码

javascript
import { reactive, markRaw } from 'vue';

export default {
  setup() {
    // 第三方库对象(假设)
    const thirdPartyObj = markRaw({
      data: '我是第三方数据',
      doSomething() {
        console.log('执行第三方逻辑');
      }
    });

    // 创建响应式对象
    const state = reactive({
      name: '小明',
      thirdParty: thirdPartyObj // 不会被转为响应式
    });

    // 修改属性
    const update = () => {
      state.name = '小红'; // 触发更新
      state.thirdParty.data = '新数据'; // 不会触发更新
      console.log('第三方对象:', state.thirdParty.data); // 新数据
    };

    return { state, update };
  }
};
html
<template>
  <div>
    <p>姓名:{{ state.name }}</p>
    <p>第三方数据:{{ state.thirdParty.data }}</p>
    <button @click="update">更新</button>
  </div>
</template>

运行结果

  • 初始显示:姓名:小明,第三方数据:我是第三方数据
  • 点击“更新”:界面更新为 姓名:小红,第三方数据在界面不更新(仍显示 我是第三方数据),但控制台打印 新数据

注意事项

  1. 完全非响应式markRaw 标记的对象及其所有嵌套属性都不会触发响应式更新。
  2. 性能优化:适合处理不需要响应式的大型对象或第三方库实例。
  3. 不可逆:一旦标记为 markRaw,对象无法再被转为响应式。
  4. 谨慎使用:确保确实不需要响应式,否则可能导致界面更新异常。

8. effectScope:管理响应式副作用

通俗解释

effectScope 就像一个“任务管理器”,可以把多个响应式副作用(比如 watchcomputed)组织在一起,统一控制它们的生命周期。你可以随时停止整个任务组,避免内存泄漏。

用法

  • 作用:创建一个作用域,用于收集和管理响应式副作用(effect),并提供批量停止的功能。
  • 适用场景:动态创建多个 watchcomputed,需要统一销毁时(比如动态组件或插件系统)。

示例代码

javascript
import { reactive, effectScope, watch } from 'vue';

export default {
  setup() {
    const scope = effectScope(); // 创建作用域
    const state = reactive({ count: 0 });

    // 在作用域内定义副作用
    scope.run(() => {
      watch(
        () => state.count,
        (newValue) => {
          console.log('计数变化:', newValue);
        }
      );
    });

    // 增加计数
    const increment = () => {
      state.count++;
    };

    // 停止所有副作用
    const stopEffects = () => {
      scope.stop(); // 停止作用域内所有副作用
      console.log('副作用已停止');
    };

    return { state, increment, stopEffects };
  }
};
html
<template>
  <div>
    <p>计数:{{ state.count }}</p>
    <button @click="increment">加 1</button>
    <button @click="stopEffects">停止副作用</button>
  </div>
</template>

运行结果

  • 初始显示:计数:0
  • 点击“加 1”:计数增加,控制台打印 计数变化:1计数变化:2 等。
  • 点击“停止副作用”:控制台打印 副作用已停止,后续计数变化不再触发 watch

注意事项

  1. 统一管理副作用effectScope 适合需要动态创建和销毁副作用的场景,比如动态组件或插件。
  2. 调用 scope.run:副作用必须在 scope.run 中定义,才能被作用域管理。
  3. 停止后不可恢复:调用 scope.stop() 后,作用域内的所有副作用(如 watchcomputed)都会停止,且无法重新启用。
  4. 内存管理:在组件卸载时使用 effectScope 确保清理副作用,防止内存泄漏。

9. computed:计算属性(与响应式结合)

通俗解释

computed 就像一个“智能计算器”,它根据响应式数据(refreactive)自动计算结果,并缓存结果。只有当依赖的数据变化时,它才会重新计算,非常适合需要动态计算的场景。

用法

  • 作用:创建一个基于响应式数据的计算属性,只有依赖变化时才会重新计算。
  • 适用场景:需要根据响应式数据衍生新数据的场景,比如格式化数据、计算总和等。

示例代码

javascript
import { ref, computed } from 'vue';

export default {
  setup() {
    const price = ref(100);
    const quantity = ref(2);

    // 创建计算属性:总价
    const total = computed(() => {
      return price.value * quantity.value;
    });

    // 修改价格或数量
    const update = () => {
      price.value += 10;
      quantity.value++;
    };

    return { price, quantity, total, update };
  }
};
html
<template>
  <div>
    <p>单价:{{ price }}</p>
    <p>数量:{{ quantity }}</p>
    <p>总价:{{ total }}</p>
    <button @click="update">更新</button>
  </div>
</template>

运行结果

  • 初始显示:单价:100,数量:2,总价:200
  • 点击“更新”:更新为 单价:110,数量:3,总价:330

注意事项

  1. 缓存机制computed 会缓存结果,只有当依赖(如 price.valuequantity.value)变化时才重新计算。
  2. 只读默认:默认的 computed 是只读的,尝试修改会抛出警告。
  3. 可写计算属性:可以通过提供 getset 函数创建可写的计算属性(见下文)。
  4. 性能优化:比直接在模板中计算更高效,适合复杂的计算逻辑。

可写计算属性示例

javascript
import { ref, computed } from 'vue';

export default {
  setup() {
    const firstName = ref('小');
    const lastName = ref('明');

    // 可写计算属性
    const fullName = computed({
      get() {
        return firstName.value + lastName.value;
      },
      set(newValue) {
        [firstName.value, lastName.value] = newValue.split('');
      }
    });

    // 修改全名
    const updateName = () => {
      fullName.value = '小红'; // 触发 setter
    };

    return { fullName, updateName };
  }
};
html
<template>
  <div>
    <p>全名:{{ fullName }}</p>
    <button @click="updateName">更新全名</button>
  </div>
</template>

运行结果

  • 初始显示:全名:小明
  • 点击“更新全名”:更新为 全名:小红

注意事项(可写计算属性)

  1. 提供 getsetget 返回计算值,set 定义如何根据新值更新依赖。
  2. 谨慎使用:可写计算属性可能增加代码复杂性,确保逻辑清晰。
  3. 适用场景:适合需要双向绑定的衍生数据,比如表单输入的格式化。

五、注意事项与常见问题

为了帮助新手避坑,以下总结了使用 refreactive 相关 API 时常见的错误和最佳实践:

1. 常见错误

  1. 直接解构 reactive 对象
    • 错误:const { name } = reactive({ name: '小明' }) 会导致 name 失去响应式。
    • 解决:使用 toRefsconst { name } = toRefs(reactive({ name: '小明' }))
  2. 替换整个 reactive 对象
    • 错误:state = { name: '新值' } 会破坏响应式。
    • 解决:修改属性(如 state.name = '新值')或使用 Object.assign(state, { name: '新值' })
  3. ref 中忘记 .value
    • 错误:在 setup 中直接用 count++ 而不是 count.value++
    • 解决:始终在 JavaScript 中使用 .value 访问或修改 ref 值。
  4. 误用 shallowRefshallowReactive
    • 错误:期望嵌套对象属性变化触发更新,但使用了 shallowRefshallowReactive
    • 解决:需要深层响应式时,使用 refreactive
  5. 忽略 readonly 的只读特性
    • 错误:尝试修改 readonlyshallowReadonly 的顶层属性。
    • 解决:确保只修改原始对象,或明确知道 shallowReadonly 的嵌套属性可改。

2. 最佳实践

  1. 选择合适的响应式 API
    • 基本类型(数字、字符串)用 ref
    • 复杂对象或数组用 reactive
    • 大型对象且只关心顶层变化时用 shallowRefshallowReactive
    • 需要保护数据时用 readonlyshallowReadonly
  2. 使用 toRefs 解构
    • 在需要解构 reactive 对象时,始终使用 toRefs 保持响应式。
  3. 优化性能
    • 使用 shallowRefshallowReactivemarkRaw 处理大数据量或第三方库对象。
    • 使用 effectScope 管理动态副作用,防止内存泄漏。
  4. 调试响应式问题
    • 使用 isRefisReactiveisReadonly 等工具函数检查变量类型。
    • 启用 Vue 的开发模式,查看控制台的警告信息。
  5. 清晰的命名
    • refreactive 变量取有意义的名字,比如 countRefuserState,避免混淆。
  6. 结合状态管理
    • 在大型应用中,结合 Pinia 或 Vuex 使用 reactivereadonly 管理全局状态。

六、总结与选择指南

Vue 3 的响应式系统提供了灵活且强大的工具,涵盖了从简单值到复杂对象的各种场景。以下是快速选择指南,帮助你决定何时使用哪个 API:

API适用场景注意事项
ref基本类型或简单对象使用 .value 访问/修改;在模板中自动解包
reactive复杂对象或数组不能替换整个对象;直接操作属性
isRef检查是否为 ref用于调试或动态处理数据
unref/toValue统一获取 ref 或普通值toValue 支持函数,Vue 3.3+ 推荐使用
toRefreactive 属性转为 ref保持与原对象同步;属性必须存在
toRefs解构 reactive 对象保持响应式适合批量传递属性到子组件
shallowRef浅层响应式,优化大数据量仅监控 .value 变化,嵌套属性不响应
triggerRef手动触发 shallowRef 更新配合 shallowRef 使用,避免滥用
customRef自定义响应式逻辑(如防抖、节流)需手动调用 tracktrigger,逻辑复杂
shallowReactive浅层响应式对象,优化性能仅监控顶层属性,嵌套属性不响应
readonly保护数据不被修改深层只读,修改抛出警告
shallowReadonly保护顶层属性,允许嵌套修改适合部分保护的场景
markRaw标记对象为非响应式用于第三方库对象或不需要响应式的数据,优化性能
effectScope统一管理响应式副作用动态组件或插件中管理 watchcomputed,需调用 scope.stop() 清理
computed动态计算衍生数据缓存结果,默认只读,可提供 get/set 实现可写

七、官方文档与参考资料

以下是 Vue 3 响应式系统的最新官方文档链接(基于 Vue 3.5.x,2025 年 5 月)以及推荐的社区资源,帮助你深入学习和查阅:

1. 官方文档

2. 社区资源

3. 学习建议

  1. 实践为主:通过小项目练习 refreactive,比如实现一个计数器、表单或 Todo 列表。
  2. 阅读源码:Vue 3 的响应式系统代码在 GitHub 的 vuejs/core 仓库,阅读 @vue/reactivity 部分有助于深入理解。
  3. 调试工具:使用 Vue Devtools 浏览器插件,观察响应式数据的变化。
  4. 关注更新:Vue 3 持续更新(如 toValue 在 3.3 引入),定期查看官方文档的变更日志。