响应式系统

Vue.js 的响应式系统是其核心特性之一,它允许开发者以声明式方式构建用户界面,当数据发生变化时,视图会自动更新,无需手动操作 DOM。Vue 3.5 对响应式系统进行了重大重构,在保持原有行为兼容性的基础上,实现了性能的显著提升:内存占用降低 56%,大型深度响应式数组操作速度提升 10 倍,并解决了 SSR 期间因挂起计算导致的过时计算值和内存问题[3][18]。这一系统基于 Proxy 实现,相比 Vue 2 的 Object.defineProperty 机制,提供了更全面的响应式能力和更优的性能表现。

响应式原理:从 Object.defineProperty 到 Proxy

Vue 2 的响应式系统基于 Object.defineProperty 实现,通过遍历数据对象的属性并为其定义 getter 和 setter 来追踪变化。然而,这种方式存在固有局限:需要在初始化时遍历所有属性,无法监听对象新增属性或删除属性,对数组的某些操作(如通过索引修改元素)也无法触发响应。

Vue 3 采用 Proxy 重构了响应式系统,从根本上解决了这些问题。Proxy 可以创建一个对象的代理,从而实现对目标对象的属性读取、赋值、删除等操作的拦截。其核心优势在于:

  • 动态监听:无需在初始化时遍历所有属性,可直接拦截对象的所有操作,包括新增属性和删除属性。
  • 数组优化:原生支持数组的所有方法(如 push、pop、splice 等)及索引操作的监听,无需像 Vue 2 那样重写数组原型方法。
  • 性能提升:Vue 3.5 进一步引入“外星信号”(alien signals)机制,通过“槽位复用+增量 GC”策略减少内存碎片化,使响应式对象内存占用从 48 字节压缩至 16 字节,响应式性能提升 40%,内存占用降低 65%[19][20]。

以下是 Proxy 实现响应式的基础示例:

javascript
复制代码
// 创建响应式对象
const target = { count: 0 };
const proxy = new Proxy(target, {
  get(target, key) {
    track(target, key); // 收集依赖
    return target[key];
  },
  set(target, key, value) {
    target[key] = value;
    trigger(target, key); // 触发更新
    return true;
  }
});

// 使用代理对象
proxy.count++; // 会触发 set 拦截,进而触发更新

在 Vue 3.5 中,这一机制被进一步优化,特别是针对大型深度响应式数组的追踪效率,某些场景下操作速度提升可达 10 倍[18]。

ref 与 reactive:响应式数据的两种创建方式

Vue 3 提供了两种主要方式创建响应式数据:refreactive。两者基于不同的设计理念,适用于不同场景。

ref:基本类型的响应式包装

ref 用于将基本类型数据(如数字、字符串)转换为响应式对象,它会创建一个包含 .value 属性的包装对象。在模板中使用时,Vue 会自动解包 .value,无需手动访问;但在 JavaScript 代码中必须显式使用 .value

类型标注与使用示例(用户信息管理案例):

typescript
复制代码
import { ref } from 'vue';

// ref 定义数字类型
const age = ref<number>(25); 
// ref 定义字符串类型
const name = ref<string>('Alice'); 

// JavaScript 中修改需使用 .value
age.value++; // age 变为 26

// 模板中自动解包(无需 .value)
// <template>{{ age }} - {{ name }}</template> 
reactive:对象类型的响应式转换

reactive 用于将对象类型数据(如对象、数组)转换为响应式对象,它直接返回一个代理对象,无需 .value 访问。但 reactive 存在局限性:不能直接赋值新对象(会丢失响应性),且解构后会失去响应性。

类型标注与使用示例

typescript
复制代码
import { reactive } from 'vue';

// reactive 定义对象类型
const user = reactive<{ name: string; age?: number }>({ 
  name: 'Alice',
  age: 25
});

// 直接修改属性(无需 .value)
user.age = 26; 

// 解构会丢失响应性(错误示范)
const { name } = user; 
name = 'Bob'; // 不会触发更新
toRefs:解决 reactive 解构问题

toRefs 可将 reactive 对象转换为包含多个 ref 对象的集合,每个属性都是一个独立的 ref,从而支持解构赋值且保持响应性:

typescript
复制代码
import { reactive, toRefs } from 'vue';

const user = reactive({ name: 'Alice', age: 25 });
const userRefs = toRefs(user); 

// 解构后仍保持响应性
const { name, age } = userRefs;
age.value++; // user.age 变为 26,视图更新

使用建议

  • 基本类型(数字、字符串等)或需要独立响应的简单值,优先使用 ref
  • 复杂对象类型(如用户信息、表单数据),使用 reactive
  • 需解构 reactive 对象时,配合 toRefs 保持响应性。

computed:带缓存的计算属性

computed 用于创建依赖其他响应式数据的计算属性,它会缓存计算结果,只有当依赖的响应式数据变化时才重新计算,相比 methods 能显著提升性能。

与 methods 的核心差异
  • methods:每次调用都会重新执行函数,无论依赖数据是否变化。
  • computed:依赖数据不变时直接返回缓存结果,避免重复计算。
购物车总价计算案例
typescript
复制代码
import { ref, computed } from 'vue';

// 商品列表(ref 数组类型)
const products = ref<{ price: number; quantity: number }[]>([
  { price: 100, quantity: 2 },
  { price: 200, quantity: 1 }
]);

// 计算总价(只读 computed)
const totalPrice = computed(() => {
  console.log('计算总价...'); // 仅依赖变化时执行
  return products.value.reduce(
    (sum, item) => sum + item.price * item.quantity, 
    0
  );
});

console.log(totalPrice.value); // 400(首次计算)
products.value[0].quantity = 2; // 依赖未变,不重新计算
console.log(totalPrice.value); // 400(返回缓存)
products.value[0].quantity = 3; // 依赖变化,重新计算
console.log(totalPrice.value); // 500(重新计算)
computed setter:实现可写计算属性

computed 还支持通过 getset 配置实现可写计算属性,用于双向绑定场景:

typescript
复制代码
import { ref, computed } from 'vue';

const count = ref(1);

// 带 setter 的 computed
const doubleCount = computed({
  get: () => count.value * 2,
  set: (val) => { count.value = val / 2; } // 设置时反向更新依赖
});

doubleCount.value = 4; // 触发 setter,count 变为 2
console.log(count.value); // 2

watch 与 watchEffect:响应式数据的监听

Vue 3 提供 watchwatchEffect 两种监听响应式数据变化的方式,适用于不同场景。

watch:精确监听指定源

watch 需显式指定监听源,支持配置监听时机、深度监听、立即执行等,灵活性高。

搜索框防抖案例(监听搜索文本变化,延迟发送请求):

typescript
复制代码
import { ref, watch } from 'vue';

const searchText = ref('');
let timeoutId: number;

// 监听 searchText 变化
watch(
  () => searchText.value, // 监听源(函数返回值为依赖)
  (newVal, oldVal) => {
    // 防抖:清除上一次定时器
    clearTimeout(timeoutId);
    // 延迟 500ms 发送请求
    timeoutId = window.setTimeout(() => {
      console.log(`搜索: ${newVal}`);
      // 实际场景:fetch(`/api/search?q=${newVal}`)
    }, 500);
  },
  { 
    immediate: true, // 初始立即执行一次
    deep: false // 非对象类型无需深度监听
  }
);
watchEffect:自动收集依赖

watchEffect 无需指定监听源,会自动收集函数内的响应式依赖,当依赖变化时重新执行。适用于“副作用跟随多个依赖变化”的场景。

清理副作用(取消过时 API 请求):

typescript
复制代码
import { ref, watchEffect } from 'vue';

const searchText = ref('');

watchEffect((onInvalidate) => {
  // 自动收集 searchText 依赖
  const query = searchText.value;
  const controller = new AbortController(); // 用于取消请求

  // 发送请求
  fetch(`/api/search?q=${query}`, { signal: controller.signal })
    .then(res => res.json())
    .then(data => console.log('搜索结果:', data));

  // 清理函数:依赖变化或组件卸载时执行
  onInvalidate(() => {
    controller.abort(); // 取消上一次未完成的请求
    console.log('取消过时请求:', query);
  });
});

// 修改 searchText 会触发重新执行,并清理上一次副作用
searchText.value = 'vue'; 

watch 与 watchEffect 对比

  • watch:需显式指定源,可获取新旧值,配置更灵活,适合精确控制监听行为。
  • watchEffect:自动收集依赖,代码更简洁,适合副作用与多个依赖关联的场景,但无法直接获取旧值。

#的性能优化与最佳实践

Vue 3.5 响应式系统的重构带来了显著的性能提升,包括内存占用降低 56%、大型数组操作速度提升 10 倍等[18]。在实际开发中,还需注意以下最佳实践:

  1. 避免过度响应式:非响应式数据(如常量、配置)无需使用 refreactive 包装。
  2. 合理使用 computed 缓存:频繁计算的场景优先使用 computed,而非 methods
  3. 控制 watch 深度:非必要时关闭 deep: true,避免性能损耗。
  4. 清理副作用watchwatchEffect 中涉及定时器、事件监听等,需通过清理函数避免内存泄漏。

通过理解并合理运用 Vue 3.5 的响应式系统,开发者可以构建高性能、易维护的前端应用,充分发挥其在性能与开发体验上的优势。