MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue Composition API ref与reactive的响应式数据声明技巧

2021-11-091.8k 阅读

Vue Composition API 简介

Vue Composition API 是 Vue 3.0 引入的一套基于函数的 API,它允许开发者使用组合式的方式来组织和复用组件逻辑。与传统的 Vue 选项式 API 相比,Composition API 提供了更灵活、更高效的代码组织方式,特别是在处理复杂组件逻辑时。

在 Vue Composition API 中,refreactive 是两个重要的函数,用于声明响应式数据。它们在实现响应式的原理和使用场景上有所不同,下面我们将详细探讨。

ref 函数

ref 基础概念

ref 函数用于创建一个包含响应式数据的引用。它接受一个初始值,并返回一个 Ref 对象。这个 Ref 对象具有一个 .value 属性,通过这个属性可以访问和修改内部的响应式数据。

例如,创建一个简单的 ref

<template>
  <div>
    <p>{{ count.value }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const count = ref(0);

const increment = () => {
  count.value++;
};
</script>

在上述代码中,我们使用 ref 创建了一个名为 count 的响应式引用,初始值为 0。在模板中,我们通过 count.value 来显示这个值,并在按钮点击时通过 count.value++ 来增加它的值。

ref 的响应式原理

ref 的响应式是基于 ES6 的 Object.defineProperty 来实现的。当创建一个 ref 时,Vue 会在内部使用 Object.definePropertyRef 对象的 .value 属性进行劫持,从而实现对数据变化的追踪。

当数据发生变化时,Vue 的响应式系统会检测到 .value 属性的变化,并触发依赖收集,进而更新相关的 DOM。

ref 的类型推断

在 TypeScript 环境下,ref 会根据传入的初始值进行类型推断。例如:

import { ref } from 'vue';

const num = ref(10); // num: Ref<number>
const str = ref('hello'); // str: Ref<string>

如果要明确指定类型,可以这样做:

import { ref } from 'vue';

const count: Ref<number> = ref(0);

ref 在模板中的使用

在模板中使用 ref 时,不需要额外写 .value。Vue 会自动解包 ref,例如:

<template>
  <div>
    <p>{{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const count = ref(0);

const increment = () => {
  count.value++;
};
</script>

这里在模板中直接使用 count 就可以,Vue 会自动访问 count.value

ref 用于复杂数据类型

ref 也可以用于复杂数据类型,如对象和数组:

<template>
  <div>
    <p>{{ user.value.name }}</p>
    <button @click="updateUser">Update User</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const user = ref({ name: 'John', age: 30 });

const updateUser = () => {
  user.value.age++;
};
</script>

虽然 ref 可以用于复杂数据类型,但在处理复杂数据结构时,reactive 通常是更好的选择。

reactive 函数

reactive 基础概念

reactive 函数用于创建一个响应式对象。它接受一个普通对象,并返回一个响应式的代理对象。这个代理对象与原始对象具有相同的属性,但对属性的访问和修改会触发 Vue 的响应式系统。

例如:

<template>
  <div>
    <p>{{ user.name }}</p>
    <p>{{ user.age }}</p>
    <button @click="updateUser">Update User</button>
  </div>
</template>

<script setup>
import { reactive } from 'vue';

const user = reactive({ name: 'John', age: 30 });

const updateUser = () => {
  user.age++;
};
</script>

在这个例子中,我们使用 reactive 创建了一个响应式的 user 对象。在模板中可以直接访问 user 的属性,并且在按钮点击时修改 user.age 会触发视图更新。

reactive 的响应式原理

reactive 是基于 ES6 的 Proxy 来实现的。Proxy 提供了一种更强大的方式来拦截和处理对对象的操作。Vue 使用 Proxy 对传入的对象进行代理,从而实现对对象属性的访问、赋值、枚举等操作的追踪。

当对象的属性发生变化时,Vue 的响应式系统会通过 Proxy 的拦截捕获到这些变化,并触发依赖更新。

reactive 的深层响应式

reactive 创建的对象是深层响应式的。这意味着即使对象内部嵌套了多层对象,对任何一层属性的修改都会触发响应式更新。

<template>
  <div>
    <p>{{ settings.theme }}</p>
    <button @click="updateTheme">Update Theme</button>
  </div>
</template>

<script setup>
import { reactive } from 'vue';

const settings = reactive({
  user: {
    name: 'John',
    preferences: {
      theme: 'light'
    }
  }
});

const updateTheme = () => {
  settings.user.preferences.theme = 'dark';
};
</script>

在上述代码中,我们修改了深层嵌套的 theme 属性,视图依然会正确更新。

reactive 与 TypeScript

在 TypeScript 中使用 reactive 时,需要注意类型声明。通常,可以使用接口或类型别名来定义对象的类型:

import { reactive } from 'vue';

interface User {
  name: string;
  age: number;
}

const user: User = reactive({ name: 'John', age: 30 });

这样可以确保 user 对象的属性类型符合定义。

ref 与 reactive 的比较

适用场景

  1. 简单数据类型:对于简单数据类型,如字符串、数字、布尔值等,ref 是更合适的选择。因为 ref 为简单数据类型提供了一种方便的包装方式,使其具有响应式。
  2. 复杂数据类型:当处理对象和数组等复杂数据类型时,reactive 通常更方便。reactive 直接创建深层响应式的对象,不需要像 ref 那样通过 .value 来访问和修改数据。

性能考虑

  1. ref:由于 ref 是基于 Object.defineProperty 实现的,对于简单数据类型的响应式处理相对高效。但是,当用于复杂数据类型时,由于每次访问和修改都需要通过 .value,可能会带来一些性能开销。
  2. reactivereactive 基于 Proxy 实现,对于复杂数据类型的处理更加高效,因为它可以直接对对象进行代理,不需要额外的 .value 操作。但是,Proxy 的创建和处理本身也有一定的性能开销,所以在处理大量简单数据类型时,ref 可能更具优势。

数据解构与响应式丢失

  1. ref:当对 ref 创建的对象进行解构时,需要特殊处理以保持响应式。例如:
<template>
  <div>
    <p>{{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const count = ref(0);
const { value: localCount } = count;

const increment = () => {
  // 这样不会更新视图,因为 localCount 不是响应式的
  localCount++; 
  // 正确的方式是
  count.value++; 
};
</script>
  1. reactive:对 reactive 创建的对象进行解构时,响应式会丢失。例如:
<template>
  <div>
    <p>{{ name }}</p>
    <p>{{ age }}</p>
    <button @click="updateUser">Update User</button>
  </div>
</template>

<script setup>
import { reactive } from 'vue';

const user = reactive({ name: 'John', age: 30 });
const { name, age } = user;

const updateUser = () => {
  // 这样不会更新视图,因为 name 和 age 不是响应式的
  name = 'Jane'; 
  age++; 
};
</script>

要保持响应式,可以使用 toRefs 函数,后面会详细介绍。

其他相关函数

toRef

toRef 函数用于创建一个 ref,它的值会链接到源对象上的某个属性。这在需要将对象的某个属性单独提取为 ref 时非常有用,同时保持与原始对象的响应式链接。

<template>
  <div>
    <p>{{ age }}</p>
    <button @click="updateAge">Update Age</button>
  </div>
</template>

<script setup>
import { reactive, toRef } from 'vue';

const user = reactive({ name: 'John', age: 30 });
const age = toRef(user, 'age');

const updateAge = () => {
  age.value++;
};
</script>

在这个例子中,age 是一个 ref,它的值与 user.age 保持同步。修改 age.value 会同时修改 user.age,并且触发视图更新。

toRefs

toRefs 函数用于将一个响应式对象转换为普通对象,其中每个属性都是一个 ref。这在对响应式对象进行解构时非常有用,可以保持解构后的属性依然是响应式的。

<template>
  <div>
    <p>{{ name }}</p>
    <p>{{ age }}</p>
    <button @click="updateUser">Update User</button>
  </div>
</template>

<script setup>
import { reactive, toRefs } from 'vue';

const user = reactive({ name: 'John', age: 30 });
const { name, age } = toRefs(user);

const updateUser = () => {
  name.value = 'Jane';
  age.value++;
};
</script>

这里通过 toRefsnameage 都是 ref,修改它们的值会触发视图更新。

unref

unref 函数是一个辅助函数,它接受一个 ref 对象,如果参数是 ref,则返回其 .value,否则直接返回参数本身。这在需要统一处理 ref 和非 ref 值时非常方便。

<template>
  <div>
    <p>{{ result }}</p>
  </div>
</template>

<script setup>
import { ref, unref } from 'vue';

const num = ref(10);
const str = 'hello';

const result = unref(num) + unref(str);
</script>

在这个例子中,unref 确保无论是 ref 对象还是普通值,都能正确处理。

isRef

isRef 函数用于检查一个值是否是 ref 对象。

<template>
  <div>
    <p>{{ isNumRef }}</p>
    <p>{{ isStrRef }}</p>
  </div>
</template>

<script setup>
import { ref, isRef } from 'vue';

const num = ref(10);
const str = 'hello';

const isNumRef = isRef(num);
const isStrRef = isRef(str);
</script>

这里 isNumReftrueisStrReffalse

实践中的应用场景

表单处理

在处理表单时,refreactive 都有各自的应用场景。对于单个表单字段,如输入框的值,可以使用 ref

<template>
  <div>
    <input v-model="username" type="text" placeholder="Username">
    <p>{{ username }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const username = ref('');
</script>

对于整个表单数据,可以使用 reactive

<template>
  <div>
    <input v-model="formData.username" type="text" placeholder="Username">
    <input v-model="formData.password" type="password" placeholder="Password">
    <button @click="submitForm">Submit</button>
  </div>
</template>

<script setup>
import { reactive } from 'vue';

const formData = reactive({
  username: '',
  password: ''
});

const submitForm = () => {
  console.log(formData.username, formData.password);
};
</script>

状态管理

在小型应用中,可以直接在组件内使用 refreactive 进行状态管理。例如,一个简单的购物车功能:

<template>
  <div>
    <ul>
      <li v-for="item in cart.items" :key="item.id">
        {{ item.name }} - ${{ item.price }}
        <button @click="removeItem(item)">Remove</button>
      </li>
    </ul>
    <p>Total: ${{ cart.total }}</p>
  </div>
</template>

<script setup>
import { reactive } from 'vue';

const cart = reactive({
  items: [
    { id: 1, name: 'Product 1', price: 10 },
    { id: 2, name: 'Product 2', price: 20 }
  ],
  total: 0
});

const removeItem = (item) => {
  const index = cart.items.indexOf(item);
  if (index > -1) {
    cart.total -= item.price;
    cart.items.splice(index, 1);
  }
};

cart.items.forEach(item => {
  cart.total += item.price;
});
</script>

这里使用 reactive 来管理购物车的状态,包括商品列表和总价。

逻辑复用

通过 refreactive,可以将组件逻辑提取到独立的函数中,实现逻辑复用。例如,一个用于处理分页的逻辑:

<template>
  <div>
    <button @click="prevPage">Previous</button>
    <button @click="nextPage">Next</button>
    <p>Page: {{ currentPage }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const usePagination = () => {
  const currentPage = ref(1);
  const totalPages = 10;

  const prevPage = () => {
    if (currentPage.value > 1) {
      currentPage.value--;
    }
  };

  const nextPage = () => {
    if (currentPage.value < totalPages) {
      currentPage.value++;
    }
  };

  return {
    currentPage,
    prevPage,
    nextPage
  };
};

const { currentPage, prevPage, nextPage } = usePagination();
</script>

这里通过 usePagination 函数返回了分页相关的 ref 和方法,方便在多个组件中复用。

总结与最佳实践

  1. 选择合适的响应式声明方式:根据数据类型和使用场景,选择 refreactive。简单数据类型优先使用 ref,复杂数据类型优先使用 reactive
  2. 注意数据解构与响应式保持:在解构 refreactive 创建的数据时,要使用 toReftoRefs 等函数来保持响应式。
  3. 合理使用辅助函数unrefisRef 等辅助函数可以帮助处理 ref 对象,提高代码的健壮性。
  4. 逻辑复用与组织:利用 refreactive 将组件逻辑提取为可复用的函数,提高代码的可维护性和复用性。

通过深入理解 refreactive 的响应式数据声明技巧,并在实践中合理应用,能够更高效地开发 Vue 应用,提升代码的质量和可维护性。在实际项目中,不断积累经验,根据具体需求灵活选择和组合这些工具,将有助于打造出优秀的前端应用。同时,随着 Vue 的不断发展,可能会有更多的优化和改进,开发者需要持续关注官方文档和社区动态,以保持技术的先进性。在处理复杂业务逻辑时,要善于分析数据的结构和变化规律,选择最合适的响应式声明方式,避免过度使用或误用导致性能问题或代码逻辑混乱。希望通过本文的介绍,读者能够对 Vue Composition API 中的 refreactive 有更深入的理解,并在实际开发中运用自如。在处理大型项目时,要从整体架构的角度考虑响应式数据的管理,合理划分模块,确保各个组件之间的响应式数据交互清晰明了。同时,要注重代码的可读性和可调试性,为后续的维护和扩展打下良好的基础。通过不断地实践和总结,将能够更好地掌握 Vue Composition API 的强大功能,开发出更优质的前端应用。