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

发布于:2025-05-28 ⋅ 阅读:(76) ⋅ 点赞:(0)

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 获取或设置值。
  • 适用场景:适合简单数据,如计数器、输入框的值等。
示例代码
import { ref } from 'vue';

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

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

    return { count, increment };
  }
};
<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,直接写 {{ count }} 即可。
  3. 适合基本类型ref 常用于数字、字符串等简单数据。如果是对象,建议考虑 reactive

2. reactive:响应式对象

通俗解释

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

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

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

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

    return { user, updateUser };
  }
};
<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)。
示例代码
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 值时。
示例代码
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,保持响应式连接。
  • 适用场景:需要单独操作对象的一个属性,但仍希望它与原对象同步。
示例代码
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 };
  }
};
<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 对象的属性解构后仍保持响应式。
示例代码
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 };
  }
};
<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 功能类似,但更强大,因为它还能处理函数。
示例代码
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 };
  }
};
<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 的直接赋值操作响应。
  • 适用场景:适合需要优化性能的场景,比如处理大型对象或只需要监控顶层值的变化。
示例代码
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 };
  }
};
<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 使用,当内部属性变化需要强制更新界面时。
示例代码
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 };
  }
};
<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)逻辑。
  • 适用场景:需要自定义响应式行为,比如防抖、节流、或与外部数据源同步。
示例代码(防抖输入)
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 };
  }
};
<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 就像一个“只管第一层”的智能文件夹。文件夹里的直接内容(顶层属性)变化会触发界面更新,但如果文件夹里还有子文件夹(嵌套对象),子文件夹里的内容变化不会触发更新。这种浅层响应式设计是为了优化性能,避免不必要的深层监听。

用法
  • 作用:创建一个浅层响应式对象,仅对对象的顶层属性变化触发响应式更新。
  • 适用场景:适合处理大型嵌套对象,只需要监控顶层属性的场景,比如配置对象或大数据量的状态管理。
示例代码
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 };
  }
};
<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 对象转为只读的响应式对象,禁止修改。
  • 适用场景:在组件间传递数据时,确保数据不被子组件或其他代码修改;或者用于状态管理的只读副本。
示例代码
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 };
  }
};
<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 的浅层版本,适合需要部分保护的场景。

用法
  • 作用:创建一个浅层只读响应式对象,仅顶层属性不可修改,嵌套对象属性可修改。
  • 适用场景:需要保护顶层属性,但允许嵌套对象被修改的场景,比如配置对象的部分保护。
示例代码
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 };
  }
};
<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)或不需要响应式的复杂数据时,优化性能。
示例代码
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 };
  }
};
<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,需要统一销毁时(比如动态组件或插件系统)。
示例代码
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 };
  }
};
<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)自动计算结果,并缓存结果。只有当依赖的数据变化时,它才会重新计算,非常适合需要动态计算的场景。

用法
  • 作用:创建一个基于响应式数据的计算属性,只有依赖变化时才会重新计算。
  • 适用场景:需要根据响应式数据衍生新数据的场景,比如格式化数据、计算总和等。
示例代码
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 };
  }
};
<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. 性能优化:比直接在模板中计算更高效,适合复杂的计算逻辑。
可写计算属性示例
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 };
  }
};
<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+ 推荐使用
toRef reactive 属性转为 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 引入),定期查看官方文档的变更日志。


网站公告

今日签到

点亮在社区的每一天
去签到