前置知识: JavaScript、HTML5、CSS

组合式 API

5 min中级

Vue3组合式API核心用法详解

前置知识

学习目标

  • 掌握「1. 组合式 API 概述 | Composition API Overview」的核心机制、典型用法与常见陷阱
  • 掌握「2. setup 函数 | Setup Function」的核心机制、典型用法与常见陷阱
  • 掌握「3. 响应式 API | Reactive APIs」的核心机制、典型用法与常见陷阱
  • 掌握「4. 生命周期钩子 | Lifecycle Hooks」的核心机制、典型用法与常见陷阱
  • 掌握「5. 组合函数 | Composables」的核心机制、典型用法与常见陷阱

1. 组合式 API 概述 | Composition API Overview

组合式 API 是 Vue3 引入的新特性,它提供了一种新的方式来组织组件逻辑,使代码更易于维护和复用。与选项式 API(Options API)相比,组合式 API 具有以下优势:

  • 更好的代码组织:可以将相关的逻辑组合在一起,而不是分散在不同的选项中
  • 更好的类型推导:TypeScript 类型推断更加准确
  • 更好的逻辑复用:可以通过组合函数(Composables)来复用逻辑
  • 更灵活的代码结构:不再受选项式 API 的限制

2. setup 函数 | Setup Function

setup 函数是组合式 API 的入口点,它在组件创建之前执行,返回的对象会暴露给模板和其他选项。

2.1 基本用法

<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>
<script>
import { ref, onMounted } from 'vue';
export default {
  setup() {
    // 创建响应式数据
    const count = ref(0);
    // 定义方法
    const increment = () => {
      count.value++;
    };
    // 生命周期钩子
    onMounted(() => {
      console.log('Component mounted');
    });
    // 返回暴露给模板的内容
    return {
      count,
      increment,
    };
  },
};
</script>

2.2 script setup 语法糖

Vue3.2+ 提供了 script setup 语法糖,使组合式 API 的使用更加简洁:

<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
// 直接定义响应式数据
const count = ref(0);
// 直接定义方法
const increment = () => {
  count.value++;
};
// 直接使用生命周期钩子
onMounted(() => {
  console.log('Component mounted');
});
</script>

3. 响应式 API | Reactive APIs

3.1 ref

ref 用于创建响应式的基本类型数据:

import { ref } from 'vue';
const count = ref(0);
console.log(count.value); // 0
count.value++;
console.log(count.value); // 1

3.2 reactive

reactive 用于创建响应式的对象:

 import { reactive } from 'vue'
 const state = reactive({
  count: 0,
  message: 'Hello'
 }
 console.log(state.count) // 0
 state.count++
 console.log(state.count) // 1

3.3 computed

computed 用于创建计算属性:

import { ref, computed } from 'vue';
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
console.log(doubleCount.value); // 0
count.value++;
console.log(doubleCount.value); // 2

3.4 watch

watch 用于监听数据变化:

import { ref, watch } from 'vue';
const count = ref(0);
watch(count, (newValue, oldValue) => {
  console.log(`Count changed from ${oldValue} to ${newValue}`);
});
count.value++; // 输出: Count changed from 0 to 1

3.5 watchEffect

watchEffect 用于自动追踪响应式依赖:

import { ref, watchEffect } from 'vue';
const count = ref(0);
watchEffect(() => {
  console.log(`Count is ${count.value}`);
});
count.value++; // 输出: Count is 1

4. 生命周期钩子 | Lifecycle Hooks

组合式 API 提供了与选项式 API 对应的生命周期钩子:

  • onMounted:组件挂载后
  • onUpdated:组件更新后
  • onUnmounted:组件卸载后
  • onBeforeMount:组件挂载前
  • onBeforeUpdate:组件更新前
  • onBeforeUnmount:组件卸载前
  • onErrorCaptured:捕获子组件错误
  • onRenderTracked:响应式依赖被追踪时
  • onRenderTriggered:响应式依赖被触发时
import { onMounted, onUpdated, onUnmounted } from 'vue';
onMounted(() => {
  console.log('Component mounted');
});
onUpdated(() => {
  console.log('Component updated');
});
onUnmounted(() => {
  console.log('Component unmounted');
});

5. 组合函数 | Composables

组合函数是组合式 API 的核心概念,用于复用逻辑:

// composables/useCounter.js
import { ref, computed } from 'vue';
export function useCounter(initialValue = 0) {
  const count = ref(initialValue);
  const doubleCount = computed(() => count.value * 2);
  const increment = () => {
    count.value++;
  };
  const decrement = () => {
    count.value--;
  };
  return {
    count,
    doubleCount,
    increment,
    decrement,
  };
}

使用组合函数:

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double Count: {{ doubleCount }}</p>
    <button @click="increment">Increment</button>
    <button @click="decrement">Decrement</button>
  </div>
</template>
<script setup>
import { useCounter } from './composables/useCounter';
const { count, doubleCount, increment, decrement } = useCounter(0);
</script>

6. 依赖注入 | Dependency Injection

6.1 provide

provide 用于向子组件提供数据:

<!-- ParentComponent.vue -->
<script setup>
import { provide, ref } from 'vue';
const message = ref('Hello from parent');
provide('message', message);
</script>

6.2 inject

inject 用于从父组件获取数据:

<!-- ChildComponent.vue -->
<script setup>
import { inject } from 'vue';
const message = inject('message', 'Default message');
</script>
<template>
  <p>{{ message }}</p>
</template>

7. 模板引用 | Template Refs

使用 ref 可以获取DOM元素或组件实例:

<template>
  <div ref="container">Hello</div>
  <MyComponent ref="myComponent" />
</template>
<script setup>
import { ref, onMounted } from 'vue';
import MyComponent from './MyComponent.vue';
const container = ref(null);
const myComponent = ref(null);
onMounted(() => {
  console.log(container.value); // DOM元素
  console.log(myComponent.value); // 组件实例
});
</script>

8. 响应式工具 | Reactive Utilities

8.1 toRefs

toRefs 用于将响应式对象转换为普通对象,其中每个属性都是一个 ref:

 import { reactive, toRefs } from 'vue'
 const state = reactive({
  count: 0,
  message: 'Hello'
 }
 const refs = toRefs(state)
 console.log(refs.count.value) // 0
 console.log(refs.message.value) // Hello

8.2 toRef

toRef 用于为响应式对象的单个属性创建 ref:

 import { reactive, toRef } from 'vue'
 const state = reactive({
  count: 0,
  message: 'Hello'
 }
 const countRef = toRef(state, 'count')
 console.log(countRef.value) // 0

8.3 unref

unref 用于获取 ref 的值,如果参数不是 ref,则直接返回参数:

import { ref, unref } from 'vue';
const count = ref(0);
const message = 'Hello';
console.log(unref(count)); // 0
console.log(unref(message)); // Hello

8.4 isRef

isRef 用于检查一个值是否是 ref:

import { ref, isRef } from 'vue';
const count = ref(0);
const message = 'Hello';
console.log(isRef(count)); //
console.log(isRef(message)); // false

9. 最佳实践 | Best Practices

  1. 使用 script setup:简洁明了,推荐使用
  2. 组织逻辑:将相关的逻辑组合在一起
  3. 使用组合函数:复用逻辑,提高代码可维护性
  4. 合理使用响应式 API:根据需要选择 ref 或 reactive
  5. 避免过度使用 watch:优先使用 computed
  6. 注意响应式陷阱:了解响应式系统的工作原理,避免常见陷阱

10. 示例 | Examples

10.1 计数器示例

<template>
  <div class="counter">
    <h2>Counter</h2>
    <p>Count: {{ count }}</p>
    <p>Double: {{ doubleCount }}</p>
    <div>
      <button @click="increment">+</button>
      <button @click="decrement">-</button>
      <button @click="reset">Reset</button>
    </div>
  </div>
</template>
<script setup>
import { ref, computed } from 'vue';
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
const increment = () => count.value++;
const decrement = () => count.value--;
const reset = () => (count.value = 0);
</script>
<style scoped>
.counter {
  text-align: center;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
  max-width: 300px;
  margin: 0 auto;
}
button {
  margin: 0 5px;
  padding: 5px 10px;
  font-size: 16px;
}
</style>

10.2 表单示例

<template>
  <div class="form">
    <h2>Form</h2>
    <div>
      <label>Name:</label>
      <input v-model="form.name" type="text" />
    </div>
    <div>
      <label>Email:</label>
      <input v-model="form.email" type="email" />
    </div>
    <div>
      <label>Message:</label>
      <textarea v-model="form.message"></textarea>
    </div>
    <button @click="submitForm">Submit</button>
    <div v-if="submitted">
      <h3>Submitted Data:</h3>
      <pre>{{ form }}</pre>
    </div>
  </div>
</template>
<script setup>
import { reactive, ref } from 'vue'
const form = reactive({
 name: '',
 email: '',
 message: ''
}
const submitted = ref(false)
const submitForm = () => {
 console.log('Form submitted:', form)
 submitted.value =
}
</script>
<style scoped>
.form {
  max-width: 400px;
  margin: 0 auto;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
}
div {
  margin-bottom: 10px;
}
label {
  display: inline-block;
  width: 80px;
}
input,
textarea {
  width: 300px;
  padding: 5px;
}
button {
  margin-top: 10px;
  padding: 5px 10px;
}
</style>

11. 小结 | Summary

组合式 API 是 Vue3 的重要特性,它提供了一种更灵活、更强大的方式来组织组件逻辑。通过本章节的学习,你已经了解了组合式 API 的基本概念和使用方法,包括 setup 函数、响应式 API、生命周期钩子、组合函数等。 组合式 API 的核心优势在于它允许你根据逻辑关注点组织代码,而不是根据选项类型。这使得代码更加模块化、可复用,并且更易于理解和维护。 在实际开发中,建议使用 script setup 语法糖,它使组合式 API 的使用更加简洁明了。同时,要善于使用组合函数来复用逻辑,提高代码的可维护性。

响应式状态

ref 响应式引用 const <state> = ref(<initialValue>);

import { ref } from 'vue';
const count = ref(0);
count.value++;              // 修改值
console.log(count.value);  // 读取值

const user = ref({ name: 'Tom' });
user.value.name = 'Jerry';  // 修改对象属性

reactive 对象响应式 const <state> = reactive(<object>);

import { reactive } from 'vue';
const state = reactive({
  count: 0,
  user: { name: 'Tom' }
});
state.count++;
state.user.name = 'Jerry';

shallowRef 浅响应式引用 const <state> = shallowRef(<initialValue>);

import { shallowRef } from 'vue';
const obj = shallowRef({ count: 0 });
obj.value.count = 1;            // 不会触发更新
obj.value = { count: 1 };       // 替换整个值才触发

shallowReactive 浅响应式对象 const <state> = shallowReactive(<object>);

import { shallowReactive } from 'vue';
const state = shallowReactive({ nested: { count: 0 } });
state.nested.count = 1;  // 不会触发更新

readonly 只读代理 const <readonly> = readonly(<reactiveSource>);

import { reactive, readonly } from 'vue';
const original = reactive({ count: 0 });
const copy = readonly(original);
copy.count++;  // 警告并失败

计算属性

computed 计算属性 const <result> = computed(() => <expression>);

import { ref, computed } from 'vue';
const count = ref(1);
const double = computed(() => count.value * 2);
console.log(double.value);  // 2

可写 computed const <result> = computed({ get, set });

const firstName = ref('John');
const lastName = ref('Doe');
const fullName = computed({
  get() { return `${firstName.value} ${lastName.value}`; },
  set(val) {
    [firstName.value, lastName.value] = val.split(' ');
  }
});
fullName.value = 'Tom Smith';

侦听器

watch 侦听器 watch(<source>, (<newVal>, [oldVal]) => {}, [options]);

import { ref, watch } from 'vue';
const count = ref(0);

watch(count, (newVal, oldVal) => {
  console.log(`从 ${oldVal} 变为 ${newVal}`);
});

watch(count, (newVal, oldVal, onCleanup) => {
  const timer = setTimeout(() => doSomething(newVal), 500);
  onCleanup(() => clearTimeout(timer));
});

watch 多源侦听

watch([fooRef, barRef], ([newFoo, newBar], [oldFoo, oldBar]) => {
  console.log('foo 或 bar 变化');
});

watch(
  () => state.user.name,
  (newVal, oldVal) => console.log('name 变化')
);

watch 配置选项

watch(count, callback, {
  immediate: true,    // 立即执行
  deep: true,         // 深度侦听
  flush: 'post',      // 'pre' | 'post' | 'sync'
  once: true          // 只触发一次
});

watchEffect 自动追踪依赖 watchEffect(<effect> => {});

import { ref, watchEffect } from 'vue';
const count = ref(0);

watchEffect(() => {
  console.log('count:', count.value);
});

watchEffect((onCleanup) => {
  const timer = setInterval(() => console.log(count.value), 1000);
  onCleanup(() => clearInterval(timer));
});

watchPostEffect DOM 更新后执行

import { watchPostEffect } from 'vue';
watchPostEffect(() => {
  console.log('DOM 已更新');
});

watchSyncEffect 同步执行

import { watchSyncEffect } from 'vue';
watchSyncEffect(() => {
  console.log('同步执行');
});

工具函数

toRef 转换为 ref const <ref> = toRef(<source>, <key>);

import { reactive, toRef } from 'vue';
const state = reactive({ count: 0 });
const countRef = toRef(state, 'count');
countRef.value++;  // 同步修改 state.count

toRefs 解构响应式对象 const { <key>, ... } = toRefs(<reactive>);

import { reactive, toRefs } from 'vue';
const state = reactive({ count: 0, name: 'Tom' });
const { count, name } = toRefs(state);
count.value++;

unref 获取值 const <value> = unref(<maybeRef>);

import { ref, unref } from 'vue';
const count = ref(0);
console.log(unref(count));  // 0
console.log(unref(123));    // 123

isRef / isReactive / isProxy

import { ref, reactive, isRef, isReactive, isProxy } from 'vue';
isRef(ref(0));         // true
isReactive(reactive({}));  // true
isProxy(reactive({}));     // true

toRaw 获取原始对象 const <raw> = toRaw(<proxy>);

import { reactive, toRaw } from 'vue';
const foo = reactive({});
const raw = toRaw(foo);
console.log(raw === foo);  // false

markRaw 标记永不响应 const <obj> = markRaw(<object>);

import { reactive, markRaw } from 'vue';
const state = reactive({});
state.classInstance = markRaw(new SomeClass());

依赖注入

provide 提供 provide(<key>, <value>);

import { provide, ref } from 'vue';
const theme = ref('dark');
provide('theme', theme);
provide('theme', 'dark');        // 静态值
provide(Symbol('config'), {});

inject 注入 const <value> = inject(<key>, [defaultValue], [treatDefaultAsFactory]);

import { inject } from 'vue';
const theme = inject('theme');
const theme = inject('theme', 'light');
const config = inject('config', () => createDefaultConfig(), true);

模板引用

useTemplateRef 模板引用(Vue 3.5+) const <el> = useTemplateRef(<refName>);

import { useTemplateRef } from 'vue';
const inputEl = useTemplateRef('inputRef');
onMounted(() => {
  inputEl.value?.focus();
});
vue
<template>
  <input ref="inputRef" />
</template>

ref 字符串方式(传统)

<template>
  <input ref="inputRef" />
</template>
<script setup>
import { ref, onMounted } from 'vue';
const inputRef = ref(null);
onMounted(() => inputRef.value?.focus());
</script>

函数式 ref

<template>
  <input :ref="(el) => { inputEl = el }" />
</template>

组件通信 API

defineProps 声明 props const <props> = defineProps(<propsSpec>);

const props = defineProps({
  title: String,
  count: { type: Number, default: 0 },
  list: { type: Array, required: true },
  callback: { type: Function, default: () => {} }
});

defineProps 泛型方式

const props = defineProps<{
  title: string;
  count?: number;
  list: string[];
}>();

响应式 props 解构(Vue 3.5+)

const { title, count = 0 } = defineProps<{
  title: string;
  count?: number;
}>();
// title 和 count 自动保持响应性

defineEmits 声明事件 const <emit> = defineEmits(<eventsSpec>);

const emit = defineEmits(['change', 'submit']);
emit('change', value);
emit('submit', { data: payload });

defineEmits 泛型方式

const emit = defineEmits<{
  (e: 'change', value: string): void;
  (e: 'submit', payload: { id: number }): void;
}>();

defineExpose 暴露方法 defineExpose({ <key>: <value>, ... });

const publicMethod = () => console.log('called');
defineExpose({ publicMethod, props });

defineModel 双向绑定(Vue 3.4+) const <model> = defineModel([modelName], [options]);

const model = defineModel<string>();
function update() {
  model.value = 'new value';
}

const title = defineModel<string>('title');
const count = defineModel<number>('count', { default: 0, local: true });

defineOptions 定义选项

defineOptions({
  name: 'MyComponent',
  inheritAttrs: false,
  customOption: 'value'
});

defineSlots 类型声明

const slots = defineSlots<{
  default(props: { item: any }): any;
  header?(): any;
}>();

useAttrs 获取透传属性 const <attrs> = useAttrs();

import { useAttrs } from 'vue';
const attrs = useAttrs();
console.log(attrs.class, attrs.id);

useSlots 获取插槽 const <slots> = useSlots();

import { useSlots } from 'vue';
const slots = useSlots();
if (slots.header) {
  // 处理插槽
}