跳转到内容

Vue 基础

四个阶段:创建、挂载、更新、卸载

Section titled “四个阶段:创建、挂载、更新、卸载”

创建阶段:setup(组合式)、beforeCreate -> created(选项式)

挂载阶段:beforeMount -> mounted

更新阶段:beforeUpdate -> updated

卸载阶段:beforeUnmount -> unmounted

如下图:

父子组件生命周期

有如下结论:

  1. 创建/挂载阶段:父组件挂载开始时才会对子组件进行创建以及挂载,父组件下所有子组件挂载完成后,才会出发父组件的mounted钩子

  2. 更新/卸载阶段:父组件执行更新/卸载 -> 子组件执行更新/卸载 -> 子组件更新/卸载完成 -> 父组件更新/卸载完成

组合式API中对响应式变量进行定义有两个API: refreactive

两者用法有如下差异:

  • 使用ref声明的变量内部数据在setup函数中需要通过.value进行访问,但在组件template中使用则不需要;调用reactive返回一个Proxy类型的对象,可以直接访问属性值。
  • reactive只能用于对象类型, 不能用于基本数据类型

计算属性对象是一种特殊的响应式对象,在组合式API中,传入computed的是一个函数,函数的返回值是一个根据其他响应式变量(定义的ref/reactive变量/组件外部传入的props等)计算的结果。调用computed返回的结果是一个只读的Ref对象,在setup函数中同样需要通过.value属性访问内部值。

单向绑定:v-bind(父组件传入子组件)

双向绑定:v-model(语法糖,等价于v-bind + 自定义事件)

组合式API相关能力:defineModel

vue3的响应式系统默认是深度的,对于层级较深的复杂对象而言,可能会造成一部分性能负担。

有的场景下我们不需要对复杂对象进行深层次监听,为此vue3提供了shallowRefshallowReactive这两个API

  1. shallowRef

    • 场景:大型对象/数组的整体替换、第三方库实例(如DOM元素)管理、与渲染无关的临时状态。

    • 收益:跳过深层响应式转换,仅响应.value引用变化,提升初始化性能,减少内存开销。

  2. shallowReactive

    • 场景:仅需响应顶层字段变化(如高频更新的坐标数据)、配合不可变数据结构、表单配置对象。

    • 收益:只追踪第一层属性,避免嵌套属性的冗余依赖收集,优化高频更新性能。

通过这两个API能精准控制响应层级,避免深度递归的性能损耗,适用于数据量大而复杂、更新频繁或非深度响应场景。

  1. 为什么data属性是一个函数而不是一个对象?

    vue2中,data属性定义为函数,vue会把函数调用的结果作为组件实例对象的数据,保证同一组件的不同实例对象数据不互相干扰。

    另外 vue3目前已经不再支持data属性直接定义一个对象,会有警告: The data option must be a function. Plain object usage is no longer supported.。源码见:core/packages/runtime-core/src /componentOptions.ts

场景:多组件共享数据、某个组件数据更新后其他组件需要进行感知

分类:

  • 父子组件通信:

    • 父 -> 子: propsprovide/inject

    • 子 -> 父: 自定义事件(emit/on)

  • 跨级组件通信:

    • 根组件 -> 子组件:provide/injectattrs

    • 其他情形:状态管理工具(VuexPinia)

v-if用于根据表达式的真假值来有条件地渲染一块内容。这块内容只会在指令的表达式返回真值时被渲染。

v-if是“真实的”条件渲染,因为它会确保在切换过程中,条件块内的事件监听器和子组件适当地被销毁和重建。它也是惰性的:如果在初始渲染时条件为假,则什么也不做,直到条件第一次变为真时,才会开始渲染条件块。

因为v-if是一个指令,它必须附着在一个元素上。如果想切换多个元素,可以用一个<template>元素作为不可见的包装器,并在其上使用v-if。最终的渲染结果将不包含<template>元素。如:

<template v-if="isVisible">
<h1>标题</h1>
<p>段落一</p>
<p>段落二</p>
</template>

v-for指令用于基于一个数组或对象来循环渲染一个列表。

v-for循环中,Vue强烈建议为每个迭代的元素提供一个唯一的key。这个keyVue用来识别节点、跟踪每个节点身份、重用和重新排序现有元素的重要机制。它对于列表的性能优化和状态维护(如表单输入值)至关重要。

为了优化大型列表渲染的场景,Vue还提供了v-memo指令。使用例子如下:

<template>
<div>
<div
v-for="user in users"
:key="user.id"
v-memo="[user.id, user.name, user.avatarUrl, user.email, user.department]"
class="user-item"
>
<Avatar :src="user.avatarUrl" />
<span>{{ user.name }}</span>
<span>{{ user.email }}</span>
<span>{{ user.department }}</span>
<!-- ... 其他复杂内容 ... -->
</div>
</div>
</template>

对于每一个列表项,Vue会缓存它最后一次渲染的结果。在下次更新时,它会对比v-memo依赖数组中所有的值(user.id, user.name 等):

  • 如果所有值都完全一样:Vue会完全跳过这个<div>及其所有子组件(如<Avatar>)的虚拟DOM生成和diff过程,直接复用之前的DOM

  • 如果其中有任何一个值发生了变化:Vue会像正常一样重新渲染该列表项。

v-show用于控制元素的显示/隐藏。

v-show传入值为true,对应元素样式会变为display: none。由于只是样式的切换,所以开销很低。但正是因为v-show只是调整元素的样式,所以不能使用于<template>标签。

适用于元素显示/隐藏切换频繁的场景。

插槽是一种让父组件向子组件传递内容的机制,常用的有四种:

插槽类型描述语法示例
默认插槽未命名的插槽,用于接收未指定插槽名称的内容。子组件:<slot>默认内容</slot>
父组件:<ChildComp>内容</ChildComp><template #default>内容</template>
具名插槽具有名称的插槽,允许将内容定向到组件中的特定位置。子组件:<slot name="header"></slot>
父组件:<template #header>内容</template>
作用域插槽允许子组件向插槽传递数据,父组件可以使用这些数据来定制插槽内容的渲染。子组件:<slot :data="item"></slot>
父组件:<template #default="slotProps">{{ slotProps.data }}</template>
动态插槽插槽的名称可以由变量动态决定,提供更大的灵活性。父组件:<template #[dynamicSlotName]>内容</template>
  1. 获取更新后的DOM状态

    当响应式数据变化后,Vue会异步更新DOMnextTick允许你在DOM更新完成后 执行回调函数,从而安全地访问最新的DOM元素或组件状态。

  2. 解决异步更新导致的逻辑依赖问题

    确保代码在Vue完成一轮数据变更到DOM渲染的流程后执行,避免因DOM未更新而引发的逻辑错误(如读取旧DOM尺寸/位置)。

  1. 在响应式数据变化后

    当修改了组件的refreactive等响应式数据时,Vue会启动一个异步更新队列。nextTick回调会在 该队列中的DOM更新任务全部完成后 触发。

  2. 在生命周期钩子中

    例如updated()钩子中修改数据后,若需立即操作更新后的DOM,应使用nextTick

  3. 在事件处理函数中

    若事件处理函数内修改了数据并需要立即操作新DOM(如聚焦输入框),需在nextTick中执行。

  1. Vue收集同一事件循环内的所有数据变更,合并为一次异步更新(微任务)。

  2. nextTick将回调函数推入微任务队列(优先使用Promise.then,降级方案为setTimeout)。

  3. 当前同步代码执行完毕 → 微任务队列执行(DOM更新) → nextTick回调执行。

组合式API是一套基于函数的API,它允许我们通过导入函数和使用它们的方式来组织组件的逻辑,而不是通过强制性地将代码分布到不同的选项(如data, methods, mounted)中来组织。

相较于选项式API,有如下优点:

  • 更好的逻辑组织与代码可读性:组合式API让我们可以将同一个逻辑关注点的所有代码集中在一起。你可以将属于某个功能的所有响应式数据、计算属性、方法和生命周期钩子都写在一块。这样,要理解一个功能,你只需要看那一个代码块即可,无需上下翻找。这使得代码更易于阅读和维护。

    如:

    <script setup>
    import { ref, onMounted } from 'vue';
    // 功能A的逻辑(可以甚至提取到一个独立的函数中)
    function useFeatureA() {
    const dataA = ref(null);
    function methodA() { /* ... */ };
    onMounted(() => { /* 初始化A */ });
    return { dataA, methodA };
    }
    // 功能B的逻辑
    function useFeatureB() {
    const dataB = ref(null);
    function methodB() { /* ... */ };
    onMounted(() => { /* 初始化B */ });
    return { dataB, methodB };
    }
    const { dataA, methodA } = useFeatureA();
    const { dataB, methodB } = useFeatureB();
    return {
    dataA, methodA,
    dataB, methodB
    };
    </script>
  • 更强的逻辑复用能力:相较于选项式APIMixin,组合式函数进行代码复用更为灵活清晰。组合式函数有如下几个特点:

    • 命名可定制: 在解构时可以重命名,避免冲突。

    • 数据来源清晰: 从哪个组合式函数返回了什么值,一目了然。

    • 可传参、可交互: 组合式函数可以接受参数,不同的组合式函数可以相互嵌套使用,灵活性极高。

  • 更小的生产包体积:组合式函数下,生命周期/响应式变量定义等都是通过组合式函数实现,按需引入即可,对Tree-shaking更为友好。

当我们需要在状态改变时去做一些操作的时候,就需要使用侦听器。Vue提供了watchwatchEffect这两个API来进行侦听器注册。

watch的功能是侦听一个或多个特定的响应式数据源,并在这些数据源发生变化时执行一个回调函数。

如:

import { ref, watch } from 'vue'
const count = ref(0)
const state = reactive({ name: 'Vue', version: 3 })
// 1. 侦听一个 ref
watch(count, (newValue, oldValue) => {
console.log(`count 从 ${oldValue} 变成了 ${newValue}`)
})
// 2. 侦听一个 getter 函数(用于监听 reactive 对象的某个属性)
watch(
() => state.name,
(newName, oldName) => {
console.log(`名字从 ${oldName} 变成了 ${newName}`)
}
)
// 3. 侦听多个源(数组形式)
watch([count, () => state.name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`多个值变了: count=${newCount}, name=${newName}`)
})
// 4. 配置选项
watch(count, (newValue, oldValue) => {
// 回调函数
}, {
immediate: true, // 立即执行一次
deep: true, // 深度遍历,用于监听对象或数组内部的嵌套变化
flush: 'post' // 控制回调的触发时机,例如在 DOM 更新后触发
})

组件初始化时默认不会执行侦听器,可以通过watch第三个参数指定immediate: true

watchEffect会立即执行传入的函数,同时在这个过程中自动追踪函数内部所依赖的所有响应式属性(类似计算属性computed)。并在这些依赖发生任何变化时,重新运行该函数。

watchEffect有如下几个特点:

  • 自动依赖收集:你不需要手动声明依赖,Vue会自动分析回调函数体并收集所有被使用的响应式属性。

  • 立即执行:在组件初始化时或watchEffect被创建时,会立即运行一次,用于收集依赖。

  • 无法获取变化前的值:回调函数只会在依赖变化后运行,你只能拿到当前的最新值。

使用示例如下:

import { ref, watchEffect } from 'vue'
const count = ref(0)
const name = ref('Alice')
// 立即执行,并自动追踪 count.value 和 name.value
watchEffect(() => {
// 这个函数体用到了哪些响应式数据,Vue 就会自动把它们作为依赖
console.log(`效果触发了!Count: ${count.value}, Name: ${name.value}`)
// 常见的用例:发送 API 请求
// fetch(`/api/user/${name.value}?count=${count.value}`)
})
// 当 count 或 name 改变时,上面的 effect 会再次运行。

Vue 3.5以前的版本,我们如果想要获取模板内元素/组件的引用,需要在setup定义一个Ref对象变量,且变量名需要和组件/元素的ref属性传入的字符串对齐。

Vue 3.5以后,可以使用useTemplateRef获取元素/组件的引用,如:

<script setup>
import { useTemplateRef, onMounted } from 'vue'
// 第一个参数必须与模板中的 ref 值匹配
const input = useTemplateRef('my-input')
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="my-input" />
</template>

使用该API的好处有如下几个:

    1. 增加代码可读性:Ref对象一般用于定义响应式变量,但我们在获取组件/元素引用时也是用Ref定义。相较于这种方式,使用useTemplateRef定义在语义上更为清晰。
    1. TypeScript自动推导类型:使用useTemplateRef创建的ref类型可以基于匹配的ref attribute所在的元素自动推断为静态类型,无需手动标注类型。(参考:TypeScript 与组合式 API | Vue.js

provideinject是一对用于实现 “依赖注入” 的 API。它们允许一个祖先组件向其所有子孙组件(无论层级有多深)提供一个依赖(数据、方法、甚至整个实例),而不需要通过props逐层传递。

一般用于祖先组件给子孙组件提供共享数据的场景。如:

<!-- 祖先组件 (Provider) -->
<script setup>
import { provide, ref, reactive } from 'vue'
// 提供静态值
provide('siteName', 'My Awesome Site')
// 提供响应式数据
const user = reactive({ name: 'Alice', id: 123 })
provide('user', user)
// 提供方法
function logout() {
// ... 退出登录逻辑
}
provide('logout', logout)
// 提供 ref
const count = ref(0)
provide('count', count)
</script>
<!-- 子孙组件 (Consumer) -->
<script setup>
import { inject } from 'vue'
// 注入默认值,避免未提供时值为 undefined
const siteName = inject('siteName', 'Default Site Name')
const user = inject('user')
const logout = inject('logout')
const count = inject('count')
// 使用注入的值
console.log(siteName.value)
user.name = 'Bob' // 修改会影响所有注入此数据的组件
</script>

TeleportVue3内置的一个组件,它可以将组件的内容”传送”到DOM中的其他位置,而不受父组件DOM结构的限制。

如:

<template>
<div>
<!-- 其他内容 -->
<Teleport to="body">
<Modal v-if="showModal">
这是一个模态框
</Modal>
</Teleport>
</div>
</template>

通过to属性可以指定组件渲染的真实DOM挂载到的目标容器。

一般如下场景会使用Teleport:

  1. 模态框/对话框

  2. 通知/提示消息

  3. 加载遮罩

  4. 工具提示

  5. 下拉菜单(在某些布局中)

KeepAliveVue3内置的一个组件,它可以缓存包裹在其中的动态组件实例,避免组件在切换时被重复创建和销毁。

当组件被KeepAlive包裹时:

  • 组件实例会被缓存而不是销毁
  • 再次显示时会复用缓存的实例,而不是重新创建
  • 组件的状态(如表单输入、滚动位置等)会被保留
<template>
<KeepAlive>
<component :is="currentComponent" />
</KeepAlive>
</template>

KeepAlive支持以下属性来控制缓存行为:

  1. include - 指定哪些组件需要被缓存
<!-- 缓存名为 ComponentA 和 ComponentB 的组件 -->
<KeepAlive include="ComponentA,ComponentB">
<component :is="currentComponent" />
</KeepAlive>
<!-- 使用数组形式 -->
<KeepAlive :include="['ComponentA', 'ComponentB']">
<component :is="currentComponent" />
</KeepAlive>
<!-- 使用正则表达式 -->
<KeepAlive :include="/Component[AB]/">
<component :is="currentComponent" />
</KeepAlive>
  1. exclude - 指定哪些组件不需要被缓存
<!-- 除了 ComponentC 之外的组件都会被缓存 -->
<KeepAlive exclude="ComponentC">
<component :is="currentComponent" />
</KeepAlive>
  1. max - 限制最多缓存多少个组件实例
<!-- 最多缓存 3 个组件实例 -->
<KeepAlive :max="3">
<component :is="currentComponent" />
</KeepAlive>

KeepAlive缓存的组件会有两个特殊的生命周期钩子:

  • onActivated - 组件被激活时调用(从缓存中恢复)
  • onDeactivated - 组件被停用时调用(进入缓存状态)
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
console.log('组件被激活')
// 可以在这里刷新数据、重新订阅事件等
})
onDeactivated(() => {
console.log('组件被停用')
// 可以在这里清理定时器、取消订阅等
})
</script>
  1. 标签页切换 - 保持各个标签页的状态和数据

  2. 表单填写 - 避免用户在页面切换时丢失已填写的表单数据

  3. 列表页面 - 保持滚动位置、搜索条件、分页状态等

  4. 复杂组件 - 避免重复渲染耗时的组件(如图表、富文本编辑器等)

  5. 路由缓存 - 结合vue-router缓存整个页面组件