环境搭建
统一安装 Node.js 作为基础,然后根据不同项目需求选择脚手架工具。
- 下载安装:前往 Node.js 官网,下载 LTS 版本(稳定版),按指引完成安装。
- 验证安装:
node -v
npm -v
- 配置国内镜像(可选)
npm config set registry https://registry.npmmirror.com
- 安装脚手架工具
Vue 2:以 Vue CLI 为主, 已进入维护模式
npm install -g @vue/cli
vue create my-vue-project
Vue 3:Vite 与 Vue CLI 两种方式 推荐使用 Vite ,执行后,你可以根据提示,为项目配置 TypeScript, JSX, Vue Router, Pinia (新一代状态管理工具), Vitest 等可选功能
npm create vue@latest my-vue-project
- 启动项目:
cd my-vue-project
npm run serve
核心概念
Vue 实例与生命周期
Vue 实例是 Vue 应用的核心。每个 Vue 应用都是通过创建一个 Vue 实例(Vue 2)或应用实例(Vue 3)开始的。这个实例连接了数据模型和 DOM,并提供了生命周期的钩子函数,让你能在实例创建、挂载、更新、销毁等不同阶段注入自己的代码。
- 创建 Vue 实例
Vue 2
// 根实例
new Vue({
el: '#app', // 挂载点
data: { message: 'Hello Vue 2!' },
methods: { ... },
mounted() { console.log('实例已挂载') }
})
Vue 3
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App) // 创建应用实例
app.mount('#app') // 挂载
Vue 3 中不再使用
new Vue(),而是通过createApp返回一个应用实例。组件实例仍然存在,但生命周期概念同样适用。
- 实例属性和方法(以 Vue 2 为例,Vue 3 类似)
每个 Vue 实例都会代理其 data 对象里的所有属性,并暴露一些有用的实例属性和方法,它们以 $ 开头:
| 属性/方法 | 说明 |
|---|---|
| $data | 组件的数据对象 |
| $props | 接收的 props |
| $el | 根 DOM 元素 |
| $refs | 持有注册过 ref 属性的 DOM 元素或组件实例 |
| $watch() | 侦听数据变化 |
| $emit() | 触发自定义事件 |
| $mount() | 手动挂载未指定 el 的实例 |
| $destroy() | 完全销毁一个实例(Vue 2) |
// Vue 2 示例
var vm = new Vue({
data: { a: 1 }
})
console.log(vm.a) // 1,代理了 data 属性
vm.$data.a === vm.a // true
vm.$watch('a', (newVal) => console.log(newVal))
- 生命周期图解
一个 Vue 实例从创建到销毁会经历一系列初始化、编译、挂载、更新、卸载等阶段。下图是官方生命周期的流程图(Vue 2/3 基本一致,部分钩子名称有变化):
beforeCreate
↓
created
↓
beforeMount
↓
mounted
↓ (数据变化时)
beforeUpdate
↓
updated
↓ (调用 vm.$destroy 或组件卸载时)
beforeDestroy (Vue2) / beforeUnmount (Vue3)
↓
destroyed (Vue2) / unmounted (Vue3)
- 生命周期钩子详解
钩子是 Vue 在特定阶段自动调用的函数。你可以在其中添加自己的业务逻辑。
| 钩子 (Vue 2) | 钩子 (Vue 3) | 触发时机 | 典型用途 |
|---|---|---|---|
| beforeCreate | beforeCreate | 实例初始化后,数据观测和事件配置之前 | 插件初始化、不依赖数据的操作(很少用) |
| created | created | 实例创建完成,已设置响应式数据、计算属性、方法,但 DOM 尚未挂载 | 调用 API 获取初始数据、设置定时器 |
| beforeMount | beforeMount | 模板编译/渲染函数首次执行之前,$el 还不存在 | 一般很少用,可在渲染前最后一次修改数据 |
| mounted | mounted | 实例被挂载到 DOM 后($el 已创建),组件的 DOM 已完成渲染 | 操作 DOM、集成第三方库、发起网络请求(但更推荐 created) |
| beforeUpdate | beforeUpdate | 响应式数据变化,DOM 重新渲染之前 | 在更新前访问现有 DOM(如移除事件监听) |
| updated | updated | 数据变化导致 DOM 重新渲染完成后 | 操作更新后的 DOM(注意避免在此修改数据导致死循环) |
| beforeDestroy | beforeUnmount | 实例销毁之前,实例仍然完全可用 | 清理定时器、取消订阅、解绑事件 |
| destroyed | unmounted | 实例销毁后,所有指令解绑,事件监听移除,子实例也销毁 | 做最后的清理工作 |
Vue 3 组合式 API 中的钩子:使用
onMounted、onUpdated、onUnmounted等函数,需要在setup()中调用,并且要提前从vue导入。
<script setup>
import { onMounted, onUnmounted } from 'vue'
onMounted(() => {
console.log('组件已挂载')
})
onUnmounted(() => {
console.log('组件即将卸载')
})
</script>
- 关键点与常见误区
createdvsmountedcreated时还没有 DOM,不能操作$el,但可以访问响应式数据、调用方法。mounted保证子组件也挂载完成,适合依赖 DOM 的操作。
- 不要在
updated中修改数据 否则会触发无限更新循环。如果必须根据状态修改,请使用计算属性或侦听器。 - Vue 3 中
beforeDestroy/destroyed已改名 注意使用beforeUnmount/unmounted,否则钩子不会执行。 - 异步更新队列 数据变化后,DOM 不会立刻更新,而是缓存在队列中异步执行。若想在
data变化后立即操作更新后的 DOM,可以用Vue.nextTick()/nextTick()。
// Vue 2
this.message = 'changed'
this.$nextTick(() => {
// DOM 已更新
})
// Vue 3
import { nextTick } from 'vue'
nextTick(() => { ... })
- 完整示例(Vue 3 + Composition API)
<template>
<div ref="container">{{ count }}</div>
<button @click="count++">增加</button>
</template>
<script setup>
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'
const count = ref(0)
const container = ref(null)
onBeforeMount(() => {
console.log('beforeMount - DOM 还未生成')
})
onMounted(() => {
console.log('mounted - DOM 已挂载', container.value)
})
onBeforeUpdate(() => {
console.log('beforeUpdate - count 即将变化', count.value)
})
onUpdated(() => {
console.log('updated - DOM 已更新', count.value)
})
onBeforeUnmount(() => {
console.log('beforeUnmount - 组件即将卸载')
})
onUnmounted(() => {
console.log('unmounted - 组件已卸载')
})
</script>
模板语法
Vue 的模板语法基于 HTML,它允许你声明式地将 DOM 绑定到底层组件实例的数据。所有 Vue 模板都是合法的 HTML,能够被浏览器和 HTML 解析器正确解析。
核心思想:模板 → 渲染函数 → 虚拟 DOM → 真实 DOM。
- 插值
最基础的文本绑定,使用双花括号 {{ }}:
<template>
<p>{{ message }}</p>
<p>{{ count + 1 }}</p>
<p>{{ isActive ? 'YES' : 'NO' }}</p>
<p>{{ message.split('').reverse().join('') }}</p>
</template>
<script setup>
const message = 'Hello Vue!'
const count = 0
const isActive = true
</script>
注意:
{{ }}内部只能使用单行表达式,不能写语句(如if、for)。它会自动将结果转为字符串。
- 原始 HTML
{{ }} 会转义 HTML 字符,防止 XSS 攻击。如果需要输出真正的 HTML,使用 v-html 指令:
<template>
<div>{{ rawHtml }}</div> <!-- 输出:<span>... -->
<div v-html="rawHtml"></div> <!-- 输出:渲染后的红色文字 -->
</template>
<script setup>
const rawHtml = '<span style="color: red">红色文字</span>'
</script>
警告:动态渲染任意 HTML 很危险,容易导致 XSS 攻击。只对可信内容使用
v-html,永远不要用在用户提供的内容上。
- 属性绑定
{{ }}不能用在 HTML 属性上,需要用 v-bind 指令(可简写为 :):
<template>
<div v-bind:id="dynamicId"></div>
<div :class="className"></div>
<button :disabled="isDisabled">按钮</button>
<!-- 动态绑定多个属性 -->
<img :src="imageSrc" :alt="imageAlt">
</template>
<script setup>
const dynamicId = 'my-id'
const className = 'container'
const isDisabled = true
const imageSrc = 'https://example.com/pic.jpg'
const imageAlt = '示例图片'
</script>
布尔型**属性**
当值为 false 或 null/undefined 时,属性会被移除;值为 true 则保留:
<button :disabled="isDisabled">按钮</button>
<!-- isDisabled = false → <button>按钮</button> -->
<!-- isDisabled = true → <button disabled>按钮</button> -->
动态绑定多个属性(对象形式)
<template>
<div v-bind="attrs"></div>
</template>
<script setup>
const attrs = {
id: 'my-id',
class: 'wrapper',
'data-role': 'toggle'
}
</script>
- 使用 JavaScript 表达式
在 {{ }} 内部和指令的值中(如 :class="..."),可以写任意单个 JavaScript 表达式:
<template>
<p>{{ number + 1 }}</p>
<p>{{ ok ? 'YES' : 'NO' }}</p>
<p>{{ message.split('').reverse().join('') }}</p>
<div :id="`list-${id}`"></div>
<!-- 方法调用(但注意性能,避免在模板中频繁调用) -->
<p>{{ formatDate(date) }}</p>
</template>
不允许:
<!-- 语句,不是表达式 -->
{{ var a = 1 }}
{{ if (ok) { return message } }}
<!-- 不能调用全局变量(如 window、document)除非手动挂载 -->
{{ console.log(message) }}
- 指令 (Directives)
指令是带有 v- 前缀的特殊属性,其值预期是单个 JavaScript 表达式(v-for 和 v-on 除外)。指令的职责是当表达式的值变化时,响应式地更新 DOM。
常用**指令速览**
| 指令 | 作用 | 简写 | 示例 |
|---|---|---|---|
| v-bind | 动态绑定一个或多个属性 | : | :src=”url” |
| v-on | 监听 DOM 事件 | @ | @click=”handleClick” |
| v-model | 表单输入双向绑定 | 无 | <input v-model=”text”> |
| v-if / v-else-if / v-else | 条件渲染(销毁/重建) | 无 | <div v-if=”show”> |
| v-show | 切换 display: none | 无 | <div v-show=”isVisible”> |
| v-for | 列表渲染 | 无 | <li v-for=”item in items”> |
| v-html | 输出原始 HTML | 无 | <div v-html=”htmlString”> |
| v-text | 输出纯文本(少用) | 无 | <span v-text=”msg”></span> |
| v-pre | 跳过编译,直接显示原始内容 | 无 | {{ this will not compile }} |
| v-cloak | 隐藏未编译的 Mustache 标签直到实例准备完毕 | 无 | 配合 CSS [v-cloak] { display: none } |
| v-once | 只渲染一次,后续数据变化不再更新 | 无 | {{ count }} |
- 修饰符 (Modifiers)
以半角句号 . 开头的特殊后缀,用于指出指令应该以特殊方式绑定。例如:
- 事件修饰符:
.stop、.prevent、.capture、.self、.once、.passive
<!-- 阻止单击事件冒泡 -->
<a @click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form @submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 -->
<a @click.stop.prevent="doThat"></a>
- 按键修饰符:
.enter、.tab、.delete、.esc、.space等
<input @keyup.enter="submit">
- 系统修饰符:
.ctrl、.alt、.shift、.meta(Windows 上为 Win 键) - 鼠标修饰符:
.left、.right、.middle - v-model 修饰符:
.lazy(改为change事件同步)、.number(自动转为数字)、.trim(自动去除首尾空格)
<input v-model.number="age">
- 动态参数 (Dynamic Arguments)
从 Vue 2.6.0 开始,可以在指令参数中使用 JavaScript 表达式,用方括号括起来:
<template>
<!-- 动态绑定属性名 -->
<a :[attributeName]="url">Link</a>
<!-- 动态事件名 -->
<button @[eventName]="handleClick">按钮</button>
</template>
<script setup>
const attributeName = 'href'
const eventName = 'click'
const url = 'https://vuejs.org'
</script>
约束:
- 动态参数的值必须是字符串或
null(null表示移除绑定),其他值会触发警告。 - 表达式内部不能有空格和引号,例如
:[key + 'foo']无效。可以用计算属性代替。 - 在 DOM 模板中(直接写在 HTML 文件里)需要避免使用大写字符,因为浏览器会将属性名转为小写。
- 过滤器 (Filters) – Vue 3 已移除
Vue 2 中有过滤器语法 {{ message | capitalize }},可以在花括号和 v-bind 中使用。 Vue 3 移除了过滤器,官方推荐用计算属性或方法替代。
// Vue 2 写法
{{ message | capitalize }}
// Vue 3 替代方案
{{ capitalize(message) }}
// 或使用计算属性
const capitalizedMessage = computed(() => message.value.toUpperCase())
- 模板中的空格与换行
Vue 模板会遵循 HTML 的空白处理规则。多个连续空格通常会被合并为一个。如果需要保留空格,可以使用 或 CSS white-space: pre-wrap。
- 访问全局变量
在模板表达式中,Vue 只允许访问有限的白名单(如 Math、Date)。不能直接访问用户自定义的全局变量(如 window、document)。如果需要,可以在组件中声明为 methods 或 computed 的返回结果。
<template>
<p>{{ Math.floor(price) }}</p> <!-- OK,Math 在白名单中 -->
<p>{{ window.innerWidth }}</p> <!-- 报错 -->
</template>
响应式原理
Vue 的响应式系统是其最核心的特性之一。它使得当数据发生变化时,所有依赖该数据的地方(视图、计算属性、侦听器等)都能自动更新。这套机制在 Vue 2 和 Vue 3 中实现方式不同,但核心思想一致:数据劫持 + 依赖收集 + 派发更新。
- 核心思想:发布-订阅模式
Vue 的响应式系统本质上是一个发布-订阅模式的实现:
- 数据对象:发布者(Publisher)
- 视图/计算属性/侦听器:订阅者(Subscriber,在 Vue 中称为
Watcher) - 依赖管理器:调度中心(
Dep,Dependency)
当数据被读取时,会进行依赖收集——把当前的 Watcher 添加到数据的依赖列表中。当数据被修改时,会触发派发更新——通知所有依赖该数据的 Watcher 执行更新。
- Vue 2 的实现:
Object.defineProperty
Vue 2 通过 Object.defineProperty 递归地将数据对象的属性转换为 getter/setter,从而拦截属性的读取和设置。
简化流程:
- 初始化:遍历
data中的所有属性,对每个属性调用defineReactive。 - getter:当读取属性时,如果存在
Dep.target(当前正在计算的 Watcher),则进行依赖收集(将 Watcher 添加到属性的 Dep 中)。 - setter:当修改属性时,调用属性的 Dep 的
notify方法,通知所有 Watcher 重新求值并更新视图。
核心代码示意(简化版):
function defineReactive(obj, key, val) {
const dep = new Dep() // 依赖管理器
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
dep.depend() // 依赖收集
}
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
dep.notify() // 派发更新
}
})
}
缺点:
- 无法检测对象属性的添加/删除:必须使用
Vue.set/this.$set或Vue.delete。 - 不能直接通过索引修改数组项:例如
arr[0] = newVal不是响应式的,需使用arr.splice或Vue.set。 - 数组的变更检测:Vue 2 通过拦截数组的
push/pop/shift/unshift/splice/sort/reverse方法实现响应式。 - 性能问题:需要递归遍历所有属性,对大型嵌套对象初始化成本高。
- Vue 3 的实现:
Proxy
Vue 3 使用 ES6 的 Proxy 替代 Object.defineProperty。Proxy 可以代理整个对象,而不仅仅是属性,因此能解决 Vue 2 的大部分限制。
优势:
- 直接检测属性的添加和删除:无需
$set/$delete。 - 直接检测数组索引和 length 修改:
arr[0] = 'new'是响应式的。 - 懒代理:Vue 3 只在访问嵌套对象时才将其转为响应式,性能更好。
- 支持更多数据类型:如
Map、Set、WeakMap、WeakSet。
核心实现(简化):
function reactive(target) {
const handler = {
get(target, key, receiver) {
track(target, key) // 依赖收集
const result = Reflect.get(target, key, receiver)
// 如果访问的属性是对象,则递归返回响应式版本(懒代理)
if (typeof result === 'object' && result !== null) {
return reactive(result)
}
return result
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
trigger(target, key) // 派发更新
}
return result
},
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey && result) {
trigger(target, key) // 派发更新
}
return result
}
}
return new Proxy(target, handler)
}
注意:
Proxy只能代理对象,无法代理原始类型(如number、string),因此 Vue 3 提供了ref包装器。reactive返回的代理对象与原始对象不相等,解构时会失去响应性(需使用toRefs)。
- 依赖收集与 Watcher
无论是 Vue 2 还是 Vue 3,依赖收集的核心机制类似:
- Dep:每个响应式属性都拥有一个 Dep 实例,内部维护一组 Watcher。
- Watcher:负责执行某个表达式(如渲染函数、计算属性、侦听器回调),并在数据变化时重新执行。
- 依赖收集过程:
- 在 Watcher 执行其
getter前,将自身赋值给全局唯一的Dep.target。 - 执行
getter时会访问响应式数据,触发 getter → 调用dep.depend()→ 将当前Dep.target添加到 Dep 的 subs 数组中。 - 执行完毕后,
Dep.target恢复为上一个 Watcher。
- 在 Watcher 执行其
- 派发更新过程:
- 数据变化触发 setter → 调用
dep.notify()。 - 遍历 Dep 中的所有 Watcher,依次调用
watcher.update()。 - 在 Vue 中,Watcher 更新通常是异步的(通过 nextTick 批量处理),以避免频繁 DOM 操作。
- 数据变化触发 setter → 调用
- Vue 3 的响应式 API:
ref与reactive
Vue 3 暴露了更细粒度的响应式 API:
| API | 用途 | 底层实现 |
|---|---|---|
| reactive | 将对象转为响应式代理 | 使用 Proxy |
| ref | 将原始类型或对象包装为响应式引用(通过 .value 访问) | 内部创建一个对象,其 value 属性由 reactive 处理 |
| computed | 创建计算属性,缓存结果 | 创建一个特殊的 Watcher,延迟求值 |
| readonly | 创建只读代理,不允许修改 | 也是 Proxy,setter 中抛出警告 |
| shallowReactive / shallowRef | 只代理顶层属性,深层不转换 | 性能优化 |
使用示例:
import { reactive, ref } from 'vue'
const state = reactive({ count: 0 }) // 深层响应
const count = ref(0) // 原始类型响应式
console.log(count.value) // 读取
count.value++ // 修改
- 响应式原理的流程图
┌─────────────┐
│ Data │
└──────┬──────┘
│
┌──────────────┴──────────────┐
│ defineProperty / Proxy │
└──────────────┬──────────────┘
│
┌────────────┴────────────┐
│ │
getter setter
│ │
依赖收集 (Dep) 派发更新 (Dep)
│ │
添加 Watcher notify Watchers
│ │
Watcher 执行 Watcher 重新执行
│ │
更新视图 / 计算属性 更新视图 / 计算属性
响应式 API
Vue 3 的 Composition API 提供了一套独立的响应式 API,让你可以在任何地方(组件外、组合式函数中)创建和管理响应式状态。这些 API 是 @vue/reactivity 包的核心,Vue 组件内部也完全基于它们构建。
- 核心 API:创建响应式状态
ref() —— 包装任意值
- 作用:接收一个内部值,返回一个响应式且可变的 ref 对象。该对象只有一个
.value属性,指向内部值。 - 适用:原始类型(
string,number,boolean,null,undefined)以及对象(但对象会被自动用reactive转换)。 - 原理:内部通过一个类
RefImpl包装,其.value的 getter/setter 劫持了依赖收集和触发。
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
// 也可以包装对象
const state = ref({ user: 'Alice' })
state.value.user = 'Bob' // 依然响应式
reactive() —— 代理对象
- 作用:接收一个普通对象,返回该对象的响应式代理(基于
Proxy)。所有属性都是响应式的,包括嵌套属性。 - 适用:仅用于对象类型(
object,array,Map,Set)。 - 注意:代理对象与原始对象不相等;解构或直接赋值会丢失响应性。
import { reactive } from 'vue'
const state = reactive({
count: 0,
nested: { foo: 'bar' }
})
state.count++ // 直接修改,视图自动更新
state.nested.foo = 'baz'
computed() —— 计算属性
- 作用:接受一个 getter 函数或带有 get/set 的对象,返回一个只读或可写的 ref 对象。结果会被缓存,仅在依赖变化时重新计算。
- 典型用法:派生状态。
import { ref, computed } from 'vue'
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
// 可写计算属性
const writable = computed({
get: () => count.value + 1,
set: (val) => { count.value = val - 1 }
})
writable.value = 10
console.log(count.value) // 9
- 辅助 API:类型判断与转换
| API | 说明 | 示例 |
|---|---|---|
| isRef(value) | 检查值是否为 ref 对象 | isRef(ref(0)) // true |
| unref(value) | 如果参数是 ref 则返回 .value,否则返回本身 | unref(ref(1)) // 1 unref(2) // 2 |
| toRef(object, key) | 为响应式对象的某个属性创建一个 ref,保持连接 | 解构时使用,避免丢失响应性 |
| toRefs(object) | 将响应式对象的每个属性转换为 ref,返回普通对象 | 用于解构后仍保持响应性 |
| isReactive(value) | 检查值是否由 reactive 创建的代理 | |
| isReadonly(value) | 检查是否只读代理 |
重要:toRefs 和 toRef 解决解构丢失响应性
import { reactive, toRefs, toRef } from 'vue'
const state = reactive({ a: 1, b: 2 })
// ❌ 解构后失去响应性
const { a, b } = state
a = 3 // 不会触发更新
// ✅ 使用 toRefs 包裹后再解构
const { a, b } = toRefs(state)
a.value = 3 // 响应式
// ✅ 单独为某个属性创建 ref
const aRef = toRef(state, 'a')
aRef.value = 3
- 只读 API
readonly()
- 作用:接收一个对象(响应式或普通)或 ref,返回一个只读代理。任何修改都会触发警告。
- 用途:保护数据不被意外修改,如向子组件传递 props 时。
import { reactive, readonly } from 'vue'
const original = reactive({ count: 0 })
const copy = readonly(original)
original.count++ // ✅ 允许
copy.count++ // ❌ 警告:Set operation on key "count" failed: target is readonly.
- 浅层响应式 API(性能优化)
默认情况下,ref 和 reactive 会进行深层响应式转换(递归遍历所有嵌套属性)。如果对象非常庞大且深层数据不需要响应式,可以使用浅层版本提升性能。
| API | 行为 |
|---|---|
| shallowRef(value) | 只有 .value 的访问是响应式的,内部嵌套对象不会被代理。 |
| shallowReactive(obj) | 只有顶层属性是响应式的,嵌套对象是普通对象。 |
import { shallowRef, shallowReactive } from 'vue'
const state = shallowRef({ nested: { a: 1 } })
state.value.nested.a = 2 // ❌ 不会触发更新
state.value = { nested: { a: 2 } } // ✅ 触发更新
const obj = shallowReactive({ foo: { bar: 1 } })
obj.foo.bar = 2 // ❌ 不是响应式
obj.foo = { bar: 2 } // ✅ 响应式
注意:Vue 3 还提供了
triggerRef()可以强制触发shallowRef的副作用(通常用于手动更新)。
- 高级 API
customRef() —— 自定义 ref 控制依赖追踪和更新
- 用途:创建一个自定义的 ref,显式控制依赖收集和派发更新(例如防抖、节流)。
import { customRef } from 'vue'
function debouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => ({
get() {
track() // 依赖收集
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger() // 派发更新
}, delay)
}
}))
}
markRaw() —— 标记永不转为响应式的对象
- 作用:返回对象本身,并阻止 Vue 将其转换为响应式代理。常用于第三方类实例、大型不可变数据等。
import { markRaw, reactive } from 'vue'
const foo = markRaw({ a: 1 })
const state = reactive({ foo })
state.foo.a = 2 // 修改不会触发响应式更新
- 侦听器 API(响应式副作用)
watch()
- 作用:侦听一个或多个响应式源(ref、reactive 对象、getter 函数),在源变化时执行回调。
- 特点:懒执行(默认首次不运行),可以访问旧值和新值。
import { ref, watch } from 'vue'
const count = ref(0)
const stop = watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`)
})
count.value++ // 触发回调
// 侦听多个源
watch([count, name], ([newCount, newName], [oldCount, oldName]) => { ... })
// 侦听 reactive 对象的某个属性(使用 getter)
const state = reactive({ a: 1 })
watch(() => state.a, (newVal) => { ... })
watchEffect()
- 作用:立即运行一个函数,并自动收集其内部所有响应式依赖,依赖变化时重新运行。
- 特点:自动追踪依赖,无需指定源;立即执行;不提供旧值。
import { ref, watchEffect } from 'vue'
const count = ref(0)
watchEffect(() => {
console.log(`count is ${count.value}`) // 立即打印 "count is 0"
})
count.value++ // 打印 "count is 1"
停止侦听:watch 和 watchEffect 都返回一个停止函数,调用即可停止。
- 与 Vue 2 的差异对比
| 功能 | Vue 2 (Options API) | Vue 3 (Composition API) |
|---|---|---|
| 声明响应式数据 | data() 返回对象 | ref() / reactive() |
| 对象添加属性 | this.$set(obj, key, value) | 直接赋值(reactive 代理对象) |
| 删除属性 | this.$delete(obj, key) | delete obj.key |
| 数组索引修改 | this.$set(arr, idx, val) | arr[idx] = val |
| 只读数据 | 无原生支持 | readonly() |
| 自定义 ref | 无 | customRef() |
| 标记原始对象 | 无 | markRaw() |
条件与循环渲染
以下是 Vue 中条件渲染与循环渲染的核心代码示例,均以代码块形式呈现,并保持合理的换行与缩进。
一、条件渲染
1. v-if / v-else-if / v-else
<template>
<div>
<!-- 简单 v-if -->
<p v-if="isLoggedIn">欢迎回来,用户!</p>
<p v-else>请先登录。</p>
<!-- v-else-if 链 -->
<div v-if="status === 'loading'">加载中...</div>
<div v-else-if="status === 'error'">出错了!</div>
<div v-else-if="status === 'success'">数据加载成功</div>
<div v-else>未知状态</div>
<!-- 使用 <template> 批量条件渲染 -->
<template v-if="showSection">
<h2>标题</h2>
<p>段落内容</p>
<button>操作按钮</button>
</template>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isLoggedIn = ref(false)
const status = ref('loading')
const showSection = ref(true)
</script>
2. v-show
<template> <div> <!-- v-show 只是切换 display: none,元素始终存在 --> <p v-show="isVisible">这个段落会频繁切换显示/隐藏。</p> <button @click="isVisible = !isVisible">切换显示</button> </div></template><script setup>import { ref } from 'vue'const isVisible = ref(true)</script>
3. v-if vs v-show 对比示例
<template>
<div>
<!-- v-show 只是切换 display: none,元素始终存在 -->
<p v-show="isVisible">这个段落会频繁切换显示/隐藏。</p>
<button @click="isVisible = !isVisible">切换显示</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isVisible = ref(true)
</script>
二、循环渲染
- 基本
v-for遍历数组
<template>
<div>
<ul>
<!-- 标准写法:(item, index) in items -->
<li v-for="(item, index) in items" :key="index">
{{ index }} - {{ item.name }}
</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 1, name: '苹果' },
{ id: 2, name: '香蕉' },
{ id: 3, name: '橘子' }
])
</script>
- 遍历对象属性
<template>
<div>
<ul>
<!-- (value, key, index) in object -->
<li v-for="(value, key, index) in userInfo" :key="key">
{{ index }}. {{ key }}: {{ value }}
</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue'
const userInfo = ref({
name: '张三',
age: 25,
city: '北京'
})
</script>
- 遍历数字范围
<template>
<div>
<span v-for="n in 10" :key="n">{{ n }} </span>
<!-- 输出:1 2 3 4 5 6 7 8 9 10 -->
</div>
</template>
- 使用
<template>包裹多个元素进行循环
<template>
<div>
<template v-for="(item, index) in list" :key="item.id">
<h3>{{ index }}. {{ item.title }}</h3>
<p>{{ item.description }}</p>
<hr />
</template>
</div>
</template>
<script setup>
import { ref } from 'vue'
const list = ref([
{ id: 1, title: '标题1', description: '描述1' },
{ id: 2, title: '标题2', description: '描述2' }
])
</script>
三、key 的作用与正确使用
<template>
<div>
<!-- ❌ 不推荐:使用 index 作为 key(列表顺序可能变化时会有问题) -->
<div v-for="(item, index) in items" :key="index">
{{ item.name }}
</div>
<!-- ✅ 推荐:使用唯一且稳定的 id 作为 key -->
<div v-for="item in items" :key="item.id">
{{ item.name }}
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 101, name: '项目A' },
{ id: 102, name: '项目B' }
])
</script>
四、v-for 与 v-if 同时使用的注意事项
Vue 3 中
v-for优先级高于v-if,不推荐在同一元素上同时使用,应通过计算属性或<template>解决。
<template>
<div>
<!-- ❌ 错误:v-for 和 v-if 同时用在同一个元素上 -->
<div v-for="item in items" v-if="item.isActive" :key="item.id">
{{ item.name }}
</div>
<!-- ✅ 方法1:使用计算属性过滤列表 -->
<div v-for="item in activeItems" :key="item.id">
{{ item.name }}
</div>
<!-- ✅ 方法2:在外层包裹 <template> 将 v-if 提升 -->
<template v-for="item in items" :key="item.id">
<div v-if="item.isActive">
{{ item.name }}
</div>
</template>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const items = ref([
{ id: 1, name: '商品1', isActive: true },
{ id: 2, name: '商品2', isActive: false },
{ id: 3, name: '商品3', isActive: true }
])
// 计算属性返回已过滤的列表
const activeItems = computed(() => {
return items.value.filter(item => item.isActive)
})
</script>
五、综合示例:条件 + 循环 + 动态样式
<template>
<div>
<h2>任务列表</h2>
<ul>
<li
v-for="task in tasks"
:key="task.id"
:style="{ color: task.completed ? 'gray' : 'black', textDecoration: task.completed ? 'line-through' : 'none' }"
>
<input type="checkbox" v-model="task.completed" />
{{ task.name }}
<button v-if="task.priority === 'high'" class="urgent">紧急</button>
</li>
</ul>
<p v-if="tasks.length === 0">暂无任务</p>
<p v-else-if="uncompletedCount === 0">恭喜!所有任务都完成了</p>
<p v-else>还有 {{ uncompletedCount }} 个任务未完成</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const tasks = ref([
{ id: 1, name: '学习 Vue', completed: false, priority: 'high' },
{ id: 2, name: '写文档', completed: true, priority: 'normal' },
{ id: 3, name: '代码审查', completed: false, priority: 'low' }
])
const uncompletedCount = computed(() => {
return tasks.value.filter(t => !t.completed).length
})
</script>
<style scoped>
.urgent {
background-color: red;
color: white;
margin-left: 10px;
}
</style>
事件与表单:
一、事件处理
- 基本用法:
v-on指令 /@简写
<template>
<div>
<!-- 完整写法 -->
<button v-on:click="handleClick">点击我</button>
<!-- 简写 @ -->
<button @click="handleClick">点击我(简写)</button>
</div>
</template>
<script setup>
const handleClick = () => {
alert('按钮被点击了!')
}
</script>
- 内联事件处理器与事件参数
<template>
<div>
<!-- 直接写 JavaScript 语句 -->
<button @click="count++">增加 count</button>
<!-- 调用方法并传递参数 -->
<button @click="sayHello('Vue')">打招呼</button>
<!-- 访问原始 DOM 事件对象:$event -->
<button @click="handleClickWithEvent($event, '参数')">传递事件对象</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const sayHello = (name) => {
alert(`Hello, ${name}!`)
}
const handleClickWithEvent = (event, message) => {
console.log('事件类型:', event.type)
console.log('自定义参数:', message)
}
</script>
- 事件修饰符
修饰符可以串联,且顺序会影响行为。
<template>
<div>
<!-- .stop:阻止冒泡 -->
<div @click="parentClick">
<button @click.stop="childClick">阻止冒泡</button>
</div>
<!-- .prevent:阻止默认行为(如表单提交) -->
<form @submit.prevent="onSubmit">
<button type="submit">提交</button>
</form>
<!-- .once:只触发一次 -->
<button @click.once="onceHandler">只生效一次</button>
<!-- .capture:捕获阶段触发 -->
<div @click.capture="captureHandler">
<button>捕获阶段</button>
</div>
<!-- .self:只有 event.target 是元素自身时触发 -->
<div @click.self="selfHandler">
<button>点我不会触发 selfHandler</button>
</div>
<!-- .passive:提升滚动性能,尤其用于 touchmove -->
<div @touchmove.passive="onTouchMove">滚动优化</div>
</div>
</template>
<script setup>
const parentClick = () => console.log('父级点击')
const childClick = () => console.log('子级点击')
const onSubmit = () => console.log('提交')
const onceHandler = () => alert('仅此一次')
const captureHandler = () => console.log('捕获')
const selfHandler = () => console.log('自身点击')
const onTouchMove = () => console.log('移动')
</script>
- 按键修饰符
<template>
<div>
<!-- 监听特定按键:Enter -->
<input @keyup.enter="submit" placeholder="按回车提交" />
<!-- 按键别名:.tab / .delete / .esc / .space / .up / .down / .left / .right -->
<input @keyup.esc="clear" placeholder="按 ESC 清空" />
<!-- 系统修饰符:.ctrl / .alt / .shift / .meta -->
<input @keyup.ctrl.enter="save" placeholder="Ctrl + Enter 保存" />
<!-- 精确按键:只有按下 C 键时触发(无其他修饰键) -->
<input @keyup.exact.c="onlyC" placeholder="仅按下 C 键" />
</div>
</template>
<script setup>
const submit = () => alert('提交')
const clear = (e) => (e.target.value = '')
const save = () => alert('保存')
const onlyC = () => alert('按下了 C 键')
</script>
- 鼠标修饰符
<template>
<div>
<button @click.left="leftClick">左键点击</button>
<button @click.right.prevent="rightClick">右键点击(阻止默认菜单)</button>
<button @click.middle="middleClick">中键点击</button>
</div>
</template>
<script setup>
const leftClick = () => console.log('左键')
const rightClick = () => console.log('右键')
const middleClick = () => console.log('中键')
</script>
- 自定义事件(组件通信)
子组件 Child.vue
<template>
<button @click="emitEvent">触发自定义事件</button>
</template>
<script setup>
const emit = defineEmits(['my-event', 'update:modelValue'])
const emitEvent = () => {
emit('my-event', { data: 'hello' })
// 支持 v-model 的自定义事件
emit('update:modelValue', '新值')
}
</script>
父组件 Parent.vue
<template>
<Child
@my-event="handleCustomEvent"
v-model="parentValue"
/>
<p>父组件值:{{ parentValue }}</p>
</template>
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
const parentValue = ref('')
const handleCustomEvent = (payload) => {
console.log('收到子组件事件:', payload)
}
</script>
二、表单输入绑定(v-model)
- 基本用法:文本、多行文本、复选框、单选按钮、选择框
<template>
<div>
<!-- 文本输入框 -->
<input v-model="text" placeholder="输入文本" />
<p>输入的值:{{ text }}</p>
<!-- 多行文本 -->
<textarea v-model="message" placeholder="多行文本"></textarea>
<p>{{ message }}</p>
<!-- 单个复选框(布尔值) -->
<input type="checkbox" v-model="agreed" />
<label>同意协议</label>
<p>同意状态:{{ agreed }}</p>
<!-- 多个复选框(绑定到数组) -->
<input type="checkbox" value="Vue" v-model="hobbies" />
<label>Vue</label>
<input type="checkbox" value="React" v-model="hobbies" />
<label>React</label>
<input type="checkbox" value="Angular" v-model="hobbies" />
<label>Angular</label>
<p>选择的爱好:{{ hobbies }}</p>
<!-- 单选按钮(绑定到相同变量) -->
<input type="radio" value="男" v-model="gender" />
<label>男</label>
<input type="radio" value="女" v-model="gender" />
<label>女</label>
<p>性别:{{ gender }}</p>
<!-- 下拉选择框(单选) -->
<select v-model="selectedCity">
<option disabled value="">请选择城市</option>
<option>北京</option>
<option>上海</option>
<option>广州</option>
</select>
<p>选择的城市:{{ selectedCity }}</p>
<!-- 下拉选择框(多选) -->
<select v-model="selectedColors" multiple>
<option value="红">红色</option>
<option value="绿">绿色</option>
<option value="蓝">蓝色</option>
</select>
<p>选择的颜色:{{ selectedColors }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const text = ref('')
const message = ref('')
const agreed = ref(false)
const hobbies = ref([])
const gender = ref('')
const selectedCity = ref('')
const selectedColors = ref([])
</script>
v-model修饰符
| 修饰符 | 作用 |
|---|---|
| .lazy | 将同步时机从 input 事件改为 change 事件(失焦或回车时更新) |
| .number | 自动将用户输入转为数字类型 |
| .trim | 自动去除首尾空白字符 |
<template>
<div>
<!-- .lazy:失焦后才更新数据 -->
<input v-model.lazy="content" placeholder="失焦后更新" />
<p>延迟绑定:{{ content }}</p>
<!-- .number:自动转数字,若无法转换则原值 -->
<input v-model.number="age" type="number" placeholder="年龄" />
<p>数字类型:{{ typeof age }},值:{{ age }}</p>
<!-- .trim:去除首尾空格 -->
<input v-model.trim="username" placeholder="自动去除空格" />
<p>用户名:'{{ username }}'</p>
<!-- 修饰符可以串联使用 -->
<input v-model.lazy.number="score" placeholder="失焦后转数字" />
</div>
</template>
<script setup>
import { ref } from 'vue'
const content = ref('')
const age = ref(0)
const username = ref('')
const score = ref(0)
</script>
- 在自定义组件上使用
v-model
默认用法(单个 v-model):
子组件 CustomInput.vue
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
父组件使用
<template>
<CustomInput v-model="parentText" />
<p>父组件数据:{{ parentText }}</p>
</template>
<script setup>
import CustomInput from './CustomInput.vue'
import { ref } from 'vue'
const parentText = ref('')
</script>
绑定多个 v-model(带参数)
子组件 CustomForm.vue
<template>
<input
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
placeholder="名"
/>
<input
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
placeholder="姓"
/>
</template>
<script setup>
defineProps(['firstName', 'lastName'])
defineEmits(['update:firstName', 'update:lastName'])
</script>
父组件使用
<template>
<CustomForm
v-model:firstName="first"
v-model:lastName="last"
/>
<p>全名:{{ first }} {{ last }}</p>
</template>
<script setup>
import CustomForm from './CustomForm.vue'
import { ref } from 'vue'
const first = ref('')
const last = ref('')
</script>
三、综合示例:登录表单(事件 + 表单绑定)
<template>
<form @submit.prevent="handleLogin">
<div>
<label>邮箱:</label>
<input
type="email"
v-model.trim="form.email"
required
/>
</div>
<div>
<label>密码:</label>
<input
type="password"
v-model="form.password"
required
/>
</div>
<div>
<label>
<input type="checkbox" v-model="form.remember" />
记住我
</label>
</div>
<div>
<label>角色:</label>
<select v-model="form.role">
<option value="user">普通用户</option>
<option value="admin">管理员</option>
</select>
</div>
<button type="submit">登录</button>
<button type="button" @click="resetForm">重置</button>
</form>
<div v-if="submitted">
<h3>提交的数据:</h3>
<pre>{{ JSON.stringify(form, null, 2) }}</pre>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue'
const form = reactive({
email: '',
password: '',
remember: false,
role: 'user'
})
const submitted = ref(false)
const handleLogin = () => {
submitted.value = true
console.log('登录数据:', form)
// 实际应发送请求到后端
}
const resetForm = () => {
form.email = ''
form.password = ''
form.remember = false
form.role = 'user'
submitted.value = false
}
</script>
<style scoped>
form div {
margin-bottom: 12px;
}
button {
margin-right: 8px;
}
</style>
组件化开发与通信
组件化是 Vue 的核心思想:将界面拆分成独立、可复用的组件,每个组件封装自己的模板、逻辑和样式。组件之间通过明确的接口(props / emit)通信,形成树形结构。
以下内容以 Vue 3 + Composition API 为主,关键处会说明 Vue 2 的差异。
一、组件定义与注册
- 单文件组件(
.vue文件)
<!-- ButtonCounter.vue -->
<template>
<button @click="increment">
点击次数:{{ count }}
</button>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
</script>
<style scoped>
button {
background-color: #42b983;
color: white;
}
</style>
- 全局注册与局部注册
// main.js (全局注册)
import { createApp } from 'vue'
import App from './App.vue'
import GlobalButton from './components/GlobalButton.vue'
const app = createApp(App)
app.component('GlobalButton', GlobalButton)
app.mount('#app')
<!-- 父组件中局部注册(使用 <script setup> 时,导入即自动注册) -->
<template>
<LocalComponent />
</template>
<script setup>
import LocalComponent from './LocalComponent.vue'
// 无需额外注册
</script>
二、父子组件通信
1. props:父传子
父组件 Parent.vue
<template>
<Child
:title="parentTitle"
:count="42"
:user="{ name: 'Alice' }"
/>
</template>
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
const parentTitle = ref('来自父组件的标题')
</script>
子组件 Child.vue
<template>
<div>
<h2>{{ title }}</h2>
<p>数字:{{ count }}</p>
<p>用户名:{{ user.name }}</p>
</div>
</template>
<script setup>
// 声明 props
const props = defineProps({
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0
},
user: {
type: Object,
default: () => ({})
}
})
// 也可以使用数组语法(简单声明)
// defineProps(['title', 'count', 'user'])
// 访问 props
console.log(props.title)
</script>
2. emits:子传父
子组件 Child.vue
<template>
<button @click="sendData">传递数据给父组件</button>
</template>
<script setup>
const emit = defineEmits(['update-data', 'click'])
const sendData = () => {
emit('update-data', { message: 'Hello Parent' })
}
</script>
父组件 Parent.vue
<template>
<Child @update-data="handleUpdate" />
<p>接收到的数据:{{ received }}</p>
</template>
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
const received = ref(null)
const handleUpdate = (payload) => {
received.value = payload.message
}
</script>
3. v-model 在组件上的应用(父子双向绑定)
子组件 CustomInput.vue
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
父组件使用
<template>
<CustomInput v-model="parentText" />
<p>父组件值:{{ parentText }}</p>
</template>
<script setup>
import CustomInput from './CustomInput.vue'
import { ref } from 'vue'
const parentText = ref('')
</script>
绑定多个 v-model(带参数)
<!-- 子组件 -->
<template>
<input
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
/>
<input
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
/>
</template>
<script setup>
defineProps(['firstName', 'lastName'])
defineEmits(['update:firstName', 'update:lastName'])
</script>
<!-- 父组件 -->
<template>
<UserForm
v-model:firstName="first"
v-model:lastName="last"
/>
</template>
三、跨层级通信:provide / inject
适用于祖孙组件传递数据,避免逐层 props 传递。
祖先组件(Provider)
<template>
<div>
<Child />
</div>
</template>
<script setup>
import { provide, ref } from 'vue'
import Child from './Child.vue'
const theme = ref('dark')
const updateTheme = (newTheme) => {
theme.value = newTheme
}
// 提供响应式数据和方法
provide('theme', theme)
provide('updateTheme', updateTheme)
</script>
后代组件(Injector)
<template>
<div :class="theme">
<button @click="changeTheme('light')">切换到亮色</button>
</div>
</template>
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
const updateTheme = inject('updateTheme')
const changeTheme = (newTheme) => {
updateTheme(newTheme)
}
</script>
四、插槽(Slots):组件内容分发
- 默认插槽
子组件 Card.vue
<template>
<div class="card">
<div class="header">卡片标题</div>
<div class="content">
<slot>默认内容(没有传入内容时显示)</slot>
</div>
</div>
</template>
父组件使用
<template>
<Card>
<p>这是插入到卡片内容区域的自定义内容</p>
</Card>
</template>
- 具名插槽
子组件 Layout.vue
<template>
<div class="layout">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot> <!-- 默认插槽 -->
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
父组件使用
<template>
<Layout>
<template #header>
<h1>页眉区域</h1>
</template>
<p>主要内容(默认插槽)</p>
<template #footer>
<p>页脚区域</p>
</template>
</Layout>
</template>
- 作用域插槽:子组件向父组件传递数据
子组件 TodoList.vue
<template>
<ul>
<li v-for="item in todos" :key="item.id">
<slot :item="item" :index="index">
{{ item.text }} <!-- 默认显示 -->
</slot>
</li>
</ul>
</template>
<script setup>
const todos = ref([
{ id: 1, text: '学习 Vue', done: false },
{ id: 2, text: '写代码', done: true }
])
</script>
父组件使用
<template>
<TodoList>
<template #default="{ item, index }">
<span v-if="item.done">✅</span>
<span>{{ index }} - {{ item.text }}</span>
</template>
</TodoList>
</template>
五、兄弟组件通信
方式一:通过共同父组件中转(事件总线)
父组件作为中转
<template>
<SiblingA @send-to-b="handleFromA" />
<SiblingB :message="messageForB" />
</template>
<script setup>
import SiblingA from './SiblingA.vue'
import SiblingB from './SiblingB.vue'
import { ref } from 'vue'
const messageForB = ref('')
const handleFromA = (data) => {
messageForB.value = data
}
</script>
SiblingA.vue
<template>
<button @click="emitToB">向兄弟B发送消息</button>
</template>
<script setup>
const emit = defineEmits(['send-to-b'])
const emitToB = () => {
emit('send-to-b', 'Hello from A')
}
</script>
SiblingB.vue
<template>
<p>收到消息:{{ message }}</p>
</template>
<script setup>
defineProps(['message'])
</script>
方式二:使用 mitt 库(轻量级事件总线)
npm install mitt
eventBus.js
import mitt from 'mitt'
export const emitter = mitt()
组件 A(发送)
<template>
<button @click="send">发送</button>
</template>
<script setup>
import { emitter } from './eventBus'
const send = () => {
emitter.emit('custom-event', { data: '来自A' })
}
</script>
组件 B(接收)
<template>
<p>{{ receivedData }}</p>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { emitter } from './eventBus'
const receivedData = ref('')
const handler = (payload) => {
receivedData.value = payload.data
}
onMounted(() => {
emitter.on('custom-event', handler)
})
onUnmounted(() => {
emitter.off('custom-event', handler)
})
</script>
六、全局状态管理:Pinia / Vuex
- Pinia(Vue 3 官方推荐)
安装与配置
npm install pinia
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
createApp(App).use(pinia).mount('#app')
定义 Store
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
user: { name: '张三' }
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
},
async fetchUser() {
// 异步操作
}
}
})
// 也可以使用 setup 语法
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() { count.value++ }
return { count, doubleCount, increment }
})
在组件中使用
<template>
<div>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
<button @click="counter.increment()">增加</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
// 直接解构会丢失响应性,需用 storeToRefs
// const { count, doubleCount } = storeToRefs(counter)
</script>
- Vuex(Vue 2 及 Vue 3 兼容)
// store/index.js (Vuex 4 for Vue 3)
import { createStore } from 'vuex'
export default createStore({
state: { count: 0 },
mutations: { increment(state) { state.count++ } },
actions: { asyncIncrement({ commit }) { setTimeout(() => commit('increment'), 1000) } },
getters: { doubleCount: (state) => state.count * 2 }
})
// main.js
import store from './store'
app.use(store)
<template>
<p>{{ $store.state.count }}</p>
<button @click="$store.commit('increment')">增加</button>
</template>
七、Vue 2 与 Vue 3 组件通信差异速览
| 特性 | Vue 2 (Options API) | Vue 3 (Composition API) |
|---|---|---|
| Props 声明 | props: [‘title’] | defineProps([‘title’]) |
| 自定义事件 | this.$emit(‘event’) | const emit = defineEmits() emit(‘event’) |
| 事件总线 | new Vue() 作为总线 | 推荐 mitt 库 |
| provide / inject | provide: { foo: ‘bar’ } | provide(‘key’, value) inject(‘key’) |
| 插槽作用域 | v-slot:default=”slotProps” | 相同,支持 #default=”slotProps” |
| 全局状态管理 | Vuex 3 | Pinia / Vuex 4 |
组件设计
优秀的组件设计能让代码更易维护、复用和测试。以下从设计原则、Props/事件设计、插槽设计、逻辑复用到性能优化,给出系统性指导,并附代码示例。
一、组件设计原则
| 原则 | 说明 | 反例 |
|---|---|---|
| 单一职责 | 一个组件只做一件事,且做好。 | 一个组件既渲染用户列表又处理表单提交。 |
| 高内聚 | 相关的逻辑、模板、样式放在一起。 | 将组件的核心逻辑分散在父组件中。 |
| 低耦合 | 组件不依赖全局状态,通过 props/events 通信。 | 组件内直接 localStorage 或 window 全局变量。 |
| 可复用性 | 组件能在不同场景使用,通过参数化配置行为。 | 硬编码文本、样式、API 地址。 |
| 可测试性 | 组件易于单元测试,无隐式依赖。 | 在组件内直接调用异步请求,没有 mock 机制。 |
二、组件分类与设计模式
- 展示型组件 vs 容器型组件
| 类型 | 职责 | 特点 | 示例 |
|---|---|---|---|
| 展示型组件 | 接收 props 渲染 UI,通过 emit 触发事件 | 无状态(或只管理内部 UI 状态),无副作用,高度可复用 | Button、Input、Card、UserAvatar |
| 容器型组件 | 获取数据、处理业务逻辑、管理多个子组件 | 与 store / API 交互,通常包含展示型组件 | UserListContainer、DashboardPage |
<!-- 展示型组件:Button.vue -->
<template>
<button :class="`btn btn-${type}`" @click="$emit('click')">
<slot />
</button>
</template>
<script setup>
defineProps({ type: { type: String, default: 'primary' } })
defineEmits(['click'])
</script>
<!-- 容器型组件:UserProfileContainer.vue -->
<template>
<div v-if="user">
<UserAvatar :src="user.avatar" />
<UserName :name="user.name" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { fetchUser } from '@/api/user'
const user = ref(null)
onMounted(async () => { user.value = await fetchUser() })
</script>
- 通用组件 vs 业务组件
- 通用组件:与业务无关,可跨项目复用(如
DatePicker、Modal、Table)。 - 业务组件:与特定业务模型绑定(如
ProductCard、OrderList)。
设计通用组件时,应提供足够的 props 和 slots,避免硬编码业务逻辑。
三、Props 设计最佳实践
- 明确的类型、验证、默认值
<script setup>
const props = defineProps({
// 基础类型
title: {
type: String,
required: true
},
// 带默认值
size: {
type: String,
default: 'medium',
validator: (val) => ['small', 'medium', 'large'].includes(val)
},
// 对象/数组使用工厂函数
config: {
type: Object,
default: () => ({ theme: 'light' })
},
// 布尔值:默认 false 可不写 default
disabled: Boolean
})
</script>
- 单向数据流:永远不要在子组件内修改 props
<!-- ❌ 错误 -->
<script setup>
const props = defineProps(['modelValue'])
props.modelValue = 'new' // 报错
</script>
<!-- ✅ 正确:通过 emit 通知父组件修改 -->
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
- 使用计算属性派生 props 数据
<script setup>
const props = defineProps(['list', 'filterText'])
const filteredList = computed(() =>
props.list.filter(item => item.name.includes(props.filterText))
)
</script>
四、事件设计
- 声明所有事件(
defineEmits)
<script setup>
const emit = defineEmits(['update:modelValue', 'submit', 'close'])
</script>
- 事件命名:使用 kebab-case(模板中)或 camelCase(脚本中)
<!-- 父组件中监听 -->
<MyComponent @item-selected="onItemSelected" />
- 支持
v-model的两种方式
<!-- 单个 v-model -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<!-- 多个 v-model -->
<script setup>
defineProps(['firstName', 'lastName'])
defineEmits(['update:firstName', 'update:lastName'])
</script>
- 携带足够的事件数据
<button @click="emit('select', { id: item.id, value: item.value })">
选择
</button>
五、插槽设计:让组件更灵活
- 提供默认插槽 + 具名插槽
<!-- Card.vue -->
<template>
<div class="card">
<div class="header">
<slot name="header">默认标题</slot>
</div>
<div class="body">
<slot>默认内容</slot>
</div>
</div>
</template>
- 作用域插槽:向父组件传递数据
<!-- DataTable.vue -->
<template>
<table>
<tr v-for="row in data" :key="row.id">
<slot name="row" :row="row" :index="index">
<!-- 默认渲染所有列 -->
<td v-for="col in columns" :key="col">{{ row[col] }}</td>
</slot>
</tr>
</table>
</template>
<!-- 使用 -->
<DataTable :data="users">
<template #row="{ row }">
<td>{{ row.name }}</td>
<td>{{ row.email }}</td>
<td><button @click="edit(row)">编辑</button></td>
</template>
</DataTable>
- 动态插槽名
<template v-slot:[dynamicSlotName]>
动态插槽内容
</template>
六、逻辑复用:组合式函数(Composables)
将可复用的逻辑(状态、方法、生命周期)提取为组合式函数。
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const double = computed(() => count.value * 2)
function increment() { count.value++ }
function decrement() { count.value-- }
return { count, double, increment, decrement }
}
<!-- 组件中使用 -->
<script setup>
import { useCounter } from './composables/useCounter'
const { count, double, increment } = useCounter(10)
</script>
最佳实践:
- 命名以
use开头。 - 返回普通对象(非 ref 自动解包需注意)。
- 可接收参数,返回响应式状态和方法。
- 在组件卸载时自动清理副作用(
onUnmounted)。
七、高阶组件模式
- 动态组件
<component :is="...">
<template>
<component :is="currentComponent" :data="data" @change="handleChange" />
</template>
<script setup>
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'
const currentComponent = computed(() => someCondition.value ? ComponentA : ComponentB)
</script>
- 异步组件(代码分割)
import { defineAsyncComponent } from 'vue'
const AsyncModal = defineAsyncComponent(() => import('./Modal.vue'))
// 可配置加载/错误组件
const AsyncDashboard = defineAsyncComponent({
loader: () => import('./Dashboard.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200,
timeout: 3000
})
- 递归组件(树形结构)
<!-- TreeNode.vue -->
<template>
<div>
<span @click="toggle">{{ node.name }}</span>
<div v-if="open && node.children">
<TreeNode
v-for="child in node.children"
:key="child.id"
:node="child"
/>
</div>
</div>
</template>
<script setup>
defineProps(['node'])
import { ref } from 'vue'
const open = ref(false)
const toggle = () => open.value = !open.value
</script>
八、组件性能设计
| 实践 | 说明 | 示例 |
|---|---|---|
| v-once | 静态内容只渲染一次 | <div v-once>{{ staticContent }}</div> |
| v-memo | 缓存子树,依赖数组未变则跳过更新 | <div v-memo=”[[user.id](http://user.id)]”>{{ user.name }}</div> |
| 合理使用 key | 帮助 Vue 高效更新列表 | <li v-for=”item in list” :key=”[item.id](http://item.id)”> |
| shallowRef / shallowReactive | 减少深度响应式开销(大对象) | const state = shallowRef({ huge: … }) |
| 避免内联函数在模板中 | 防止子组件不必要的重渲染 | @click=”() => doSomething(item)” → 使用 @click=”doSomething(item)” 或 useCallback |
| 使用 KeepAlive | 缓存动态组件状态 | <KeepAlive><component :is=”tabComp” /></KeepAlive> |
九、组件设计检查清单
在设计一个组件前,可以问自己这些问题:
- 这个组件是否只有一个明确的职责?
- 是否可以通过 props 配置其行为和外观?
- 是否提供了足够的插槽以满足扩展需求?
- 组件内部状态是否最小化(避免不必要的响应式)?
- 是否避免了直接修改 props?
- 事件命名是否清晰且携带足够数据?
- 是否考虑了无数据/加载/错误等边界状态?
- 是否为通用组件提供了良好的默认值?
- 样式是否使用
scoped或 BEM 避免污染? - 是否编写了简单的单元测试?
十、完整示例:一个设计良好的 Pagination 组件
<!-- Pagination.vue -->
<template>
<div class="pagination">
<button :disabled="currentPage === 1" @click="changePage(currentPage - 1)">
上一页
</button>
<span>第 {{ currentPage }} / {{ totalPages }} 页</span>
<button :disabled="currentPage === totalPages" @click="changePage(currentPage + 1)">
下一页
</button>
<div class="slot-demo">
<slot name="extra" :page="currentPage"></slot>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
total: { type: Number, required: true },
pageSize: { type: Number, default: 10 },
modelValue: { type: Number, default: 1 }
})
const emit = defineEmits(['update:modelValue'])
const totalPages = computed(() => Math.ceil(props.total / props.pageSize))
const currentPage = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const changePage = (page) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
}
}
</script>
<style scoped>
.pagination button:disabled { opacity: 0.5; }
</style>
使用示例:
<template>
<Pagination v-model="page" :total="100" :page-size="10">
<template #extra="{ page }">
当前页码:{{ page }}
</template>
</Pagination>
</template>
前端工程化与生态
前端工程化是指将开发、构建、测试、部署等环节标准化、自动化,以提升效率和代码质量。Vue 生态提供了完整的工程化工具链,覆盖从项目创建到上线的全流程。
一、项目创建与脚手架
| 工具 | 适用版本 | 特点 |
|---|---|---|
| Vue CLI | Vue 2 / Vue 3 | 基于 Webpack,配置丰富,插件生态成熟(已进入维护模式) |
| create-vue | Vue 3 官方推荐 | 基于 Vite,极速启动,开箱即用 TypeScript、JSX 等 |
使用示例
# Vue CLI (Vue 2/3)
npm install -g @vue/cli
vue create my-project
# create-vue (Vue 3)
npm create vue@latest my-project
二、构建工具:从 Webpack 到 Vite
- Vite 配置(Vue 3 默认)
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
server: {
port: 3000,
proxy: {
'/api': 'http://localhost:8080'
}
},
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia']
}
}
}
}
})
- Vue CLI 配置(Vue 2 / 3)
// vue.config.js
module.exports = {
devServer: {
port: 8080,
proxy: 'http://localhost:3000'
},
configureWebpack: {
resolve: {
alias: { '@': path.resolve(__dirname, 'src') }
}
},
chainWebpack: config => {
config.plugin('define').tap(args => {
args[0]['process.env'].APP_VERSION = JSON.stringify('1.0.0')
return args
})
}
}
三、包管理工具
| 工具 | 优点 | 命令示例 |
|---|---|---|
| npm | 默认 | npm install |
| yarn | 速度快,离线缓存 | yarn add |
| pnpm | 磁盘高效,严格依赖 | pnpm add |
推荐使用 pnpm(monorepo 友好,节省磁盘空间)
# 安装 pnpm
npm install -g pnpm
# 创建 Vue 项目
pnpm create vue@latest my-app
cd my-app
pnpm install
pnpm run dev
四、代码规范与格式化
- ESLint + Prettier 配置
// .eslintrc.cjs
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'prettier'
],
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 'latest',
sourceType: 'module'
},
rules: {
'vue/multi-word-component-names': 'off',
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
}
// .prettierrc
{
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100
}
- EditorConfig
# .editorconfig
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
- Git Hooks(husky + lint-staged)
pnpm add -D husky lint-staged
npx husky install
npm pkg set scripts.prepare="husky install"
npx husky add .husky/pre-commit "npx lint-staged"
// package.json
{
"lint-staged": {
"*.{js,ts,vue}": ["eslint --fix", "prettier --write"],
"*.{css,scss,md}": ["prettier --write"]
}
}
五、单元测试与 E2E 测试
- Vitest + Vue Test Utils(Vue 3)
pnpm add -D vitest @vue/test-utils jsdom
// vitest.config.js
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
globals: true
}
})
// src/components/HelloWorld.spec.js
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import HelloWorld from './HelloWorld.vue'
describe('HelloWorld', () => {
it('renders props.msg', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello' } })
expect(wrapper.text()).toContain('Hello')
})
})
- Cypress E2E 测试
pnpm add -D cypress
pnpm exec cypress open
// cypress/e2e/home.cy.js
describe('Home Page', () => {
it('visits the app root url', () => {
cy.visit('/')
cy.contains('h1', 'Welcome')
})
})
六、环境变量与模式
Vite 支持 .env.[mode] 文件,使用 import.meta.env 访问。
# .env.development
VITE_API_BASE_URL=http://localhost:3000
# .env.production
VITE_API_BASE_URL=https://api.example.com
// 代码中使用
const apiUrl = import.meta.env.VITE_API_BASE_URL
Vue CLI 使用 process.env.VUE_APP_*。
七、性能分析与优化
Vite 分析插件
pnpm add -D vite-plugin-inspect
// vite.config.js
import Inspect from 'vite-plugin-inspect'
export default { plugins: [Inspect()] }
Webpack 分析(Vue CLI)
vue-cli-service build --report
八、状态管理与路由
- Pinia(Vue 3 官方)
pnpm add pinia
// stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({ name: '', isLoggedIn: false }),
actions: {
login(name) { this.name = name; this.isLoggedIn = true }
}
})
- Vue Router
pnpm add vue-router
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{ path: '/', component: () => import('@/views/Home.vue') }
]
export default createRouter({ history: createWebHistory(), routes })
九、CI/CD 配置示例(GitHub Actions)
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with: { node-version: 18 }
- run: pnpm install
- run: pnpm run test:unit
- run: pnpm run build
- uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist
原理探究
一、响应式原理(深度解析)
- Vue 2 实现:
Object.defineProperty
Vue 2 递归遍历 data 对象,为每个属性定义 getter/setter。
// 简化版 defineReactive
function defineReactive(obj, key, val) {
const dep = new Dep() // 依赖收集器
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
dep.depend() // 依赖收集
}
return val
},
set(newVal) {
if (val === newVal) return
val = newVal
dep.notify() // 派发更新
}
})
}
缺陷:
- 无法检测对象属性的添加/删除 → 需要
Vue.set/Vue.delete - 数组索引/长度变更不响应 → 拦截数组方法(
push,pop等) - 需要递归遍历所有属性,初始化性能差
- Vue 3 实现:
Proxy
Vue 3 使用 Proxy 代理整个对象,无需递归遍历,能拦截属性增删和数组变更。
// 简化版 reactive
function reactive(target) {
const handler = {
get(target, key, receiver) {
track(target, key) // 依赖收集
const result = Reflect.get(target, key, receiver)
// 惰性代理:只有访问嵌套对象时才转为响应式
if (typeof result === 'object' && result !== null) {
return reactive(result)
}
return result
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
trigger(target, key) // 派发更新
}
return result
},
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey && result) {
trigger(target, key)
}
return result
}
}
return new Proxy(target, handler)
}
- 依赖收集与派发更新(核心流程图)
┌─────────────┐
│ 组件渲染 │
│ (Watcher) │
└──────┬──────┘
│ 读取响应式数据
▼
┌─────────────┐ 依赖收集 ┌─────────┐
│ getter │ ◄────────────► │ Dep │
└──────┬──────┘ └────┬────┘
│ │ 拥有多个 Watcher
│ 数据变更 │
▼ ▼
┌─────────────┐ 派发更新 ┌─────────┐
│ setter │ ─────────────► │ Watcher │
└─────────────┘ └────┬────┘
│ 异步执行更新
▼
┌─────────────┐
│ 重新渲染 │
└─────────────┘
- 异步更新队列与
nextTick
Vue 将 Watcher 更新放入队列,同一事件循环内的多次数据变更只会触发一次 DOM 更新。
// 简化版 nextTick
const callbacks = []
let pending = false
function nextTick(cb) {
callbacks.push(cb)
if (!pending) {
pending = true
Promise.resolve().then(flushCallbacks)
}
}
function flushCallbacks() {
pending = false
const copies = callbacks.slice()
callbacks.length = 0
copies.forEach(cb => cb())
}
二、虚拟 DOM 与 Diff 算法
- 虚拟 DOM 本质
虚拟 DOM 是真实 DOM 的轻量级 JavaScript 对象描述。
// 虚拟节点结构
{
tag: 'div',
props: { id: 'app', class: 'container' },
children: [
{ tag: 'h1', props: {}, children: ['Hello'] }
]
}
- 渲染函数与 VNode 生成
模板会被编译为渲染函数,渲染函数返回 VNode 树。
// 模板:<div>{{ msg }}</div>
// 编译后渲染函数
function render() {
return h('div', null, this.msg)
}
- Diff 策略(Vue 2 与 Vue 3 共通)
- 同层比较:不跨层级移动节点,时间复杂度 O(n)
- 双端比较:新旧 VNode 队列头尾同时比较,尽量原地复用
- key 的作用:标识节点稳定性,帮助精确移动/复用
// 简化版 patch 核心
function patch(oldVNode, newVNode) {
if (sameVNode(oldVNode, newVNode)) {
patchVNode(oldVNode, newVNode)
} else {
replaceNode(oldVNode, newVNode)
}
}
function sameVNode(a, b) {
return a.key === b.key && a.tag === b.tag
}
- Vue 3 优化:静态提升、Block 树
- 静态提升:静态节点提升到渲染函数外部,避免重复创建
- PatchFlags:标记动态内容,diff 时跳过静态部分
- Block 树:收集动态节点,直接定位需要更新的位置
// 静态提升示例
const _hoisted_1 = { class: 'static' }
function render() {
return h('div', _hoisted_1, [h('span', null, this.dynamicText)])
}
三、模板编译原理
模板 → 抽象语法树(AST)→ 优化 AST(标记静态节点)→ 生成渲染函数。
- 解析阶段(Parse)
将模板字符串转为 AST(使用正则匹配标签、属性、文本等)。
// 简化示例
function parse(template) {
const ast = { type: 'root', children: [] }
let index = 0
while (index < template.length) {
if (template[index] === '<') {
// 解析开始标签
const tagMatch = /<(\w+)/.exec(template.slice(index))
// ... 构建 AST 节点
} else {
// 解析文本
}
}
return ast
}
- 优化阶段(Optimize)
标记静态根节点和静态属性,跳过后续更新。
function optimize(ast) {
markStatic(ast) // 标记静态节点
markStaticRoots(ast) // 标记静态根(只有静态子节点且层级 > 1)
}
- 生成阶段(Generate)
AST 转成渲染函数字符串。
function generate(ast) {
const code = `
function render() {
return ${genNode(ast)}
}
`
return new Function(code)
}
- 完整流程示例
模板: <div id="app">{{ msg }}</div>
↓ parse
AST: { type: 'element', tag: 'div', props: [{ name: 'id', value: 'app' }], children: [{ type: 'text', expression: 'msg' }] }
↓ optimize(标记静态节点)
↓ generate
输出渲染函数:
function render() {
with(this) {
return _c('div', { attrs: { id: 'app' } }, [_v(_s(msg))])
}
}
四、组件渲染流程
- 组件实例化
// Vue 3 简化
const instance = {
vnode: null, // 组件 VNode
component: null, // 组件定义
props: {},
slots: {},
ctx: null, // 渲染上下文
subTree: null, // 子树 VNode
update: null, // 更新函数
isMounted: false
}
- 首次渲染流程
createApp → mount → 创建根组件实例 → 调用 setup → 生成渲染函数 →
执行 render() 得到 VNode → patch(container, VNode) → 递归创建真实 DOM →
挂载到容器 → 触发 mounted 钩子
- 更新流程
响应式数据变化 → 触发 scheduler 队列 → 执行组件 update 函数 →
重新执行 render 得到 newVNode → 调用 patch(oldVNode, newVNode) →
执行 diff 算法 → 最小化 DOM 操作 → 触发 updated 钩子
- 卸载流程
组件销毁 → 调用 unmount 函数 → 递归卸载子组件 → 移除 DOM 节点 → 触发 unmounted 钩子
五、Vue 3 编译优化详解
- Block 与 PatchFlags
Vue 3 在编译时提取动态节点,形成 Block 树。每个 Block 包含其内部所有动态节点,patch 时直接遍历 Block 数组,无需深度遍历整个 VNode 树。
// 生成的渲染函数包含 PatchFlag
function render() {
return openBlock(), createBlock('div', null, [
createVNode('span', null, msg, 1 /* TEXT */) // PatchFlag = 1 表示动态文本
])
}
- 静态提升(HoistStatic)
静态节点提升到函数外部,多次渲染复用同一对象。
// 编译前
<div>
<p>静态文本</p>
<span>{{ dynamic }}</span>
</div>
// 编译后
const _hoisted_1 = h('p', null, '静态文本')
function render() {
return h('div', null, [_hoisted_1, h('span', null, this.dynamic)])
}
- 缓存事件处理函数
// 编译前
<button @click="handleClick">点击</button>
// 编译后(缓存事件)
const _cache = []
function render() {
return h('button', {
onClick: _cache[0] || (_cache[0] = (...args) => handleClick(...args))
}, '点击')
}
性能优化
一、运行时优化
- 合理使用
v-if和v-show
| 场景 | 推荐 | 原因 |
|---|---|---|
| 很少切换的条件 | v-if | 惰性渲染,初始渲染开销小 |
| 频繁切换 | v-show | 切换成本仅为 CSS,元素始终存在 |
vue
<template>
<!-- ✅ 频繁切换使用 v-show -->
<div v-show="isModalOpen">模态框内容</div>
<!-- ✅ 初始不显示且很少切换使用 v-if -->
<ExpensiveComponent v-if="loadExpensive" />
</template>
- 列表渲染优化
- 必须提供稳定的
key(不用index除非列表静态且无增删改) - 避免
v-for与v-if同时使用(Vue 3 中v-for优先级更高,每次循环都会执行v-if)
<template>
<!-- ❌ 错误:v-for 和 v-if 同级 -->
<div v-for="item in list" v-if="item.active" :key="item.id">{{ item.name }}</div>
<!-- ✅ 正确:使用计算属性过滤 -->
<div v-for="item in activeList" :key="item.id">{{ item.name }}</div>
</template>
<script setup>
import { computed } from 'vue'
const list = ref([...])
const activeList = computed(() => list.value.filter(item => item.active))
</script>
- 列表虚拟滚动(超长列表,如 1 万+条):使用
vue-virtual-scroller或@tanstack/virtual
npm install vue-virtual-scroller
<template>
<RecycleScroller :items="items" :item-size="50" key-field="id">
<template #default="{ item }">
<div>{{ item.name }}</div>
</template>
</RecycleScroller>
</template>
- 组件懒加载与异步组件
路由懒加载(Vue Router):
// router/index.js
const routes = [
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue') // 代码分割
}
]
异步组件(Vue 3):
import { defineAsyncComponent } from 'vue'
const AsyncModal = defineAsyncComponent({
loader: () => import('./Modal.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200,
timeout: 3000
})
- 使用
v-once和v-memo跳过更新
v-once:静态内容只渲染一次,不再更新。v-memo(Vue 3+):缓存子树,依赖数组未变则跳过更新。
<template>
<!-- 一次性渲染的静态内容 -->
<div v-once>{{ longStaticHtml }}</div>
<!-- 只有 user.id 变化时才重新渲染该区域 -->
<div v-memo="[user.id]">
<UserCard :user="user" />
<ExpensiveComponent :data="user.profile" />
</div>
</template>
- 避免内联函数在模板中
内联函数会在每次父组件渲染时创建新函数,导致子组件不必要的重渲染。
<template>
<!-- ❌ 每次渲染都创建新函数 -->
<Child @click="() => doSomething(item)" />
<!-- ✅ 使用稳定的事件处理器 -->
<Child @click="handleClick" />
</template>
<script setup>
const handleClick = (item) => { doSomething(item) }
</script>
- 使用
shallowRef/shallowReactive减少深度响应式开销
对于大型对象或不需要深度响应的数据,使用浅层响应式。
import { shallowRef, triggerRef } from 'vue'
const hugeData = shallowRef({ list: new Array(10000).fill(0) })
// 修改整个对象时触发更新
hugeData.value = { list: newData }
// 如果修改内部属性,需手动触发
hugeData.value.list[0] = 1
triggerRef(hugeData)
- 避免在模板中执行复杂计算
使用计算属性缓存结果,或使用方法时注意性能。
<template>
<!-- ❌ 每次渲染都重新计算 -->
<div>{{ expensiveCalculation(data) }}</div>
<!-- ✅ 计算属性缓存结果 -->
<div>{{ computedValue }}</div>
</template>
<script setup>
import { computed } from 'vue'
const computedValue = computed(() => expensiveCalculation(data.value))
</script>
二、构建优化
- 使用 Vite(Vue 3)替代 Webpack
Vite 基于原生 ES modules,开发服务器启动和热更新极快。生产构建使用 Rollup,支持 Tree Shaking。
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia'],
ui: ['element-plus']
}
}
}
}
})
- 代码分割(Code Splitting)
- 路由级别的懒加载(如上)
- 第三方库单独打包
// vite.config.js 中 manualChunks 配置
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('lodash')) return 'lodash'
if (id.includes('element-plus')) return 'element'
return 'vendor'
}
}
- 启用 Tree Shaking
确保使用 ES modules 语法,避免副作用。对于 lodash,使用 lodash-es 并按需导入。
// ❌ 导入整个库
import _ from 'lodash'
// ✅ 按需导入
import debounce from 'lodash-es/debounce'
- 压缩与优化
- Vite:默认使用 Terser,可配置
minify: 'esbuild'加快速度。 - 图片压缩:使用
vite-plugin-imagemin。
import imagemin from 'vite-plugin-imagemin'
export default {
plugins: [
imagemin({
optipng: { optimizationLevel: 7 },
pngquant: { quality: [0.8, 0.9] }
})
]
}
- 分析打包产物
# Vite
npm run build -- --report
# 使用 rollup-plugin-visualizer
npm add -D rollup-plugin-visualizer
// vite.config.js
import visualizer from 'rollup-plugin-visualizer'
export default {
plugins: [vue(), visualizer({ open: true })]
}
三、加载优化
- 预加载与预获取
<!-- 关键资源预加载 -->
<link rel="preload" href="/fonts/Inter.woff2" as="font" crossorigin />
<!-- 下一个页面的懒加载模块预获取 -->
<link rel="prefetch" href="/js/dashboard.js" />
- HTTP/2 多路复用 + 域名分散(对于静态资源)
使用 CDN 托管第三方库。
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
- 服务端渲染(SSR)或静态生成(SSG)
对于首屏速度要求极高的项目,使用 Nuxt(Vue 3)或 VuePress / VitePress(文档站点)。
npm create vue@latest my-app -- --ssr
- 图片懒加载
<template>
<img v-lazy="imageSrc" />
</template>
<script setup>
import { VueLazyLoad } from 'vue-lazyload'
// 配置后使用 v-lazy 指令
</script>
四、内存优化
- 及时清理定时器、事件监听、全局订阅
<script setup>
import { onMounted, onUnmounted } from 'vue'
let timer = null
const handleResize = () => {}
onMounted(() => {
timer = setInterval(() => {}, 1000)
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
if (timer) clearInterval(timer)
window.removeEventListener('resize', handleResize)
})
</script>
- 避免内存泄漏:谨慎使用
ref存储 DOM
// 组件卸载时,Vue 会自动断开 ref 引用,无需手动处理
// 但如果是全局变量持有 DOM,需要手动置 null
let globalRef = null
onMounted(() => { globalRef = someDomRef.value })
onUnmounted(() => { globalRef = null })
- 使用
keep-alive缓存组件时限制最大缓存数量
<KeepAlive :max="10">
<component :is="currentTab" />
</KeepAlive>
五、性能检测工具
| 工具 | 用途 |
|---|---|
| Vue Devtools | 查看组件渲染性能、事件、状态变化 |
| Lighthouse | 整体性能评分(CLS、LCP、FID) |
| WebPageTest | 详细加载时间分析 |
| Chrome Performance | 记录运行时火焰图,定位长任务 |
| vite-plugin-inspect | 分析 Vite 中间状态和模块依赖 |
| rollup-plugin-visualizer | 分析打包产物构成 |
使用 Vue Devtools 分析组件渲染
- 打开 Vue Devtools → Performance 标签
- 点击录制,操作页面
- 查看每个组件的渲染时间、更新次数
面试题
一、基础概念
- 说说 Vue 的响应式原理(Vue 2 和 Vue 3 的区别)
Vue 2:Object.defineProperty 递归遍历对象属性,重写 getter/setter。缺点:无法检测属性增删、数组索引/length 修改,需要 $set。
Vue 3:Proxy 代理整个对象,支持属性增删、数组变更,惰性代理(访问时才递归),性能更好。
口语化:**Vue 2 是给每个属性加“监控器”,但新增属性监控不到,数组改下标也不行,所以有** $set**。Vue 3 用“代理”把整个对象包起来,增删属性、改数组都能监控到,而且用到了才代理,性能更好。**
v-if和v-show的区别
v-if:真正的条件渲染,切换时销毁/重建组件,初始渲染条件为假时不渲染。
v-show:始终渲染,只是切换 CSS display 属性。
适用场景:v-if 适合很少切换的条件;v-show 适合频繁切换。
口语化:**v-if** 是真的“造”或“拆”**DOM,条件不成立时页面根本没有那个元素;v-show** 只是改 display 样式,元素还在但看不见。频繁切换用 v-show**,很少切换用** v-if**。**
- 为什么
v-for要加key?使用index作为 key 有什么问题?
key 帮助 Vue 识别节点,高效复用和重新排序,避免错误渲染。
使用 index 作为 key 的问题:当列表顺序变化(增删、排序)时,Vue 会复用错误的节点,导致状态错乱或性能下降。应用唯一且稳定的标识(如 id)。
口语化:**key** 是每个条目的“身份证”,让 Vue 准确知道谁是谁。用 index 当身份证,列表顺序一变(比如新增、删除、排序),身份证就对不上了,导致状态错乱(比如复选框勾到别人身上)。最好用后端返回的 id**。**
computed和watch的区别
computed:计算属性,依赖其它响应式数据,有缓存,只有依赖变化才重新计算。适合派生数据。
watch:侦听器,监听数据变化执行副作用(异步、DOM 操作),无缓存。适合处理数据变化时的复杂逻辑。
口语化:**computed** 有缓存,依赖没变就不重新算,适合“全名 = 姓 + 名”这种派生数据。**watch** 是盯着数据变化做事情,比如搜关键词变化就去调接口。一个用来“算新值”,一个用来“做事情”。
- Vue 生命周期钩子有哪些?请求放在哪个钩子?
Vue 2:beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, beforeDestroy, destroyed
Vue 3:同 Vue 2 但 beforeDestroy → beforeUnmount,destroyed → unmounted;组合式 API 中为 onMounted 等。
请求放在 created 或 mounted:created 时已有响应式数据,但无 DOM;mounted 可操作 DOM,适合依赖 DOM 的初始化。一般推荐 created。
口语化:**常用的是** created**(实例有了,能拿数据,但没** DOM**)、mounted(DOM 出来了)、updated(数据变了页面更新后)、unmounted(组件销毁前)。请求我一般放** created**,能早点发。如果非要等 DOM 出来再请求(比如画图),就放** mounted**。**
v-model的原理
语法糖,默认绑定 value 属性和 input 事件。
在组件上使用时:v-model 展开为 :modelValue + @update:modelValue。
可以自定义修饰符(.lazy, .number, .trim)和绑定多个 v-model(v-model:title)。
口语化:**v-model** 就是个“双向绑定的**快捷方式”。对输入框,它帮你做了两件事:把数据赋给** value**,再监听** input 事件把新值写回去。在组件上,它相当于传了 modelValue 属性,然后监听 update:modelValue 事件。
二、组件通信
- Vue 组件间通信方式有哪些?
| 通信场景 | 方式 |
|---|---|
| 父子 | props / $emit,v-model,ref 访问实例 |
| 兄弟 | 通过共同父组件中转,或事件总线(mitt) |
| 跨层级 | provide / inject |
| 全局 | Vuex(Vue 2)/ Pinia(Vue 3) |
| 任意组件 | 事件总线(mitt)或全局状态管理 |
口语化:**父传子用** props**,子传父用** $emit**。兄弟之间可以找父亲当“中间人”,或者用全局事件总线(mitt)。跨好多层用** provide/inject**,但数据不好追踪。大型项目用 Vuex 或 Pinia 集中管理。**
$emit和$on的事件总线在 Vue 3 中还能用吗?
Vue 3 移除了 $on、$off、$once 实例方法,推荐使用第三方库 mitt 或 tiny-emitter。
// mitt 示例
import mitt from 'mitt'
const emitter = mitt()
emitter.on('foo', e => console.log(e))
emitter.emit('foo', { a: 'b' })
口语化:**不行了,Vue 3 去掉了** $on**。现在要用事件总线,得装一个** mitt**,用法差不多:emitter.on('event', handler),emitter.emit('event', data)。**
provide/inject有什么缺点?适合什么场景?
缺点:数据追踪困难(不直观),无响应性(除非提供响应式数据),不适合大型应用。
场景:插件、主题切换、语言包等跨多层但非全局的状态。
口语化:**缺点是谁传的、传了啥不好找,而且默认不会自动更新,得传响应式数据。适合那种层级很深但又不全局的状态,比如主题色、语言包,改得不频繁。**
三、响应式与原理
- Vue 2 中如何检测对象新增属性?数组变更如何检测?
对象新增属性:Vue.set(obj, key, value) 或 this.$set。
数组:Vue 2 拦截了 push/pop/shift/unshift/splice/sort/reverse 七个方法,调用这些方法会触发更新。直接通过索引修改数组项 arr[0] = val 不响应,需用 $set 或 splice。
口语化:**给对象加新属性要用** this.$set**,不然不响应。数组用** push**、pop** 这些方法是响应式的,但直接 arr[0]=xxx 不行,也要用 $set 或 splice 去改。
- Vue 3 的
ref和reactive的区别?
| 特性 | ref | reactive |
|---|---|---|
| 数据类型 | 任意(原始类型/对象) | 仅对象(object, array, Map, Set) |
| 访问方式 | .value | 直接访问属性 |
| 解构 | 不影响响应性 | 解构会丢失响应性,需 toRefs |
| 传递 | 可以传递整个 ref 对象 | 传递后依然响应 |
口语化:**ref** 啥都能包,但要 .value 取值。**reactive** 只能包对象,取值不用 .value**,但解构出来就变普通数据了。简单变量用** ref**,对象用** reactive 方便。如果你要整个对象重新赋值,用 ref 更省事。
toRefs和toRef的作用?
toRefs:将 reactive 对象的所有属性转为 ref,并保持响应性连接,用于解构。
toRef:为 reactive 对象的某个属性创建一个 ref,保持连接。
const state = reactive({ a: 1, b: 2 })
const { a, b } = toRefs(state)
a.value++ // 同时更新 state.a
口语化:**你用** reactive 搞了个对象,如果直接解构成 {a,b}**,那** a 和 b 就不响应了。用 toRefs 包一下再解构,每个属性还是 ref**,改了还能同步回原对象。toRef** 就是只转一个属性。
nextTick的原理是什么?
Vue 异步更新队列:数据变化后,Watcher 不会立即更新 DOM,而是推入队列,在下一个事件循环(microtask)统一执行。nextTick 将回调推迟到 DOM 更新之后。实现上优先使用 Promise.then,降级 MutationObserver、setImmediate、setTimeout。
口语化:**Vue 改数据后不会马上更新** DOM**,而是攒一批一起改。nextTick** 就是等 DOM 更新完后执行你的**回调。它内部优先用** Promise**(微任务),不行再用** setTimeout**。**
四、组件与设计
- 说说你对 SPA 的理解,Vue 如何实现路由切换?
- SPA:单页面应用,通过 JS 动态重写当前页面,而非从服务器加载新页面。优点是用户体验流畅,缺点是首屏加载慢、SEO 不友好。
- Vue Router:基于
hash或history模式,监听 URL 变化,匹配路由组件,通过<router-view>动态渲染。
口语化:**SPA** 就是整个网站只有一个 HTML 文件,切页面不刷新,靠 JS 换内容。Vue Router 监听 URL 变化,匹配到对应的组件,然后放到 <router-view> 里显示出来。
- 动态组件和异步组件
- 动态组件:
<component :is="componentName">,根据数据切换组件。 - 异步组件:使用
defineAsyncComponent,代码分割,按需加载。
口语化:**动态组件就是** <component is="组件名">**,可以随时换组件。异步组件就是按需加载,比如点某个按钮才去加载那个组件的代码,能减小初始包体积。**
- 递归组件如何实现?
组件名称可在模板中直接使用自身。注意设置 name 选项(Vue 2)或使用文件名(Vue 3 <script setup> 中通过文件名递归)。典型场景:树形菜单、评论列表。
<!-- TreeNode.vue -->
<template>
<div>
<span>{{ node.name }}</span>
<TreeNode v-for="child in node.children" :key="child.id" :node="child" />
</div>
</template>
口语化:**组件自己调用自己,比如树形菜单。Vue 2 里要写** name**,Vue 3 用文件名就能递归。最关键的是要有结束条件(比如** v-if="hasChildren"**),不然会死循环。**
五、性能优化
- Vue 项目中你做过哪些性能优化?
参考答案(从运行、构建、加载、内存四方面):
- 运行时:
v-if/v-show合理使用;列表加唯一key;长列表虚拟滚动;组件懒加载;使用v-once/v-memo。 - 构建:路由懒加载;代码分割;Tree Shaking;图片压缩;Gzip/Brotli。
- 加载:CDN 加速;预加载关键资源;SSR/SSG 提升首屏速度。
- 内存:清除定时器/事件监听;避免全局变量持有 DOM。
口语化:**我常用的:列表用** v-show 而不是 v-if 来频繁切换;给 v-for 加唯一 id**;大列表用虚拟滚动;路由用懒加载;图片压缩成** WebP**;开启** Gzip**;组件销毁时清掉定时器和监听事件。**
- 如何减少重绘和重排?
- 避免频繁读取和修改布局属性(如
offsetTop),使用变量缓存。 - 批量修改样式,使用
class或cssText。 - 脱离文档流(
position: absolute)进行大量 DOM 操作。 - 使用
requestAnimationFrame优化动画。
口语化:**不要频繁读** offsetHeight 这种属性,要读就先缓存;改样式尽量一次改完,比如加个 class;大批量操作可以把元素先隐藏(**display: none),改完再显示;动画用** transform 代替 top/left**,因为它不触发重排。**
keep-alive的作用及生命周期?
- 作用:缓存动态组件或路由组件,避免重复渲染,保留组件状态。
- 新增生命周期:
activated(激活时调用)、deactivated(失活时调用)。 - 属性:
include、exclude、max(最大缓存实例数)。
口语化:**keep-alive** 能把切走的组件缓存起来,回来时不用重新渲染。比如 Tab 页签,切回来数据还在。被缓存的组件多了两个钩子:**activated(进来时)和** deactivated**(离开时)。**
六、Vue 2 vs Vue 3
- Vue 3 相比 Vue 2 有哪些重大改进?
| 方面 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式 | defineProperty | Proxy |
| 组合式 API | 无(Options API) | setup 函数 + 组合式 API |
| 模板编译 | 全量 diff | 静态提升、Block、PatchFlags |
| TypeScript | 支持较弱 | 完美支持 |
| 体积 | 较大(全局 API 不可摇树) | 更小(可摇树) |
| 新内置组件 | – | Teleport、Suspense |
| 自定义渲染器 | 复杂 | 更容易(createRenderer) |
口语化:**响应式强了,$set** 不需要了;写法上多了组合式 API**,逻辑可以抽出来复用;编译更聪明,把静态部分提出来,更新更快;对** TS 支持完美;打包更小;还有 Teleport**(把组件传送到别的位置)和** Suspense**(处理异步组件)。**
- Composition API 相比 Options API 有什么优势?
- 更好的逻辑复用:
composables替代 mixins,避免命名冲突和来源不明。 - 更灵活的代码组织:按功能分组,而非按选项类型(data、methods)。
- 更好的类型推导:对 TypeScript 更友好。
- 更小的打包体积:函数式编写更容易 Tree Shaking。
口语化:**以前用 Options** API**,一个功能要分散在 data、methods、watch 里,代码不在一块。组合式 API 可以把相关代码聚在一起,还能抽成自定义 hook(useXxx),复用起来很方便,也更容易写** TS**。**
setup函数中如何获取this?
setup 中无法访问组件实例的 this,因为它在组件创建之前执行。需要获取组件实例可使用 getCurrentInstance(),但不推荐(除非高级用法)。
口语化:**setup** 里没有 this**,因为组件还没建好。实在要拿实例,可以用** getCurrentInstance()**,但这属于偏门用法,一般不需要。**
- Vue 3 的
Teleport和Suspense是什么?
Teleport:将组件模板内容渲染到 DOM 的任意位置(如 body 下的 modal)。
Suspense:用于协调异步组件,在等待多个异步依赖时显示 fallback 内容。
口语化:**Teleport** 让你把组件内容“传送”到别的地方,比如弹窗要挂到 body 下,避免被父组件样式截断。**Suspense** 就是在等异步组件加载时先显示一个“加载中”的占位,多个异步组件都好了才显示真正内容。
七、场景题
- 如何设计一个全局弹窗组件?
思路:
- 创建一个
Modal.vue组件,通过v-model控制显示隐藏。 - 使用
provide/inject或全局状态管理控制弹窗栈。 - 命令式调用:扩展组件实例,通过
createApp动态挂载。
// 命令式调用
function showModal(options) {
const modal = createApp(Modal, {
visible: true,
...options,
onClose: () => modal.unmount()
})
const div = document.createElement('div')
document.body.appendChild(div)
modal.mount(div)
}
口语化:**我会写一个** Modal 组件,然后用一个全局方法 $modal**,调用时动态创建一个** DOM 节点,用 createApp 挂载这个弹窗,传参数进去,点关闭时再 unmount 销毁。这样就能像 this.$modal.confirm() 一样调用了。
- 列表数据量大(10万条),如何优化渲染?
- 虚拟滚动(只渲染可视区域),使用
vue-virtual-scroller。 - 使用
v-once如果数据不会变。 - 使用
Object.freeze冻结静态数据,避免响应式开销。 - 分页加载。
口语化:**10万条直接渲染** DOM 会卡死。我用虚拟滚动,只画当前屏幕能看到的几十条。也可以做成分页,一页只显示 20 条。如果这些数据只是展示不改,用 Object.freeze 冻结掉,Vue 就不给它们加响应式了,性能会好很多。
- Vue 项目首屏加载慢如何优化?
- 路由懒加载
- 开启 Gzip / Brotli 压缩
- CDN 加速静态资源
- 预渲染或 SSR(Nuxt)
- 图片懒加载 + WebP 格式
- 减少入口文件体积(分析 bundle,移除无用依赖)
口语化:**我会做这些事:路由懒加载(按需加载组件);服务器开** Gzip**;把 Vue、Vue Router 这些库用** CDN 引入;图片懒加载;还有就是把大的依赖包找出来,看能不能替换成更小的库。如果项目要求特别高,就用 Nuxt 做服务端渲染。
- 如何实现一个
v-model自定义组件?
<!-- CustomInput.vue -->
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
口语化:**子组件里声明** modelValue 这个 prop**,然后监听** input 事件,用 $emit('update:modelValue', 新值) 发回去。父组件就能用 v-model 绑定了。如果要多个 v-model**,就改成** v-model:title 这种形式。
- 如何处理 Vue 项目中的内存泄漏?
- 在
beforeDestroy/onUnmounted中清理定时器、取消事件监听、断开 WebSocket、取消订阅。 - 避免使用全局变量引用组件 DOM 或实例。
- 谨慎使用
$refs缓存组件,组件销毁时置空。 - 使用
weakmap存储临时数据。
口语化:**在组件销毁的钩子(onUnmounted)里,把** setInterval**、addEventListener** 都清掉。全局变量不要乱挂 DOM 引用,用完手动 null**。如果用了事件总线,记得** off**。WebSocket 也要关掉。这样能防止页面长时间不刷新导致内存越来越高。**
八、综合题
- 说说你对虚拟 DOM 的理解。
虚拟 DOM 是用 JS 对象描述真实 DOM 树。当数据变化时,Vue 生成新虚拟 DOM,通过 diff 算法比较新旧树,找出最小差异并更新真实 DOM。优点:跨平台(可渲染到 native)、批量更新、减少 DOM 操作。缺点:首次渲染较慢(需要创建虚拟 DOM)。
口语化:**虚拟** DOM 就是 JS 对象表示的 DOM 结构。数据变了,新生成一个 JS 对象,跟旧的对比,找出哪里变了,再只改那部分真实 DOM。这样就不用直接操作 DOM,性能也好,而且可以支持小程序、原生渲染。
- 简单实现一个响应式函数(Vue 3 风格)
function reactive(target) {
return new Proxy(target, {
get(obj, key, receiver) {
track(obj, key)
const result = Reflect.get(obj, key, receiver)
return typeof result === 'object' ? reactive(result) : result
},
set(obj, key, value, receiver) {
const old = obj[key]
const result = Reflect.set(obj, key, value, receiver)
if (old !== value) trigger(obj, key)
return result
},
deleteProperty(obj, key) {
const had = Object.hasOwn(obj, key)
const result = Reflect.deleteProperty(obj, key)
if (had && result) trigger(obj, key)
return result
}
})
}
口语化:**用** Proxy 包一层,在 get 里记录“谁在用这个属性”(依赖收集),在 set 和 deleteProperty 里通知“这个属性变了,快更新”(派发更新)。如果取到的值还是对象,就**递归包装。这样就实现了响应式。