Vue

环境搭建

统一安装 Node.js 作为基础,然后根据不同项目需求选择脚手架工具。

  1. 下载安装:前往 Node.js 官网,下载 LTS 版本(稳定版),按指引完成安装。
  2. 验证安装:
node -v
npm -v
  1. 配置国内镜像(可选)
npm config set registry https://registry.npmmirror.com
  1. 安装脚手架工具

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
  1. 启动项目:
cd my-vue-project
npm run serve

核心概念

Vue 实例与生命周期

Vue 实例是 Vue 应用的核心。每个 Vue 应用都是通过创建一个 Vue 实例(Vue 2)或应用实例(Vue 3)开始的。这个实例连接了数据模型和 DOM,并提供了生命周期的钩子函数,让你能在实例创建、挂载、更新、销毁等不同阶段注入自己的代码。

  1. 创建 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 返回一个应用实例。组件实例仍然存在,但生命周期概念同样适用。

  1. 实例属性和方法(以 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))
  1. 生命周期图解

一个 Vue 实例从创建到销毁会经历一系列初始化、编译、挂载、更新、卸载等阶段。下图是官方生命周期的流程图(Vue 2/3 基本一致,部分钩子名称有变化):

beforeCreate

created

beforeMount

mounted
↓ (数据变化时)
beforeUpdate

updated
↓ (调用 vm.$destroy 或组件卸载时)
beforeDestroy (Vue2) / beforeUnmount (Vue3)

destroyed (Vue2) / unmounted (Vue3)
  1. 生命周期钩子详解

钩子是 Vue 在特定阶段自动调用的函数。你可以在其中添加自己的业务逻辑。

钩子 (Vue 2)钩子 (Vue 3)触发时机典型用途
beforeCreatebeforeCreate实例初始化后,数据观测和事件配置之前插件初始化、不依赖数据的操作(很少用)
createdcreated实例创建完成,已设置响应式数据、计算属性、方法,但 DOM 尚未挂载调用 API 获取初始数据、设置定时器
beforeMountbeforeMount模板编译/渲染函数首次执行之前,$el 还不存在一般很少用,可在渲染前最后一次修改数据
mountedmounted实例被挂载到 DOM 后($el 已创建),组件的 DOM 已完成渲染操作 DOM、集成第三方库、发起网络请求(但更推荐 created)
beforeUpdatebeforeUpdate响应式数据变化,DOM 重新渲染之前在更新前访问现有 DOM(如移除事件监听)
updatedupdated数据变化导致 DOM 重新渲染完成后操作更新后的 DOM(注意避免在此修改数据导致死循环)
beforeDestroybeforeUnmount实例销毁之前,实例仍然完全可用清理定时器、取消订阅、解绑事件
destroyedunmounted实例销毁后,所有指令解绑,事件监听移除,子实例也销毁做最后的清理工作

Vue 3 组合式 API 中的钩子:使用 onMountedonUpdatedonUnmounted 等函数,需要在 setup() 中调用,并且要提前从 vue 导入。

<script setup>
import { onMounted, onUnmounted } from 'vue'

onMounted(() => {
 console.log('组件已挂载')
})

onUnmounted(() => {
 console.log('组件即将卸载')
})
</script>
  1. 关键点与常见误区
  • created vs mounted
    • created 时还没有 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(() => { ... })
  1. 完整示例(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。

  1. 插值

最基础的文本绑定,使用双花括号 {{ }}

<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>

注意:{{ }} 内部只能使用单行表达式,不能写语句(如 iffor)。它会自动将结果转为字符串。

  1. 原始 HTML

{{ }} 会转义 HTML 字符,防止 XSS 攻击。如果需要输出真正的 HTML,使用 v-html 指令:

<template>
 <div>{{ rawHtml }}</div>     <!-- 输出:&lt;span&gt;... -->
 <div v-html="rawHtml"></div> <!-- 输出:渲染后的红色文字 -->
</template>

<script setup>
const rawHtml = '<span style="color: red">红色文字</span>'
</script>

警告:动态渲染任意 HTML 很危险,容易导致 XSS 攻击。只对可信内容使用 v-html,永远不要用在用户提供的内容上。

  1. 属性绑定

{{ }}不能用在 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>

布尔型**属性**

当值为 falsenull/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>
  1. 使用 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) }}
  1. 指令 (Directives)

指令是带有 v- 前缀的特殊属性,其值预期是单个 JavaScript 表达式(v-forv-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 }}
  1. 修饰符 (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">
  1. 动态参数 (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>

约束:

  • 动态参数的值必须是字符串或 nullnull 表示移除绑定),其他值会触发警告。
  • 表达式内部不能有空格和引号,例如 :[key + 'foo'] 无效。可以用计算属性代替。
  • 在 DOM 模板中(直接写在 HTML 文件里)需要避免使用大写字符,因为浏览器会将属性名转为小写。
  1. 过滤器 (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())
  1. 模板中的空格与换行

Vue 模板会遵循 HTML 的空白处理规则。多个连续空格通常会被合并为一个。如果需要保留空格,可以使用 或 CSS white-space: pre-wrap

  1. 访问全局变量

在模板表达式中,Vue 只允许访问有限的白名单(如 MathDate)。不能直接访问用户自定义的全局变量(如 windowdocument)。如果需要,可以在组件中声明为 methodscomputed 的返回结果。

<template>
 <p>{{ Math.floor(price) }}</p>   <!-- OK,Math 在白名单中 -->
 <p>{{ window.innerWidth }}</p>   <!-- 报错 -->
</template>

响应式原理

Vue 的响应式系统是其最核心的特性之一。它使得当数据发生变化时,所有依赖该数据的地方(视图、计算属性、侦听器等)都能自动更新。这套机制在 Vue 2 和 Vue 3 中实现方式不同,但核心思想一致:数据劫持 + 依赖收集 + 派发更新。

  1. 核心思想:发布-订阅模式

Vue 的响应式系统本质上是一个发布-订阅模式的实现:

  • 数据对象:发布者(Publisher)
  • 视图/计算属性/侦听器:订阅者(Subscriber,在 Vue 中称为 Watcher
  • 依赖管理器:调度中心(Dep,Dependency)

当数据被读取时,会进行依赖收集——把当前的 Watcher 添加到数据的依赖列表中。当数据被修改时,会触发派发更新——通知所有依赖该数据的 Watcher 执行更新。

  1. Vue 2 的实现:Object.defineProperty

Vue 2 通过 Object.defineProperty 递归地将数据对象的属性转换为 getter/setter,从而拦截属性的读取和设置。

简化流程:

  1. 初始化:遍历 data 中的所有属性,对每个属性调用 defineReactive
  2. getter:当读取属性时,如果存在 Dep.target(当前正在计算的 Watcher),则进行依赖收集(将 Watcher 添加到属性的 Dep 中)。
  3. 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.$setVue.delete
  • 不能直接通过索引修改数组项:例如 arr[0] = newVal 不是响应式的,需使用 arr.spliceVue.set
  • 数组的变更检测:Vue 2 通过拦截数组的 push/pop/shift/unshift/splice/sort/reverse 方法实现响应式。
  • 性能问题:需要递归遍历所有属性,对大型嵌套对象初始化成本高。
  1. Vue 3 的实现:Proxy

Vue 3 使用 ES6 的 Proxy 替代 Object.definePropertyProxy 可以代理整个对象,而不仅仅是属性,因此能解决 Vue 2 的大部分限制。

优势:

  • 直接检测属性的添加和删除:无需 $set / $delete
  • 直接检测数组索引和 length 修改:arr[0] = 'new' 是响应式的。
  • 懒代理:Vue 3 只在访问嵌套对象时才将其转为响应式,性能更好。
  • 支持更多数据类型:如 MapSetWeakMapWeakSet

核心实现(简化):

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 只能代理对象,无法代理原始类型(如 numberstring),因此 Vue 3 提供了 ref 包装器。
  • reactive 返回的代理对象与原始对象不相等,解构时会失去响应性(需使用 toRefs)。
  1. 依赖收集与 Watcher

无论是 Vue 2 还是 Vue 3,依赖收集的核心机制类似:

  • Dep:每个响应式属性都拥有一个 Dep 实例,内部维护一组 Watcher。
  • Watcher:负责执行某个表达式(如渲染函数、计算属性、侦听器回调),并在数据变化时重新执行。
  • 依赖收集过程:
    • 在 Watcher 执行其 getter 前,将自身赋值给全局唯一的 Dep.target
    • 执行 getter 时会访问响应式数据,触发 getter → 调用 dep.depend() → 将当前 Dep.target 添加到 Dep 的 subs 数组中。
    • 执行完毕后,Dep.target 恢复为上一个 Watcher。
  • 派发更新过程:
    • 数据变化触发 setter → 调用 dep.notify()
    • 遍历 Dep 中的所有 Watcher,依次调用 watcher.update()
    • 在 Vue 中,Watcher 更新通常是异步的(通过 nextTick 批量处理),以避免频繁 DOM 操作。
  1. Vue 3 的响应式 API:refreactive

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++                           // 修改
  1. 响应式原理的流程图
                    ┌─────────────┐
                   │    Data     │
                   └──────┬──────┘
                          │
           ┌──────────────┴──────────────┐
           │    defineProperty / Proxy    │
           └──────────────┬──────────────┘
                          │
             ┌────────────┴────────────┐
             │                         │
          getter                     setter
             │                         │
      依赖收集 (Dep)              派发更新 (Dep)
             │                         │
      添加 Watcher               notify Watchers
             │                         │
       Watcher 执行                Watcher 重新执行
             │                         │
      更新视图 / 计算属性          更新视图 / 计算属性

响应式 API

Vue 3 的 Composition API 提供了一套独立的响应式 API,让你可以在任何地方(组件外、组合式函数中)创建和管理响应式状态。这些 API 是 @vue/reactivity 包的核心,Vue 组件内部也完全基于它们构建。

  1. 核心 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
  1. 辅助 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)检查是否只读代理

重要:toRefstoRef 解决解构丢失响应性

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
  1. 只读 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.
  1. 浅层响应式 API(性能优化)

默认情况下,refreactive 会进行深层响应式转换(递归遍历所有嵌套属性)。如果对象非常庞大且深层数据不需要响应式,可以使用浅层版本提升性能。

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 的副作用(通常用于手动更新)。

  1. 高级 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  // 修改不会触发响应式更新
  1. 侦听器 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"

停止侦听:watchwatchEffect 都返回一个停止函数,调用即可停止。

  1. 与 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()
自定义 refcustomRef()
标记原始对象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>

二、循环渲染

  1. 基本 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>
  1. 遍历对象属性
<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>
  1. 遍历数字范围
<template>
 <div>
   <span v-for="n in 10" :key="n">{{ n }} </span>
   <!-- 输出:1 2 3 4 5 6 7 8 9 10 -->
 </div>
</template>
  1. 使用 <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-forv-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>

事件与表单:

一、事件处理

  1. 基本用法:v-on 指令 / @ 简写
<template>
 <div>
   <!-- 完整写法 -->
   <button v-on:click="handleClick">点击我</button>

   <!-- 简写 @ -->
   <button @click="handleClick">点击我(简写)</button>
 </div>
</template>

<script setup>
const handleClick = () => {
alert('按钮被点击了!')
}
</script>
  1. 内联事件处理器与事件参数
<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>
  1. 事件修饰符

修饰符可以串联,且顺序会影响行为。

<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>
  1. 按键修饰符
<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>
  1. 鼠标修饰符
<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>
  1. 自定义事件(组件通信)

子组件 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

  1. 基本用法:文本、多行文本、复选框、单选按钮、选择框
<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>
  1. 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>
  1. 在自定义组件上使用 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 的差异。

一、组件定义与注册

  1. 单文件组件(.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>
  1. 全局注册与局部注册
// 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):组件内容分发

  1. 默认插槽

子组件 Card.vue

<template>
 <div class="card">
   <div class="header">卡片标题</div>
   <div class="content">
     <slot>默认内容(没有传入内容时显示)</slot>
   </div>
 </div>
</template>

父组件使用

<template>
 <Card>
   <p>这是插入到卡片内容区域的自定义内容</p>
 </Card>
</template>
  1. 具名插槽

子组件 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>
  1. 作用域插槽:子组件向父组件传递数据

子组件 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

  1. 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>
  1. 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 / injectprovide: { foo: ‘bar’ }provide(‘key’, value) inject(‘key’)
插槽作用域v-slot:default=”slotProps”相同,支持 #default=”slotProps”
全局状态管理Vuex 3Pinia / Vuex 4

组件设计

优秀的组件设计能让代码更易维护、复用和测试。以下从设计原则、Props/事件设计、插槽设计、逻辑复用到性能优化,给出系统性指导,并附代码示例。

一、组件设计原则

原则说明反例
单一职责一个组件只做一件事,且做好。一个组件既渲染用户列表又处理表单提交。
高内聚相关的逻辑、模板、样式放在一起。将组件的核心逻辑分散在父组件中。
低耦合组件不依赖全局状态,通过 props/events 通信。组件内直接 localStorage 或 window 全局变量。
可复用性组件能在不同场景使用,通过参数化配置行为。硬编码文本、样式、API 地址。
可测试性组件易于单元测试,无隐式依赖。在组件内直接调用异步请求,没有 mock 机制。

二、组件分类与设计模式

  1. 展示型组件 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>
  1. 通用组件 vs 业务组件
  • 通用组件:与业务无关,可跨项目复用(如 DatePickerModalTable)。
  • 业务组件:与特定业务模型绑定(如 ProductCardOrderList)。

设计通用组件时,应提供足够的 props 和 slots,避免硬编码业务逻辑。

三、Props 设计最佳实践

  1. 明确的类型、验证、默认值
<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>
  1. 单向数据流:永远不要在子组件内修改 props
<!-- ❌ 错误 -->
<script setup>
const props = defineProps(['modelValue'])
props.modelValue = 'new' // 报错
</script>

<!-- ✅ 正确:通过 emit 通知父组件修改 -->
<template>
 <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
  1. 使用计算属性派生 props 数据
<script setup>
const props = defineProps(['list', 'filterText'])
const filteredList = computed(() =>
 props.list.filter(item => item.name.includes(props.filterText))
)
</script>

四、事件设计

  1. 声明所有事件(defineEmits
<script setup>
const emit = defineEmits(['update:modelValue', 'submit', 'close'])
</script>
  1. 事件命名:使用 kebab-case(模板中)或 camelCase(脚本中)
<!-- 父组件中监听 -->
<MyComponent @item-selected="onItemSelected" />
  1. 支持 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>
  1. 携带足够的事件数据
<button @click="emit('select', { id: item.id, value: item.value })">
选择
</button>

五、插槽设计:让组件更灵活

  1. 提供默认插槽 + 具名插槽
<!-- Card.vue -->
<template>
 <div class="card">
   <div class="header">
     <slot name="header">默认标题</slot>
   </div>
   <div class="body">
     <slot>默认内容</slot>
   </div>
 </div>
</template>
  1. 作用域插槽:向父组件传递数据
<!-- 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>
  1. 动态插槽名
<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)。

七、高阶组件模式

  1. 动态组件 <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>
  1. 异步组件(代码分割)
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
})
  1. 递归组件(树形结构)
<!-- 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 CLIVue 2 / Vue 3基于 Webpack,配置丰富,插件生态成熟(已进入维护模式)
create-vueVue 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

  1. 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']
      }
    }
  }
}
})
  1. 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

四、代码规范与格式化

  1. 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
}
  1. 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
  1. 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 测试

  1. 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')
})
})
  1. 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

八、状态管理与路由

  1. 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 }
}
})
  1. 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

原理探究

一、响应式原理(深度解析)

  1. 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 等)
  • 需要递归遍历所有属性,初始化性能差
  1. 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)
}
  1. 依赖收集与派发更新(核心流程图)
┌─────────────┐
│ 组件渲染   │
│ (Watcher) │
└──────┬──────┘
      │ 读取响应式数据
      ▼
┌─────────────┐   依赖收集   ┌─────────┐
│   getter   │ ◄────────────► │   Dep   │
└──────┬──────┘               └────┬────┘
      │                           │ 拥有多个 Watcher
      │ 数据变更                   │
      ▼                           ▼
┌─────────────┐   派发更新   ┌─────────┐
│   setter   │ ─────────────► │ Watcher │
└─────────────┘               └────┬────┘
                                    │ 异步执行更新
                                    ▼
                            ┌─────────────┐
                            │ 重新渲染   │
                            └─────────────┘
  1. 异步更新队列与 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 算法

  1. 虚拟 DOM 本质

虚拟 DOM 是真实 DOM 的轻量级 JavaScript 对象描述。

// 虚拟节点结构
{
 tag: 'div',
 props: { id: 'app', class: 'container' },
 children: [
  { tag: 'h1', props: {}, children: ['Hello'] }
]
}
  1. 渲染函数与 VNode 生成

模板会被编译为渲染函数,渲染函数返回 VNode 树。

// 模板:<div>{{ msg }}</div>
// 编译后渲染函数
function render() {
 return h('div', null, this.msg)
}
  1. 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
}
  1. 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(标记静态节点)→ 生成渲染函数。

  1. 解析阶段(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
}
  1. 优化阶段(Optimize)

标记静态根节点和静态属性,跳过后续更新。

function optimize(ast) {
 markStatic(ast)     // 标记静态节点
 markStaticRoots(ast) // 标记静态根(只有静态子节点且层级 > 1)
}
  1. 生成阶段(Generate)

AST 转成渲染函数字符串。

function generate(ast) {
 const code = `
   function render() {
     return ${genNode(ast)}
   }
 `
 return new Function(code)
}
  1. 完整流程示例
模板: <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))])
}
}

四、组件渲染流程

  1. 组件实例化
// Vue 3 简化
const instance = {
 vnode: null,        // 组件 VNode
 component: null,    // 组件定义
 props: {},
 slots: {},
 ctx: null,          // 渲染上下文
 subTree: null,      // 子树 VNode
 update: null,       // 更新函数
 isMounted: false
}
  1. 首次渲染流程
createApp → mount → 创建根组件实例 → 调用 setup → 生成渲染函数 → 
执行 render() 得到 VNode → patch(container, VNode) → 递归创建真实 DOM →
挂载到容器 → 触发 mounted 钩子
  1. 更新流程
响应式数据变化 → 触发 scheduler 队列 → 执行组件 update 函数 → 
重新执行 render 得到 newVNode → 调用 patch(oldVNode, newVNode) →
执行 diff 算法 → 最小化 DOM 操作 → 触发 updated 钩子
  1. 卸载流程
组件销毁 → 调用 unmount 函数 → 递归卸载子组件 → 移除 DOM 节点 → 触发 unmounted 钩子

五、Vue 3 编译优化详解

  1. 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 表示动态文本
])
}
  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)])
}
  1. 缓存事件处理函数
// 编译前
<button @click="handleClick">点击</button>

// 编译后(缓存事件)
const _cache = []
function render() {
 return h('button', {
   onClick: _cache[0] || (_cache[0] = (...args) => handleClick(...args))
}, '点击')
}

性能优化

一、运行时优化

  1. 合理使用 v-ifv-show
场景推荐原因
很少切换的条件v-if惰性渲染,初始渲染开销小
频繁切换v-show切换成本仅为 CSS,元素始终存在

vue

<template>
 <!-- ✅ 频繁切换使用 v-show -->
 <div v-show="isModalOpen">模态框内容</div>

 <!-- ✅ 初始不显示且很少切换使用 v-if -->
 <ExpensiveComponent v-if="loadExpensive" />
</template>
  1. 列表渲染优化
  • 必须提供稳定的 key(不用 index 除非列表静态且无增删改)
  • 避免 v-forv-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>
  1. 组件懒加载与异步组件

路由懒加载(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
})
  1. 使用 v-oncev-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>
  1. 避免内联函数在模板中

内联函数会在每次父组件渲染时创建新函数,导致子组件不必要的重渲染。

<template>
 <!-- ❌ 每次渲染都创建新函数 -->
 <Child @click="() => doSomething(item)" />

 <!-- ✅ 使用稳定的事件处理器 -->
 <Child @click="handleClick" />
</template>

<script setup>
const handleClick = (item) => { doSomething(item) }
</script>
  1. 使用 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)
  1. 避免在模板中执行复杂计算

使用计算属性缓存结果,或使用方法时注意性能。

<template>
 <!-- ❌ 每次渲染都重新计算 -->
 <div>{{ expensiveCalculation(data) }}</div>

 <!-- ✅ 计算属性缓存结果 -->
 <div>{{ computedValue }}</div>
</template>

<script setup>
import { computed } from 'vue'
const computedValue = computed(() => expensiveCalculation(data.value))
</script>

二、构建优化

  1. 使用 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']
      }
    }
  }
}
})
  1. 代码分割(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'
}
}
  1. 启用 Tree Shaking

确保使用 ES modules 语法,避免副作用。对于 lodash,使用 lodash-es 并按需导入。

// ❌ 导入整个库
import _ from 'lodash'

// ✅ 按需导入
import debounce from 'lodash-es/debounce'
  1. 压缩与优化
  • 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] }
  })
]
}
  1. 分析打包产物
# 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 })]
}

三、加载优化

  1. 预加载与预获取
<!-- 关键资源预加载 -->
<link rel="preload" href="/fonts/Inter.woff2" as="font" crossorigin />

<!-- 下一个页面的懒加载模块预获取 -->
<link rel="prefetch" href="/js/dashboard.js" />
  1. HTTP/2 多路复用 + 域名分散(对于静态资源)

使用 CDN 托管第三方库。

<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
  1. 服务端渲染(SSR)或静态生成(SSG)

对于首屏速度要求极高的项目,使用 Nuxt(Vue 3)或 VuePress / VitePress(文档站点)。

npm create vue@latest my-app -- --ssr
  1. 图片懒加载
<template>
 <img v-lazy="imageSrc" />
</template>

<script setup>
import { VueLazyLoad } from 'vue-lazyload'
// 配置后使用 v-lazy 指令
</script>

四、内存优化

  1. 及时清理定时器、事件监听、全局订阅
<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>
  1. 避免内存泄漏:谨慎使用 ref 存储 DOM
// 组件卸载时,Vue 会自动断开 ref 引用,无需手动处理
// 但如果是全局变量持有 DOM,需要手动置 null
let globalRef = null
onMounted(() => { globalRef = someDomRef.value })
onUnmounted(() => { globalRef = null })
  1. 使用 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 分析组件渲染

  1. 打开 Vue Devtools → Performance 标签
  2. 点击录制,操作页面
  3. 查看每个组件的渲染时间、更新次数

面试题

一、基础概念

  1. 说说 Vue 的响应式原理(Vue 2 和 Vue 3 的区别)

Vue 2:Object.defineProperty 递归遍历对象属性,重写 getter/setter。缺点:无法检测属性增删、数组索引/length 修改,需要 $set

Vue 3:Proxy 代理整个对象,支持属性增删、数组变更,惰性代理(访问时才递归),性能更好。

口语化:**Vue 2 是给每个属性加“监控器”,但新增属性监控不到,数组改下标也不行,所以有** $set**。Vue 3 用“代理”把整个对象包起来,增删属性、改数组都能监控到,而且用到了才代理,性能更好。**

  1. v-ifv-show 的区别

v-if:真正的条件渲染,切换时销毁/重建组件,初始渲染条件为假时不渲染。

v-show:始终渲染,只是切换 CSS display 属性。

适用场景:v-if 适合很少切换的条件;v-show 适合频繁切换。

口语化:**v-if** 是真的“造”或“拆”**DOM,条件不成立时页面根本没有那个元素;v-show** 只是改 display 样式,元素还在但看不见。频繁切换用 v-show**,很少切换用** v-if**。**

  1. 为什么 v-for 要加 key?使用 index 作为 key 有什么问题?

key 帮助 Vue 识别节点,高效复用和重新排序,避免错误渲染。

使用 index 作为 key 的问题:当列表顺序变化(增删、排序)时,Vue 会复用错误的节点,导致状态错乱或性能下降。应用唯一且稳定的标识(如 id)。

口语化:**key** 是每个条目的“身份证”,让 Vue 准确知道谁是谁。用 index 当身份证,列表顺序一变(比如新增、删除、排序),身份证就对不上了,导致状态错乱(比如复选框勾到别人身上)。最好用后端返回的 id**。**

  1. computedwatch 的区别

computed:计算属性,依赖其它响应式数据,有缓存,只有依赖变化才重新计算。适合派生数据。

watch:侦听器,监听数据变化执行副作用(异步、DOM 操作),无缓存。适合处理数据变化时的复杂逻辑。

口语化:**computed** 有缓存,依赖没变就不重新算,适合“全名 = 姓 + 名”这种派生数据。**watch** 是盯着数据变化做事情,比如搜关键词变化就去调接口。一个用来“算新值”,一个用来“做事情”。

  1. Vue 生命周期钩子有哪些?请求放在哪个钩子?

Vue 2:beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, beforeDestroy, destroyed

Vue 3:同 Vue 2 但 beforeDestroybeforeUnmountdestroyedunmounted;组合式 API 中为 onMounted 等。

请求放在 createdmountedcreated 时已有响应式数据,但无 DOM;mounted 可操作 DOM,适合依赖 DOM 的初始化。一般推荐 created

口语化:**常用的是** created**(实例有了,能拿数据,但没** DOM**)、mounted(DOM 出来了)、updated(数据变了页面更新后)、unmounted(组件销毁前)。请求我一般放** created**,能早点发。如果非要等 DOM 出来再请求(比如画图),就放** mounted**。**

  1. v-model 的原理

语法糖,默认绑定 value 属性和 input 事件。

在组件上使用时:v-model 展开为 :modelValue + @update:modelValue

可以自定义修饰符(.lazy, .number, .trim)和绑定多个 v-modelv-model:title)。

口语化:**v-model** 就是个“双向绑定的**快捷方式”。对输入框,它帮你做了两件事:把数据赋给** value**,再监听** input 事件把新值写回去。在组件上,它相当于传了 modelValue 属性,然后监听 update:modelValue 事件。

二、组件通信

  1. Vue 组件间通信方式有哪些?
通信场景方式
父子props / $emit,v-model,ref 访问实例
兄弟通过共同父组件中转,或事件总线(mitt)
跨层级provide / inject
全局Vuex(Vue 2)/ Pinia(Vue 3)
任意组件事件总线(mitt)或全局状态管理

口语化:**父传子用** props**,子传父用** $emit**。兄弟之间可以找父亲当“中间人”,或者用全局事件总线(mitt)。跨好多层用** provide/inject**,但数据不好追踪。大型项目用 Vuex 或 Pinia 集中管理。**

  1. $emit$on 的事件总线在 Vue 3 中还能用吗?

Vue 3 移除了 $on$off$once 实例方法,推荐使用第三方库 mitttiny-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)。**

  1. provide / inject 有什么缺点?适合什么场景?

缺点:数据追踪困难(不直观),无响应性(除非提供响应式数据),不适合大型应用。

场景:插件、主题切换、语言包等跨多层但非全局的状态。

口语化:**缺点是谁传的、传了啥不好找,而且默认不会自动更新,得传响应式数据。适合那种层级很深但又不全局的状态,比如主题色、语言包,改得不频繁。**

三、响应式与原理

  1. Vue 2 中如何检测对象新增属性?数组变更如何检测?

对象新增属性:Vue.set(obj, key, value)this.$set

数组:Vue 2 拦截了 push/pop/shift/unshift/splice/sort/reverse 七个方法,调用这些方法会触发更新。直接通过索引修改数组项 arr[0] = val 不响应,需用 $setsplice

口语化:**给对象加新属性要用** this.$set**,不然不响应。数组用** push**、pop** 这些方法是响应式的,但直接 arr[0]=xxx 不行,也要用 $set splice 去改。

  1. Vue 3 的 refreactive 的区别?
特性refreactive
数据类型任意(原始类型/对象)仅对象(object, array, Map, Set)
访问方式.value直接访问属性
解构不影响响应性解构会丢失响应性,需 toRefs
传递可以传递整个 ref 对象传递后依然响应

口语化:**ref** 啥都能包,但要 .value 取值。**reactive** 只能包对象,取值不用 .value**,但解构出来就变普通数据了。简单变量用** ref**,对象用** reactive 方便。如果你要整个对象重新赋值,用 ref 更省事。

  1. toRefstoRef 的作用?

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** 就是只转一个属性。

  1. nextTick 的原理是什么?

Vue 异步更新队列:数据变化后,Watcher 不会立即更新 DOM,而是推入队列,在下一个事件循环(microtask)统一执行。nextTick 将回调推迟到 DOM 更新之后。实现上优先使用 Promise.then,降级 MutationObserversetImmediatesetTimeout

口语化:**Vue 改数据后不会马上更新** DOM**,而是攒一批一起改。nextTick** 就是等 DOM 更新完后执行你的**回调。它内部优先用** Promise**(微任务),不行再用** setTimeout**。**

四、组件与设计

  1. 说说你对 SPA 的理解,Vue 如何实现路由切换?
  • SPA:单页面应用,通过 JS 动态重写当前页面,而非从服务器加载新页面。优点是用户体验流畅,缺点是首屏加载慢、SEO 不友好。
  • Vue Router:基于 hashhistory 模式,监听 URL 变化,匹配路由组件,通过 <router-view> 动态渲染。

口语化:**SPA** 就是整个网站只有一个 HTML 文件,切页面不刷新,靠 JS 换内容。Vue Router 监听 URL 变化,匹配到对应的组件,然后放到 <router-view> 里显示出来。

  1. 动态组件和异步组件
  • 动态组件:<component :is="componentName">,根据数据切换组件。
  • 异步组件:使用 defineAsyncComponent,代码分割,按需加载。

口语化:**动态组件就是** <component is="组件名">**,可以随时换组件。异步组件就是按需加载,比如点某个按钮才去加载那个组件的代码,能减小初始包体积。**

  1. 递归组件如何实现?

组件名称可在模板中直接使用自身。注意设置 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"**),不然会死循环。**

五、性能优化

  1. 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**;组件销毁时清掉定时器和监听事件。**

  1. 如何减少重绘和重排?
  • 避免频繁读取和修改布局属性(如 offsetTop),使用变量缓存。
  • 批量修改样式,使用 classcssText
  • 脱离文档流(position: absolute)进行大量 DOM 操作。
  • 使用 requestAnimationFrame 优化动画。

口语化:**不要频繁读** offsetHeight 这种属性,要读就先缓存;改样式尽量一次改完,比如加个 class;大批量操作可以把元素先隐藏(**display: none),改完再显示;动画用** transform 代替 top/left**,因为它不触发重排。**

  1. keep-alive 的作用及生命周期?
  • 作用:缓存动态组件或路由组件,避免重复渲染,保留组件状态。
  • 新增生命周期:activated(激活时调用)、deactivated(失活时调用)。
  • 属性:includeexcludemax(最大缓存实例数)。

口语化:**keep-alive** 能把切走的组件缓存起来,回来时不用重新渲染。比如 Tab 页签,切回来数据还在。被缓存的组件多了两个钩子:**activated(进来时)和** deactivated**(离开时)。**

六、Vue 2 vs Vue 3

  1. Vue 3 相比 Vue 2 有哪些重大改进?
方面Vue 2Vue 3
响应式definePropertyProxy
组合式 API无(Options API)setup 函数 + 组合式 API
模板编译全量 diff静态提升、Block、PatchFlags
TypeScript支持较弱完美支持
体积较大(全局 API 不可摇树)更小(可摇树)
新内置组件Teleport、Suspense
自定义渲染器复杂更容易(createRenderer)

口语化:**响应式强了,$set** 不需要了;写法上多了组合式 API**,逻辑可以抽出来复用;编译更聪明,把静态部分提出来,更新更快;对** TS 支持完美;打包更小;还有 Teleport**(把组件传送到别的位置)和** Suspense**(处理异步组件)。**

  1. Composition API 相比 Options API 有什么优势?
  • 更好的逻辑复用:composables 替代 mixins,避免命名冲突和来源不明。
  • 更灵活的代码组织:按功能分组,而非按选项类型(data、methods)。
  • 更好的类型推导:对 TypeScript 更友好。
  • 更小的打包体积:函数式编写更容易 Tree Shaking。

口语化:**以前用 Options** API**,一个功能要分散在 data、methods、watch 里,代码不在一块。组合式 API 可以把相关代码聚在一起,还能抽成自定义 hook(useXxx),复用起来很方便,也更容易写** TS**。**

  1. setup 函数中如何获取 this

setup 中无法访问组件实例的 this,因为它在组件创建之前执行。需要获取组件实例可使用 getCurrentInstance(),但不推荐(除非高级用法)。

口语化:**setup** 里没有 this**,因为组件还没建好。实在要拿实例,可以用** getCurrentInstance()**,但这属于偏门用法,一般不需要。**

  1. Vue 3 的 TeleportSuspense 是什么?

Teleport:将组件模板内容渲染到 DOM 的任意位置(如 body 下的 modal)。

Suspense:用于协调异步组件,在等待多个异步依赖时显示 fallback 内容。

口语化:**Teleport** 让你把组件内容“传送”到别的地方,比如弹窗要挂到 body 下,避免被父组件样式截断。**Suspense** 就是在等异步组件加载时先显示一个“加载中”的占位,多个异步组件都好了才显示真正内容。

七、场景题

  1. 如何设计一个全局弹窗组件?

思路:

  1. 创建一个 Modal.vue 组件,通过 v-model 控制显示隐藏。
  2. 使用 provide/inject 或全局状态管理控制弹窗栈。
  3. 命令式调用:扩展组件实例,通过 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() 一样调用了。

  1. 列表数据量大(10万条),如何优化渲染?
  • 虚拟滚动(只渲染可视区域),使用 vue-virtual-scroller
  • 使用 v-once 如果数据不会变。
  • 使用 Object.freeze 冻结静态数据,避免响应式开销。
  • 分页加载。

口语化:**10万条直接渲染** DOM 会卡死。我用虚拟滚动,只画当前屏幕能看到的几十条。也可以做成分页,一页只显示 20 条。如果这些数据只是展示不改,用 Object.freeze 冻结掉,Vue 就不给它们加响应式了,性能会好很多。

  1. Vue 项目首屏加载慢如何优化?
  • 路由懒加载
  • 开启 Gzip / Brotli 压缩
  • CDN 加速静态资源
  • 预渲染或 SSR(Nuxt)
  • 图片懒加载 + WebP 格式
  • 减少入口文件体积(分析 bundle,移除无用依赖)

口语化:**我会做这些事:路由懒加载(按需加载组件);服务器开** Gzip**;把 Vue、Vue Router 这些库用** CDN 引入;图片懒加载;还有就是把大的依赖包找出来,看能不能替换成更小的库。如果项目要求特别高,就用 Nuxt 做服务端渲染。

  1. 如何实现一个 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 这种形式。

  1. 如何处理 Vue 项目中的内存泄漏?
  • beforeDestroy / onUnmounted 中清理定时器、取消事件监听、断开 WebSocket、取消订阅。
  • 避免使用全局变量引用组件 DOM 或实例。
  • 谨慎使用 $refs 缓存组件,组件销毁时置空。
  • 使用 weakmap 存储临时数据。

口语化:**在组件销毁的钩子(onUnmounted)里,把** setInterval**、addEventListener** 都清掉。全局变量不要乱挂 DOM 引用,用完手动 null**。如果用了事件总线,记得** off**。WebSocket 也要关掉。这样能防止页面长时间不刷新导致内存越来越高。**

八、综合题

  1. 说说你对虚拟 DOM 的理解。

虚拟 DOM 是用 JS 对象描述真实 DOM 树。当数据变化时,Vue 生成新虚拟 DOM,通过 diff 算法比较新旧树,找出最小差异并更新真实 DOM。优点:跨平台(可渲染到 native)、批量更新、减少 DOM 操作。缺点:首次渲染较慢(需要创建虚拟 DOM)。

口语化:**虚拟** DOM 就是 JS 对象表示的 DOM 结构。数据变了,新生成一个 JS 对象,跟旧的对比,找出哪里变了,再只改那部分真实 DOM。这样就不用直接操作 DOM,性能也好,而且可以支持小程序、原生渲染。

  1. 简单实现一个响应式函数(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 里通知“这个属性变了,快更新”(派发更新)。如果取到的值还是对象,就**递归包装。这样就实现了响应式。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇