Vue3常用API

Vue3中常用的API

ref、reactive

​ Vue 3 的响应式系统是其能够自动更新视图的基石。当修改一个响应式数据时,Vue 能够追踪到这个变化,并自动重新渲染依赖于这个数据的组件部分。refreactive 就是创建这种响应式数据的两种主要方式

reactive

  • reactive() 是 Vue 3 提供的用于创建响应式对象的函数

  • 它接收一个普通的 JavaScript 对象(或数组),并返回该对象的响应式代理(Proxy)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import { reactive } from 'vue';

    const state = reactive({
    count: 0,
    user: {
    name: 'Alice',
    age: 30
    },
    hobbies: ['reading', 'music']
    });

    现在,对 state 的任何修改都会触发响应式更新

    1
    2
    3
    4
    // 所有这些都是响应式的
    state.count++; // 基本类型
    state.user.name = 'Bob'; // 嵌套对象
    state.hobbies.push('coding'); // 数组
  • 处理对象:专门用于对象类型(Object, Array, Map, Set等)

  • Proxy 实现:其核心基于 ES6 的 Proxy。它拦截对对象的各种操作(如 get, set, deleteProperty 等),从而实现依赖追踪和触发更新

  • 深度响应式:默认是深度响应的。即使嵌套很深的对象或数组,它们的修改也会被追踪到

  • 访问方式:直接访问属性,无需额外语法。state.count

  • 局限性:

    • 不能用于原始值(如 string, number, boolean
    • 如果你将响应式对象的属性解构赋值给一个局部变量,该变量的访问和修改将会失去响应性
    1
    2
    3
    4
    5
    6
    // 错误:解构会使数据失去响应性
    const { count, user } = state;
    count++; // 不会触发视图更新!

    // 正确:始终通过 `state.count` 来访问和修改
    state.count++;
    • 为了解决解构丢失响应性的问题,可以使用 toRefs 工具函数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import { reactive, toRefs } from 'vue';

    const state = reactive({ count: 0, name: 'Alice' });

    // `toRefs` 将响应式对象的每个属性都转换为一个 ref
    const { count, name } = toRefs(state);

    // 现在 count 和 name 都是 ref,需要使用 .value 访问
    console.log(count.value); // 0
    count.value++; // 有效,并且会更新源对象 state.count

ref

  • ref() 用于创建一个响应式的“引用”。它可以持有任何类型的值,包括原始值和对象

  • 它接收一个内部值(可以是原始值或对象),并返回一个响应式的、可变的 ref 对象。这个 ref 对象只有一个 .value 属性,指向其内部值

    1
    2
    3
    4
    import { ref } from 'vue';

    const count = ref(0); // 持有原始值
    const objectRef = ref({ name: 'Alice' }); // 持有对象
  • 访问和修改都必须通过 .value 属性

    1
    2
    3
    4
    5
    6
    // 读取值
    console.log(count.value); // 0

    // 修改值
    count.value = 1;
    objectRef.value.name = 'Bob'; // 修改对象属性
  • 处理任何类型:既可以处理原始值,也可以处理对象。

  • .value 访问:这是 ref 最显著的特征。在 JavaScript 中需要通过 .value 来操作数据。

  • 模板中自动解包:在模板中,你不需要.value。Vue 会自动解包顶层 ref。

    1
    2
    3
    4
    <template>
    <div>{{ count }}</div> <!-- 无需 .value,直接写 count -->
    <button @click="count++">Increment</button> <!-- 这里也会自动解包 -->
    </template>
  • 实现机制:

    • 当持有原始值时,Vue 通过对象的 getter/setter 来拦截 .value 的访问和修改,实现响应性
    • 当持有对象时,它内部会调用 reactive() 来深度转换这个对象,使其也变成响应式的。所以 const objRef = ref({}) 等价于 ref(reactive({}))

watchEffect、watch

​ Vue 3 中两个用于执行副作用(Side Effects) 的核心函数:watchEffectwatch。它们是 Composition API 中响应式系统的重要组成部分,用于响应数据变化并执行相应的操作

  • 副作用是指一段代码的执行会影响或依赖于函数外部状态的操作。在前端开发中,典型的副作用包括:

    • 操作 DOM

    • 发送异步请求(API calls)

    • 操作浏览器存储(LocalStorage, Cookies)

    • 打印日志(console.log)**

watchEffectwatch 的作用就是在响应式数据发生变化时,自动、高效地执行这些副作用

watchEffect

  • watchEffect立即执行传入的函数,并在执行过程中自动追踪其依赖的所有响应式数据(如 ref, reactive 的属性)。当这些依赖项的任何一方发生变化时,该函数会再次自动执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import { ref, watchEffect } from 'vue';

    const count = ref(0);
    const name = ref('Alice');

    // watchEffect 会立即执行一次
    watchEffect(() => {
    // 这个函数内部使用了哪些响应式数据,Vue 就会自动把它们作为依赖收集起来
    console.log(`Count is: ${count.value}, Name is: ${name.value}`);
    // 副作用,比如操作DOM:
    // document.title = `Count: ${count.value}`;
    });

    // 以下操作会触发上面的 effect 重新执行:
    count.value++; // 输出: Count is: 1, Name is: Alice
    name.value = 'Bob'; // 输出: Count is: 1, Name is: Bob
  • 核心特点:

    1. 自动依赖追踪:需要手动指定要监听哪些数据。Vue 在副作用函数第一次运行时,会记录下函数中用到了哪些响应式属性。这是它最方便的特性
    2. 立即执行:它会在初始化时立即执行一次,确保副作用在最初就能生效
    3. 无效回调(onInvalidate):副作用函数可以接收一个 onInvalidate 函数作为参数。这个函数用于在副作用即将重新执行或组件卸载时,清理上一次副作用留下的资源(如取消未完成的异步请求)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    watchEffect(async (onInvalidate) => {
    // 模拟一个异步请求
    const data = await fetchData(count.value);

    onInvalidate(() => {
    // 这个清理函数会在两种情况下被调用:
    // 1. 副作用即将重新执行(因为 count 又变了)
    // 2. 组件被卸载
    // 你可以在这里取消之前的请求
    cancelRequest();
    });

    // 处理 data...
    });

watch

  • watch 的功能更接近 Vue 2 中的 watch 选项。它需要显式指定要监听的一个或多个数据源,并在数据源变化时执行指定的回调函数。它默认是惰性的,不会立即执行。

  • 监听单个数据源
    数据源可以是一个 getter 函数,或者一个 ref

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import { ref, reactive, watch } from 'vue';

    // 监听一个 ref
    const count = ref(0);
    watch(count, (newValue, oldValue) => {
    console.log(`count changed from ${oldValue} to ${newValue}`);
    });

    // 监听一个 reactive 对象的某个属性(需要使用getter函数)
    const state = reactive({ name: 'Alice', age: 30 });
    watch(
    () => state.name, // 数据源:一个返回具体值的getter函数
    (newName, oldName, onInvalidate) => {
    console.log(`Name changed from ${oldName} to ${newName}`);
    }
    );

    // 触发变化
    count.value++; // 输出: count changed from 0 to 1
    state.name = 'Bob'; // 输出: Name changed from Alice to Bob
  • 监听多个数据源
    传入一个数组,回调函数参数也对应为数组

    1
    2
    3
    4
    5
    6
    7
    watch(
    [() => state.name, count], // 数据源数组:一个getter和一个ref
    ([newName, newCount], [oldName, oldCount]) => {
    console.log(`Name: ${oldName} -> ${newName}`);
    console.log(`Count: ${oldCount} -> ${newCount}`);
    }
    );
  • 深度监听和立即执行
    watch 的第三个参数是一个可选配置对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const deepObj = reactive({ nested: { value: 1 } });

    watch(
    () => deepObj,
    (newValue, oldValue) => {
    // 默认情况下, reactive 对象的属性变化不会触发,
    // 因为新旧值指向同一个对象引用。
    },
    {
    deep: true, // 开启深度监听,嵌套属性变化也会触发
    immediate: true // 立即以当前值执行一次回调(类似 watchEffect 的初始执行)
    }
    );

    deepObj.nested.value = 2; // 在 deep: true 时,会触发回调
  • 核心特点:

    1. 显式指定源:你必须明确告诉 Vue 要监听哪个或哪些值
    2. 惰性:默认不会在初始化时执行,只有在监听源发生变化时才会执行回调
    3. 访问新旧值:回调函数会提供变化前后的值,这对于比较逻辑非常有用
    4. 更具体控制:通过配置 deepimmediate 等选项,可以更精细地控制监听行为

emit、prop

在深入细节之前,必须理解 Vue 的一个核心设计原则:单向数据流

  • 数据向下:父组件通过 props 将数据传递给子组件
  • 事件向上:子组件通过 emits 发送事件通知父组件

Props

  • Props 是父组件向子组件传递数据的一种方式。你可以将它理解为函数的参数

  • 父组件 (Parent.vue): 使用 v-bind(或简写 :)来传递数据

    1
    2
    3
    <template>
    <ChildComponent :title="postTitle" :likes="42" :is-published="true" />
    </template>
  • 子组件 (ChildComponent.vue): 使用 defineProps 来声明接收的属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <template>
    <div>{{ title }} - {{ likes }} 个赞</div>
    </template>

    <script setup>
    // 使用 defineProps 来声明 props
    const props = defineProps({
    title: String,
    likes: Number,
    isPublished: Boolean
    })
    // 在 JS 中访问需要使用 props.title
    console.log(props.title)
    </script>

​ 在模板中可以直接使用 title,在 JavaScript 中则需要通过 props.title 访问

  • 单向传递:Props 是只读的。子组件绝对不能直接修改一个 prop(如 props.likes = 43)。如果试图修改,Vue 会在控制台发出警告。这是为了防止子组件意外改变父组件的状态,从而导致数据流变得难以理解

  • Prop 验证:你可以为 props 提供详细的验证要求,这在开发大型应用或组件库时非常有用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    defineProps({
    title: String, // 基础类型检查
    likes: Number,
    // 多个可能的类型
    propA: [String, Number],
    // 必填的字符串
    propB: {
    type: String,
    required: true
    },
    // 带有默认值的数字
    propC: {
    type: Number,
    default: 100
    },
    // 自定义验证函数
    propD: {
    validator(value) {
    // 值必须匹配下列字符串中的一个
    return ['success', 'warning', 'danger'].includes(value)
    }
    }
    })
  • 命名规范:在父组件模板中传递 prop 时,应使用 kebab-case(短横线分隔命名),如 <MyComponent greeting-message="hello" />。在子组件 defineProps 内部声明时,使用 camelCase(驼峰命名)

Emits

  • Emits 是子组件向父组件通信的方式。当子组件中发生了某些事情(如按钮被点击、输入完成等),它可以通过触发(emit)一个自定义事件来通知父组件

  • 子组件 (ChildComponent.vue): 使用 defineEmits 声明要触发的事件,然后调用 emit('事件名', 参数)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <template>
    <button @click="onButtonClick">点赞</button>
    </template>

    <script setup>
    // 声明触发的事件
    const emit = defineEmits(['incrementLikes'])

    function onButtonClick() {
    // 当按钮被点击时,触发 'incrementLikes' 事件,并传递一个值
    emit('incrementLikes', 1)
    }
    </script>
  • 父组件 (Parent.vue): 使用 v-on(或简写 @)来监听子组件发出的事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <template>
    <ChildComponent @increment-likes="handleLikesIncrease" />
    </template>

    <script setup>
    function handleLikesIncrease(incrementBy) {
    // 当收到子组件的事件后,更新父组件自己的状态
    // 这才是数据应该被修改的地方!
    postLikes.value += incrementBy
    }
    </script>
  • 命名规范:与 props 不同,事件名不存在任何自动化的大小写转换。建议始终使用 kebab-case(短横线分隔命名)作为事件名,因为 HTML 属性是大小写不敏感的。@my-event 无法被监听成 @myEvent


computed

  • computed` 是 Vue Composition API 中用于创建计算属性的函数。计算属性是基于其它响应式数据计算衍生出来的值

  • 它的核心思想是:声明式地描述一个值如何依赖其它值

  • 可以把它想象成一个高效的、自动缓存的计算器。它只会在其依赖的响应式数据发生变化时才会重新计算,否则会直接返回上一次缓存的计算结果

  • 最常见的用法是只提供一个 get 函数,此时计算属性是只读

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import { ref, computed } from 'vue';

    const firstName = ref('张');
    const lastName = ref('三');

    // 创建一个计算属性 fullName
    // 它接收一个 getter 函数
    const fullName = computed(() => {
    console.log('计算属性重新计算了!'); // 依赖变化时才会打印
    return `${firstName.value}${lastName.value}`;
    });

    console.log(fullName.value); // 输出:'张三',同时打印 '计算属性重新计算了!'
    console.log(fullName.value); // 输出:'张三',但不会打印!因为使用了缓存。

    firstName.value = '李'; // 修改依赖项

    console.log(fullName.value); // 输出:'李三',同时打印 '计算属性重新计算了!'
  • 关键特性:

    1. 响应式fullName 本身是一个 ref 对象,需要通过 .value 访问,在模板中会自动解包
    2. 缓存:只要 firstNamelastName 没变,多次访问 fullName.value 不会再次执行计算函数,而是直接返回缓存的值。这是它与普通方法最根本的区别
    3. 自动依赖追踪:和 watchEffect 类似,Vue 会自动追踪 getter 函数内部使用了哪些响应式数据,并建立依赖关系

getset 的用途

  • 上面的例子只使用了 getter,所以计算属性是只读的。如果你尝试 fullName.value = '王五',将会导致一个错误

  • 但有时候,我们需要一个可写的计算属性。例如,你想同时允许设置 fullName,并且这个设置操作会反向分解并更新它依赖的 firstNamelastName

  • 这时,你就需要为 computed 传入一个包含 getset 函数的对象,而不是单个 getter 函数

    • get 函数

      • 作用获取计算属性的值。当有人读取 computedRef.value 时,此函数被调用

      • 职责:根据依赖的响应式数据,计算并返回最终的值

    • set 函数

      • 作用设置计算属性的值。当有人尝试给 computedRef.value 赋值(如 fullName.value = '王 五')时,此函数被调用

      • 参数:接收一个参数,即试图设置的新值

      • 职责:通常不是直接改变计算属性本身(因为它的值是由 get 函数决定的),而是根据新值去修改计算属性所依赖的源数据

  • 下面的例子展示了如何使用 getset 创建一个双向绑定的计算属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    import { ref, computed } from 'vue';

    // 原始的源数据
    const firstName = ref('张');
    const lastName = ref('三');

    // 可写的计算属性
    const fullName = computed({
    // Getter:当读取 fullName.value 时工作
    get() {
    return `${firstName.value}${lastName.value}`;
    },
    // Setter:当设置 fullName.value 时工作
    set(newValue) {
    // 当 fullName 被设置时,
    // 我们需要将新值分解并更新它所依赖的源数据(firstName 和 lastName)
    console.log(`有人试图将 fullName 设置为: ${newValue}`);

    // 假设新值的格式是 "姓 名"(中间有空格)
    const names = newValue.split(' ');
    if (names.length >= 2) {
    firstName.value = names[0]; // 更新依赖的源数据
    lastName.value = names[1]; // 更新依赖的源数据
    }
    // 注意:这里没有直接设置 fullName 的值,
    // 因为下次读取 fullName.value 时,get() 会根据新的 firstName 和 lastName 重新计算。
    }
    });

    // 1. 读取 get()
    console.log(fullName.value); // 输出:'张三'

    // 2. 写入 set()
    fullName.value = '王 五'; // 这会触发 set('王 五')

    // 3. Setter 函数内部更新了 firstName 和 lastName
    console.log(firstName.value); // 输出:'王'
    console.log(lastName.value); // 输出:'五'

    // 4. 现在再读取 get(),得到的自然是新计算的值
    console.log(fullName.value); // 输出:'王五'

provide、inject

  • provideinject 是 Vue 提供的一种依赖注入机制,它允许:

    • 祖先组件通过 provide 向其所有后代组件(无论嵌套多深)”提供”数据或方法

    • 任何后代组件都可以通过 inject 来”注入”并接收这些数据或方法

Provide

在祖先组件中,使用 provide 函数来提供数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { ref, provide } from 'vue'

const user = ref('张三')
const count = ref(0)

// 提供静态值
provide('siteName', '我的网站')

// 提供响应式数据
provide('user', user)
provide('count', count)

// 提供方法
function updateUser(newUser) {
user.value = newUser
}
provide('updateUser', updateUser)

Inject

在后代组件中,使用 inject 函数来注入需要的数据。

1
2
3
4
5
6
7
8
9
10
import { inject } from 'vue'

// 注入数据
const siteName = inject('siteName')
const user = inject('user')
const updateUser = inject('updateUser')

// 使用注入的数据和方法
console.log(siteName)
updateUser('李四')

示例

祖先组件 (App.vue)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup>
import { ref, provide } from 'vue'
import ChildComponent from './ChildComponent.vue'

const theme = ref('light')
const user = ref({ name: '张三', age: 25 })

// 提供主题和用户信息
provide('theme', theme)
provide('user', user)

// 提供切换主题的方法
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide('toggleTheme', toggleTheme)
</script>

<template>
<div :class="theme">
<ChildComponent />
</div>
</template>

后代组件 (DeepChild.vue)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup>
import { inject } from 'vue'

// 注入需要的数据和方法
const theme = inject('theme')
const user = inject('user')
const toggleTheme = inject('toggleTheme')
</script>

<template>
<div>
<p>当前主题: {{ theme }}</p>
<p>用户名: {{ user.name }}</p>
<button @click="toggleTheme">切换主题</button>
</div>
</template>

nextTick

  • 简单一句话:nextTick 用来等 Vue 把响应式变更渲染到 DOM(并完成本轮的更新队列)后再执行代码。它保证你在 JS 中读写 DOM 时拿到的是更新后的真实 DOM 状态

  • Vue 的 响应式数据(ref/reactive)在 JS 层是即时更新的,但 把这些变化反映到真实 DOM 是异步、可批量合并的(为性能把多次变更合并在一轮渲染里)

  • nextTick 的作用是把回调排到「Vue 完成本轮渲染并把修改更新到 DOM 之后」再执行

  • 在 Vue 3 中,你可以 import { nextTick } from 'vue',如果不给回调它会返回一个 Promise,因此可以 await nextTick()

  • 常见使用场景

    • 在显示/隐藏元素后立即访问该元素的 DOM(如 focus()getBoundingClientRect()

    • 在条件渲染后触发动画前需要先确保 DOM 已存在

    • 在单元测试里等待组件渲染完成再断言

    • 读写依赖于最终 DOM 布局的值(高度、宽度、位置)

    • 当你多次修改响应式值并需要在渲染完成后执行一次后续工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// MyComponent.vue
<script setup>
import { ref, nextTick } from 'vue';

const visible = ref(false);
const inputRef = ref(null);

async function open() {
visible.value = true; // 视图需要显示 input
await nextTick(); // 等 Vue 把 input 渲染到 DOM
inputRef.value?.focus(); // 现在安全执行 DOM 操作
}
</script>

<template>
<div>
<button @click="open">Open</button>
<div v-if="visible">
<input ref="inputRef" />
</div>
</div>
</template>

使用回调或 Promise

1
2
3
4
5
6
7
// 回调形式
nextTick(() => {
// DOM 已更新
});

// Promise/await 形式(更常用)
await nextTick();