Vue
一、Vue 基础概念
1. 什么是 Vue.js?
核心特性
- 数据驱动视图:采用 MVVM 模式,数据变化自动更新视图
- 组件化开发:将页面拆分为独立可复用的组件
- 双向数据绑定:通过 v-model 实现表单数据的双向同步
- 虚拟 DOM:提升 DOM 操作性能
- 响应式系统:数据变化自动追踪并更新依赖
- 单文件组件 (SFC):.vue 文件将模板、逻辑、样式封装在一起
Vue 与传统开发的区别
| 对比维度 | jQuery/传统开发 | Vue.js |
|---|---|---|
| 操作对象 | DOM | 数据 |
| 更新方式 | 手动操作 DOM | 自动更新 |
| 开发模式 | 命令式 | 声明式 |
| 数据绑定 | 手动同步 | 双向绑定 |
| 代码组织 | 全局/散乱 | 组件化 |
渐进式框架的含义
Vue 可以自底向上逐层应用:
- 核心库只关注视图层,易于上手
- 可以只在一个页面的一小部分使用 Vue
- 可以配合官方生态(Router、Vuex、CLI)构建复杂单页应用
- 可以与第三方库共存
2. MVVM 模式及其在 Vue 中的体现
MVVM 定义
MVVM(Model-View-ViewModel)是一种软件架构模式,是 MVC 的改进版:
- Model(模型):数据模型,负责业务逻辑和数据存储
- View(视图):UI 层,负责展示数据
- ViewModel(视图模型):连接 Model 和 View 的桥梁,负责数据双向绑定
MVVM 在 Vue 中的体现
scss
┌─────────────────────────────────────────────┐
│ MVVM 架构 │
├─────────────────────────────────────────────┤
│ │
│ View ViewModel │
│ (DOM 模板) ←→ (Vue 实例) │
│ / \ │
│ / \ │
│ 数据绑定 事件监听 │
│ / \ │
│ / \ │
│ Model (data/响应式数据) │
│ │
└─────────────────────────────────────────────┘
具体体现
| MVVM 元素 | Vue 对应实现 |
|---|---|
| Model | Vue 实例中的 data、computed 等响应式数据 |
| View | 模板语法 {{}}、指令渲染的 DOM |
| ViewModel | Vue 实例本身(new Vue({...})) |
数据流向
javascript
// 数据变化 → 视图更新(自动)
this.message = 'Hello Vue' // DOM 自动更新显示
// 视图变化 → 数据更新(双向绑定)
<input v-model="message"> // 输入框变化自动更新 message
3. Vue 的优点与缺点
优点
- 简单易学,API 设计友好
- 轻量级,核心库体积小(gzip 后约 20KB)
- 双向数据绑定,减少手动 DOM 操作
- 组件化开发,提高代码复用性
- 官方生态完善(Vue Router、Vuex、Vue CLI)
- 虚拟 DOM 提升性能
- 支持过渡动画、自定义指令等高级特性
- 文档友好,中文文档完善
缺点
- 生态圈相对 React 较小
- 不适合超大型项目(组件层级过深时性能下降)
- 首屏加载较慢(SPA 通病)
- SEO 不友好(需配合 SSR 解决)
- IE 兼容性有限(Vue 3 不支持 IE)
4. Vue 与 React 的对比
| 对比维度 | Vue | React |
|---|---|---|
| 核心思想 | MVVM 模式 | 函数式 UI |
| 模板语法 | 基于 HTML 的模板语法 | JSX(JavaScript XML) |
| 数据绑定 | 双向绑定 | 单向数据流 |
| 状态管理 | Vuex | Redux/MobX |
| 响应式 | Object.defineProperty/Proxy | setState/hooks |
| 学习曲线 | 平缓,易于上手 | 较陡,需理解 JSX、Hooks 等 |
| 性能优化 | 自动依赖追踪 | 手动 shouldComponentUpdate/memo |
| 类型支持 | TypeScript 支持(Vue 3 更好) | TypeScript 原生支持好 |
选择策略
- 选 Vue:中小型项目、快速开发、团队对 Vue 熟悉、需要双向绑定
- 选 React:大型复杂项目、需要灵活架构、生态要求高、团队有 React 经验
5. Vue 与 jQuery 的区别
| 对比维度 | jQuery | Vue |
|---|---|---|
| 操作对象 | DOM | 数据 |
| 编程模式 | 命令式(手动操作) | 声明式(描述状态) |
| 数据更新 | 手动 $('#el').text() |
自动响应式更新 |
| 代码组织 | 散乱 | 组件化 |
| 适用场景 | 简单交互、老项目维护 | 现代 SPA 应用 |
javascript
// jQuery 方式:手动操作 DOM
$('#btn').click(function() {
var count = parseInt($('#count').text());
$('#count').text(count + 1);
});
// Vue 方式:数据驱动
<button @click="count++">{{ count }}</button>
二、Vue 实例与生命周期
1. Vue 生命周期概述
定义
每个 Vue 实例在被创建时都要经过一系列的初始化过程,这个过程就是生命周期。生命周期钩子是实例在特定阶段自动执行的函数。
生命周期的 8 个阶段
markdown
beforeCreate → created → beforeMount → mounted
↓ ↓ ↓ ↓
实例初始化 数据注入完成 模板编译完成 DOM 渲染完成
↓ ↓ ↓ ↓
beforeUpdate → updated → beforeDestroy → destroyed
↓ ↓ ↓ ↓
数据变化前 数据变化后 实例销毁前 实例销毁后
各阶段详细说明
| 生命周期钩子 | 触发时机 | 可访问内容 | 常见用途 |
|---|---|---|---|
beforeCreate |
实例初始化后,数据观测和事件配置之前 | 无(data/props/methods 都未初始化) | 极少使用 |
created |
实例创建完成,数据观测、属性计算完成 | data、methods、computed、watch | 发起 API 请求、初始化数据 |
beforeMount |
模板编译完成,挂载开始前 | 可以访问模板,但 DOM 还未生成 | 挂载前最后修改数据的机会 |
mounted |
实例挂载到 DOM 后 | 可以访问真实 DOM | DOM 操作、第三方库初始化 |
beforeUpdate |
数据变化导致虚拟 DOM 重新渲染前 | 可以访问更新前后的数据 | 更新前清理或记录状态 |
updated |
虚拟 DOM 重新渲染和打补丁后 | 可以访问更新后的 DOM | DOM 更新后的操作 |
beforeDestroy |
实例销毁前 | 实例仍然完全可用 | 清理定时器、解绑事件 |
destroyed |
实例销毁后 | 所有东西都被解绑 | 最终清理工作 |
代码示例
javascript
export default {
data() {
return {
message: 'Hello Vue'
}
},
beforeCreate() {
console.log('beforeCreate: 实例初始化')
// console.log(this.message) // undefined
},
created() {
console.log('created: 实例创建完成')
console.log(this.message) // 'Hello Vue'
// 发起 API 请求的最佳时机
this.fetchData()
},
beforeMount() {
console.log('beforeMount: 挂载前')
// DOM 还未生成
},
mounted() {
console.log('mounted: 已挂载')
// 可以操作 DOM
console.log(this.$el)
},
beforeUpdate() {
console.log('beforeUpdate: 更新前')
},
updated() {
console.log('updated: 已更新')
},
beforeDestroy() {
console.log('beforeDestroy: 销毁前')
// 清理定时器、事件监听器
clearInterval(this.timer)
},
destroyed() {
console.log('destroyed: 已销毁')
},
methods: {
fetchData() {
// 模拟 API 请求
setTimeout(() => {
this.message = 'Data loaded'
}, 1000)
}
}
}
常见误区
- ❌ 在
beforeCreate中访问 data → data 还未初始化 - ❌ 在
created中操作 DOM → DOM 还未生成 - ❌ 在
mounted中发起大量请求 → 可能导致页面卡顿,应考虑在created中发起 - ❌ 忘记在
beforeDestroy中清理资源 → 导致内存泄漏
2. created 与 mounted 的区别
| 对比维度 | created | mounted |
|---|---|---|
| 触发时机 | 实例创建完成后 | DOM 挂载完成后 |
| 能否访问 data | ✅ 可以 | ✅ 可以 |
| 能否访问 DOM | ❌ 不能(DOM 未生成) | ✅ 可以 |
| 能否访问 $el | ❌ 不能 | ✅ 可以 |
| 服务端渲染 SSR | ✅ 会执行 | ❌ 不执行 |
| 适用场景 | 数据初始化、API 请求 | DOM 操作、图表初始化、第三方库 |
选择策略
- 如果只是获取数据,优先使用 created(SSR 友好,更早执行)
- 如果需要操作 DOM,必须使用 mounted
javascript
export default {
created() {
// ✅ 适合:数据请求
this.getUserInfo()
this.getArticleList()
},
mounted() {
// ✅ 适合:DOM 操作
this.initChart()
this.focusInput()
// ✅ 适合:第三方库初始化
this.editor = new Editor(this.$refs.editorEl)
}
}
3. 父子组件生命周期执行顺序
加载渲染过程
父 beforeCreate
父 created
父 beforeMount
↓ 递归渲染子组件
子 beforeCreate
子 created
子 beforeMount
子 mounted
↓ 子组件挂载完成
父 mounted
更新过程
父 beforeUpdate
↓ 递归更新子组件
子 beforeUpdate
子 updated
↓ 子组件更新完成
父 updated
销毁过程
父 beforeDestroy
↓ 递归销毁子组件
子 beforeDestroy
子 destroyed
↓ 子组件销毁完成
父 destroyed
代码验证
javascript
// 父组件 Parent.vue
export default {
name: 'Parent',
beforeCreate() { console.log('Parent beforeCreate') },
created() { console.log('Parent created') },
beforeMount() { console.log('Parent beforeMount') },
mounted() { console.log('Parent mounted') },
}
// 子组件 Child.vue
export default {
name: 'Child',
beforeCreate() { console.log('Child beforeCreate') },
created() { console.log('Child created') },
beforeMount() { console.log('Child beforeMount') },
mounted() { console.log('Child mounted') },
}
// 输出顺序:
// Parent beforeCreate
// Parent created
// Parent beforeMount
// Child beforeCreate
// Child created
// Child beforeMount
// Child mounted
// Parent mounted
4. keep-alive 带来的生命周期变化
当组件被 <keep-alive> 包裹时:
activated:组件被激活时调用deactivated:组件被停用时调用beforeDestroy和destroyed不再触发(组件被缓存而非销毁)
html
<template>
<keep-alive>
<router-view />
</keep-alive>
</template>
<script>
export default {
activated() {
console.log('组件被激活')
// 恢复状态、重新获取数据
},
deactivated() {
console.log('组件被停用')
// 保存状态、暂停定时器等
}
}
</script>
5. Vue 实例挂载过程
挂载流程
markdown
1. new Vue(options)
↓
2. 初始化生命周期、事件、数据
↓
3. 调用 $mount()
↓
4. 编译模板(compile)
↓
5. 生成渲染函数(render function)
↓
6. 创建虚拟 DOM
↓
7. 将虚拟 DOM 转换为真实 DOM
↓
8. 替换 el 指定的元素
源码流程简述
javascript
// 简化版挂载流程
Vue.prototype.$mount = function(el) {
el = el && document.querySelector(el)
// 如果没有 render 函数,则编译模板
if (!this.$options.render) {
const template = this.$options.template
if (template) {
// 模板编译为渲染函数
this.$options.render = compileToFunctions(template)
}
}
// 调用挂载核心方法
return mountComponent(this, el)
}
function mountComponent(vm, el) {
vm.$el = el
// 调用 beforeMount 钩子
callHook(vm, 'beforeMount')
// 定义更新组件
const updateComponent = () => {
vm._update(vm._render())
}
// 创建 Watcher
new Watcher(vm, updateComponent, noop, { before() {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}})
vm._isMounted = true
callHook(vm, 'mounted')
return vm
}
三、模板语法与指令系统
1. Vue 指令概述
定义
指令(Directives)是带有 v- 前缀的特殊 attribute,用于在响应式数据变化时,将副作用反应式地应用到 DOM 上。
2. 常用内置指令
| 指令 | 作用 | 示例 |
|---|---|---|
v-bind / : |
动态绑定属性 | <img :src="imgUrl"> |
v-on / @ |
事件监听 | <button @click="handleClick"> |
v-model |
双向数据绑定 | <input v-model="msg"> |
v-if |
条件渲染(销毁/创建) | <div v-if="show"> |
v-else |
条件渲染的 else 分支 | <div v-else> |
v-else-if |
条件渲染的 else-if 分支 | <div v-else-if="type === 'A'"> |
v-show |
条件显示(CSS 切换) | <div v-show="visible"> |
v-for |
列表渲染 | <li v-for="item in list"> |
v-text |
设置文本内容 | <span v-text="msg"> |
v-html |
设置 HTML 内容 | <div v-html="htmlStr"> |
v-pre |
跳过编译 | <span v-pre>{{ raw }}</span> |
v-once |
只渲染一次 | <span v-once>{{ static }}</span> |
v-cloak |
隐藏未编译模板 | <div v-cloak>{{ msg }}</div> |
v-slot / # |
具名插槽 | <template #header> |
3. v-if 与 v-show 的区别
定义对比
| 对比维度 | v-if | v-show |
|---|---|---|
| 实现方式 | 条件为 false 时,元素从 DOM 中移除 | 条件为 false 时,设置 display: none |
| 初始渲染 | 惰性渲染(条件为 false 时不渲染) | 始终渲染 |
| 切换开销 | 较大(创建/销毁 DOM) | 较小(仅切换 CSS) |
| 初始开销 | 较小 | 较大 |
| 适用场景 | 切换频率低 | 频繁切换 |
代码示例
html
<template>
<div>
<!-- v-if:条件为 false 时,DOM 中不存在该元素 -->
<div v-if="isVisible">v-if 控制的元素</div>
<!-- v-show:条件为 false 时,DOM 中存在但 display: none -->
<div v-show="isVisible">v-show 控制的元素</div>
</div>
</template>
选择策略
- 需要频繁切换 → 使用
v-show - 运行时条件不太可能改变 → 使用
v-if - 需要配合
<template>使用 → 只能用v-if - 需要配合
v-else/v-else-if→ 只能用v-if
常见误区
- ❌ v-show 支持
<template>语法 → 实际上不支持 - ❌ v-if 性能一定差 → 初始不渲染时反而性能更好
- ❌ 两者可以混用同一元素 → 不推荐,v-if 优先级更高
4. v-for 与 v-if 的优先级
Vue 2 中 :v-for 优先级高于 v-if
html
<!-- v-for 先执行,再对每个项目判断 v-if -->
<li v-for="item in list" v-if="item.active">
{{ item.name }}
</li>
Vue 3 中 :v-if 优先级高于 v-for
最佳实践 :不要在同一元素上同时使用 v-for 和 v-if
html
<!-- ✅ 推荐:用 computed 过滤 -->
<li v-for="item in activeItems" :key="item.id">
{{ item.name }}
</li>
<script>
export default {
computed: {
activeItems() {
return this.list.filter(item => item.active)
}
}
}
</script>
<!-- ✅ 推荐:用 template 包裹 v-if -->
<template v-if="shouldShow">
<li v-for="item in list" :key="item.id">
{{ item.name }}
</li>
</template>
5. v-model 的原理
定义
v-model 是 Vue 提供的语法糖,用于在表单元素上创建双向数据绑定。
原理
v-model 本质上是 :value 和 @input 的组合:
html
<!-- 等价写法 -->
<input v-model="message">
<!-- 等同于 -->
<input :value="message" @input="message = $event.target.value">
在不同表单元素上的实现
| 表单元素 | 绑定属性 | 触发事件 |
|---|---|---|
<input type="text"> |
value |
input |
<textarea> |
value |
input |
<select> |
value |
change |
<input type="checkbox"> |
checked |
change |
<input type="radio"> |
checked |
change |
自定义组件上的 v-model
javascript
// 父组件
<custom-input v-model="searchText"></custom-input>
// 子组件 CustomInput.vue
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
template: `
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
`
}
v-model 修饰符
| 修饰符 | 作用 | 示例 |
|---|---|---|
.lazy |
将 input 事件改为 change 事件 | <input v-model.lazy="msg"> |
.number |
自动将输入转为数字 | <input v-model.number="age"> |
.trim |
自动去除首尾空格 | <input v-model.trim="name"> |
6. v-bind 与 v-model 的区别
| 对比维度 | v-bind | v-model |
|---|---|---|
| 数据流向 | 单向绑定(父→子 / 数据→视图) | 双向绑定 |
| 用途 | 绑定任意 HTML 属性 | 仅用于表单元素的双向绑定 |
| 实现 | element.attribute = value |
:value + @input |
| 示例 | <img :src="url"> |
<input v-model="text"> |
7. v-on 修饰符
事件修饰符
| 修饰符 | 作用 | 等价原生代码 |
|---|---|---|
.stop |
阻止事件冒泡 | event.stopPropagation() |
.prevent |
阻止默认行为 | event.preventDefault() |
.capture |
使用事件捕获模式 | - |
.self |
只在事件目标是当前元素时触发 | if (event.target === event.currentTarget) |
.once |
事件只触发一次 | - |
.passive |
不阻止默认行为(提升滚动性能) | - |
html
<!-- 阻止单击事件冒泡 -->
<a @click.stop="handleClick"></a>
<!-- 提交事件不再重载页面 -->
<form @submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 -->
<a @click.stop.prevent="handleClick"></a>
<!-- 只有修饰符 -->
<form @submit.prevent></form>
<!-- 点击事件只会触发一次 -->
<a @click.once="handleClick"></a>
按键修饰符
html
<!-- 只有在 key 是 Enter 时调用 -->
<input @keyup.enter="submit">
<!-- 常用按键别名 -->
.enter
.tab
.delete (捕获"删除"和"退格"键)
.esc
.space
.up
.down
.left
.right
<!-- 系统修饰键 -->
.ctrl
.alt
.shift
.meta (Mac 上是 ⌘)
<!-- 精确匹配 -->
<input @keyup.exact="handleKeyup">
<input @keyup.ctrl.exact="handleCtrlKeyup">
8. v-slot 的用法
基本用法
html
<!-- 父组件 -->
<base-layout>
<template v-slot:header>
<h1>这里是头部</h1>
</template>
<template #default>
<p>这里是默认内容</p>
</template>
<template #footer>
<p>这里是底部</p>
</template>
</base-layout>
<!-- 子组件 BaseLayout.vue -->
<template>
<div class="layout">
<header><slot name="header"></slot></header>
<main><slot></slot></main>
<footer><slot name="footer"></slot></footer>
</div>
</template>
9. v-pre / v-cloak / v-once
v-pre:跳过该元素及其子元素的编译,用于显示原始 Mustache 标签
html
<span v-pre>{{ 这里的内容不会被编译 }}</span>
<!-- 渲染结果:<span>{{ 这里的内容不会被编译 }}</span> -->
v-cloak:保持在元素上直到关联实例结束编译,配合 CSS 隐藏未编译的 Mustache 标签
css
[v-cloak] {
display: none;
}
html
<div v-cloak>
{{ message }}
</div>
v-once:只渲染元素和组件一次,后续更新跳过
html
<span v-once>{{ message }}</span>
<!-- 后续 message 变化时,这里不会更新 -->
10. 自定义指令
全局注册
javascript
// main.js
Vue.directive('focus', {
bind(el) {
// 指令绑定到元素时调用(只调用一次)
},
inserted(el) {
// 元素插入父节点时调用
el.focus()
},
update(el, binding) {
// 组件更新前调用
},
componentUpdated(el, binding) {
// 组件及其子组件更新后调用
},
unbind(el) {
// 指令解绑时调用(只调用一次)
}
})
局部注册
javascript
export default {
directives: {
focus: {
inserted(el) {
el.focus()
}
}
}
}
使用自定义指令
html
<input v-focus>
钩子函数参数
el:指令绑定的元素binding:包含name、value、oldValue、expression、arg、modifiersvnode:虚拟节点oldVnode:上一个虚拟节点
四、计算属性与侦听器
1. 计算属性(computed)
定义
计算属性是基于响应式依赖进行缓存的派生状态。只有依赖的响应式数据发生变化时,才会重新计算。
基本用法
javascript
export default {
data() {
return {
firstName: 'John',
lastName: 'Doe'
}
},
computed: {
// 只读计算属性
fullName() {
return `${this.firstName} ${this.lastName}`
}
}
}
getter 和 setter
javascript
export default {
data() {
return {
firstName: 'John',
lastName: 'Doe'
}
},
computed: {
fullName: {
get() {
return `${this.firstName} ${this.lastName}`
},
set(newValue) {
const names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
}
2. computed 与 methods 的区别
| 对比维度 | computed | methods |
|---|---|---|
| 缓存 | ✅ 有缓存(依赖不变不重新计算) | ❌ 每次调用都执行 |
| 调用方式 | 像属性一样使用 \{\{ fullName \}\} |
像函数一样调用 \{\{ getName() \}\} |
| 适用场景 | 依赖响应式数据的计算结果 | 需要传参或无依赖的计算 |
| 性能 | 更高(缓存机制) | 较低(每次执行) |
html
<template>
<div>
<!-- computed:不重新执行,直接返回缓存 -->
<p>{{ reversedMessage }}</p>
<!-- methods:每次渲染都会执行 -->
<p>{{ reverseMessage() }}</p>
</div>
</template>
<script>
export default {
data() {
return { message: 'Hello' }
},
computed: {
reversedMessage() {
return this.message.split('').reverse().join('')
}
},
methods: {
reverseMessage() {
return this.message.split('').reverse().join('')
}
}
}
</script>
3. computed 与 watch 的区别
| 对比维度 | computed | watch |
|---|---|---|
| 用途 | 派生状态(一个依赖多个 → 输出一个) | 响应数据变化(执行副作用) |
| 缓存 | ✅ 有缓存 | ❌ 无缓存 |
| 异步支持 | ❌ 不支持异步 | ✅ 支持异步 |
| 返回值 | ✅ 必须 return | ❌ 不需要 return |
| 性能 | 更高 | 较低 |
| 适用场景 | 数据转换、过滤、计算 | 异步操作、开销大的操作 |
javascript
export default {
data() {
return {
firstName: 'John',
lastName: 'Doe',
fullName: ''
}
},
computed: {
// 计算属性:自动缓存
computedFullName() {
return `${this.firstName} ${this.lastName}`
}
},
watch: {
// 侦听器:支持异步
firstName(newVal) {
this.fullName = `${newVal} ${this.lastName}`
},
// 深度监听
obj: {
handler(newVal) {
console.log('对象变化', newVal)
},
deep: true,
immediate: true
}
}
}
4. watch 的用法
基本侦听
javascript
watch: {
// 监听基础类型
message(newVal, oldVal) {
console.log(`从 ${oldVal} 变为 ${newVal}`)
},
// 监听对象(需要 deep: true)
user: {
handler(newVal) {
console.log('用户信息变化', newVal)
},
deep: true // 深度监听
},
// 立即执行
searchQuery: {
handler(newVal) {
this.fetchResults(newVal)
},
immediate: true // 组件创建时立即执行
}
}
侦听多个数据源
javascript
// Vue 2 需要分别侦听
watch: {
firstName() { this.updateFullName() },
lastName() { this.updateFullName() }
}
// 或者使用 computed 中转
computed: {
fullName() {
return `${this.firstName} ${this.lastName}`
}
},
watch: {
fullName(newVal) {
console.log('全名变化', newVal)
}
}
深度监听排除某些属性
javascript
watch: {
user: {
handler(newVal) {
// 手动处理
},
deep: true
}
}
// 或者使用 computed 排除
computed: {
userWithoutAge() {
const { age, ...rest } = this.user
return rest
}
},
watch: {
userWithoutAge(newVal) {
// 只监听除 age 外的属性
}
}
5. watch、methods、computed 三者对比
| 特性 | computed | watch | methods |
|---|---|---|---|
| 缓存 | ✅ | ❌ | ❌ |
| 异步 | ❌ | ✅ | ✅ |
| 需要传参 | ❌ | ❌ | ✅ |
| 响应式依赖 | ✅ | ✅ | 可选 |
| 使用方式 | 属性 | 回调 | 函数调用 |
五、组件系统与组件通信
1. Vue 组件概述
什么是组件?
组件是可复用的 Vue 实例,拥有自己的数据、计算属性、方法、生命周期等。
为什么要使用组件?
- 提高代码复用性
- 便于维护和测试
- 实现关注点分离
- 提高开发效率
2. 组件的创建方式
全局注册
javascript
// main.js
Vue.component('my-component', {
template: '<div>全局组件</div>'
})
局部注册
javascript
import MyComponent from './MyComponent.vue'
export default {
components: {
MyComponent
}
}
单文件组件(SFC)
html
<!-- MyComponent.vue -->
<template>
<div class="my-component">
{{ message }}
</div>
</template>
<script>
export default {
name: 'MyComponent',
data() {
return { message: 'Hello' }
}
}
</script>
<style scoped>
.my-component { color: red; }
</style>
3. 组件中 data 为什么是函数?
原因
组件可能被多次实例化,如果 data 是对象,所有实例会共享同一个 data 对象,导致数据相互影响。使用函数可以确保每个实例都有独立的 data 副本。
javascript
// ❌ 错误:data 是对象,实例间共享
Vue.component('counter', {
data: { count: 0 },
template: '<button @click="count++">{{ count }}</button>'
})
// ✅ 正确:data 是函数,每个实例独立
Vue.component('counter', {
data() {
return { count: 0 }
},
template: '<button @click="count++">{{ count }}</button>'
})
4. 组件间通信方式总览
| 通信方式 | 适用场景 | 说明 |
|---|---|---|
props / $emit |
父子组件 | 最常用 |
$parent / $children |
父子组件 | 直接访问实例 |
$refs |
父访问子 | 直接访问子组件实例 |
provide / inject |
祖先-后代 | 跨层级传递 |
$attrs / $listeners |
隔代传递 | 属性透传 |
Event Bus |
兄弟组件 | 事件总线 |
Vuex |
复杂状态 | 集中式状态管理 |
$root |
任意组件 | 访问根实例 |
5. 父子组件通信
父传子:props
html
<!-- 父组件 Parent.vue -->
<template>
<child-component :message="parentMsg" :count="5" />
</template>
<script>
export default {
data() {
return { parentMsg: 'Hello from parent' }
}
}
</script>
<!-- 子组件 Child.vue -->
<template>
<div>{{ message }} - {{ count }}</div>
</template>
<script>
export default {
props: {
message: {
type: String,
required: true
},
count: {
type: Number,
default: 0,
validator(value) {
return value >= 0
}
}
}
}
</script>
props 验证机制
javascript
props: {
// 基础类型检查
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 必填字符串
propC: {
type: String,
required: true
},
// 带有默认值
propD: {
type: Number,
default: 100
},
// 带有默认值的对象
propE: {
type: Object,
default() {
return { message: 'hello' }
}
},
// 自定义验证函数
propF: {
validator(value) {
return ['success', 'warning', 'danger'].includes(value)
}
}
}
子传父:$emit
html
<!-- 子组件 Child.vue -->
<template>
<button @click="sendToParent">发送数据给父组件</button>
</template>
<script>
export default {
methods: {
sendToParent() {
this.$emit('custom-event', { message: 'Hello parent!' })
}
}
}
</script>
<!-- 父组件 Parent.vue -->
<template>
<child-component @custom-event="handleEvent" />
</template>
<script>
export default {
methods: {
handleEvent(data) {
console.log('收到子组件数据', data)
}
}
}
</script>
v-model 实现父子双向绑定
html
<!-- 父组件 -->
<child-input v-model="parentValue" />
<!-- 子组件 ChildInput.vue -->
<script>
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
methods: {
updateValue(e) {
this.$emit('update:modelValue', e.target.value)
}
}
}
</script>
6. 兄弟组件通信
方式一:通过父组件中转
html
<!-- 父组件 -->
<template>
<div>
<child-a @send-data="handleData" />
<child-b :data="sharedData" />
</div>
</template>
<script>
export default {
data() {
return { sharedData: '' }
},
methods: {
handleData(data) {
this.sharedData = data
}
}
}
</script>
方式二:Event Bus(事件总线)
javascript
// eventBus.js
import Vue from 'vue'
export const EventBus = new Vue()
// 组件 A 发送事件
EventBus.$emit('custom-event', { data: 'hello' })
// 组件 B 接收事件
EventBus.$on('custom-event', (data) => {
console.log('收到数据', data)
})
// 组件销毁时移除监听
beforeDestroy() {
EventBus.$off('custom-event')
}
7. 跨层级通信
provide / inject
javascript
// 祖先组件
export default {
provide() {
return {
theme: 'dark',
updateUser: this.updateUser
}
},
data() {
return { user: {} }
},
methods: {
updateUser(userData) {
this.user = userData
}
}
}
// 后代组件(无论多深)
export default {
inject: ['theme', 'updateUser'],
mounted() {
console.log('当前主题', this.theme)
this.updateUser({ name: 'John' })
}
}
provide/inject 与 props 的区别
| 对比维度 | props | provide/inject |
|---|---|---|
| 传递范围 | 直接父子 | 任意层级的祖先-后代 |
| 响应式 | ✅ 是 | 默认非响应式(传递对象属性可响应) |
| 适用场景 | 常规父子通信 | 插件、主题等跨层级配置 |
<math xmlns="http://www.w3.org/1998/Math/MathML"> a t t r s / attrs / </math>attrs/listeners
html
<!-- 父组件传递 -->
<child-a :foo="foo" :bar="bar" @click="handleClick" />
<!-- 子组件 ChildA.vue(透传给孙组件) -->
<template>
<grand-child v-bind="$attrs" v-on="$listeners" />
</template>
<script>
export default {
inheritAttrs: false, // 阻止默认行为
// $attrs 包含所有未声明的 props
// $listeners 包含所有事件监听器
}
</script>
8. $refs 的使用
html
<template>
<div>
<input ref="myInput" />
<child-component ref="myChild" />
<ul>
<li v-for="item in list" :key="item.id" ref="items">
{{ item.name }}
</li>
</ul>
</div>
</template>
<script>
export default {
mounted() {
// 访问 DOM 元素
this.$refs.myInput.focus()
// 访问子组件实例
this.$refs.myChild.childMethod()
// v-for 中的 refs 是数组
this.$refs.items.forEach(el => {
console.log(el)
})
}
}
</script>
9. 组件中 name 选项的作用
- 递归组件:组件调用自身时需要 name
- Vue Devtools:调试时显示组件名称
- keep-alive 匹配 :
include/exclude属性依赖 name - 动态组件:注册全局组件时需要 name
javascript
export default {
name: 'TreeMenu',
// 递归调用自身
components: {
TreeMenu
}
}
10. 动态组件与异步组件
动态组件
html
<template>
<!-- 根据 currentTab 动态切换组件 -->
<component :is="currentTabComponent"></component>
</template>
<script>
import TabHome from './TabHome.vue'
import TabPosts from './TabPosts.vue'
import TabArchive from './TabArchive.vue'
export default {
data() {
return { currentTab: 'Home' }
},
computed: {
currentTabComponent() {
return {
Home: TabHome,
Posts: TabPosts,
Archive: TabArchive
}[this.currentTab]
}
}
}
</script>
异步组件
javascript
// 工厂函数方式
Vue.component('async-component', () => import('./AsyncComponent.vue'))
// 带加载状态
const AsyncComponent = () => ({
component: import('./AsyncComponent.vue'),
loading: LoadingComponent,
error: ErrorComponent,
delay: 200,
timeout: 3000
})
// 局部注册
export default {
components: {
AsyncComponent: () => import('./AsyncComponent.vue')
}
}
11. 递归组件
html
<!-- TreeItem.vue -->
<template>
<li>
<div @click="toggle">{{ item.name }}</div>
<ul v-show="isOpen" v-if="item.children">
<tree-item
v-for="child in item.children"
:key="child.id"
:item="child"
/>
</ul>
</li>
</template>
<script>
export default {
name: 'TreeItem', // 必须有 name 才能递归
props: ['item'],
data() {
return { isOpen: false }
},
methods: {
toggle() {
this.isOpen = !this.isOpen
}
}
}
</script>
12. 如何解决 Vue 组件的命名冲突?
javascript
// 方式一:使用别名
import { Button as MyButton } from './MyButton.vue'
import { Button } from 'element-ui'
export default {
components: {
MyButton,
ElButton: Button
}
}
// 方式二:使用 name 选项区分
export default {
name: 'CustomButton'
}
13. 如何重置组件的 data?
javascript
export default {
data() {
return {
form: { name: '', age: 0 },
isLoading: false
}
},
methods: {
resetData() {
// 方式一:重新调用 data 函数
Object.assign(this.$data, this.$options.data.call(this))
// 方式二:逐个重置
this.form = { name: '', age: 0 }
this.isLoading = false
}
}
}
六、插槽(Slots)
1. 插槽概述
定义
插槽(Slot)是 Vue 提供的组件内容分发机制,允许父组件向子组件传递内容。
2. 默认插槽
html
<!-- 子组件 Alert.vue -->
<template>
<div class="alert">
<slot>默认内容(当没有传入内容时显示)</slot>
</div>
</template>
<!-- 父组件 -->
<alert>
<p>自定义内容</p>
</alert>
3. 具名插槽
html
<!-- 子组件 BaseLayout.vue -->
<template>
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot> <!-- 默认插槽 -->
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
<!-- 父组件 -->
<base-layout>
<template v-slot:header>
<h1>页面标题</h1>
</template>
<p>主要内容</p>
<template #footer>
<p>版权信息</p>
</template>
</base-layout>
4. 作用域插槽
子组件暴露数据给父组件
html
<!-- 子组件 UserList.vue -->
<template>
<ul>
<li v-for="user in users" :key="user.id">
<slot :user="user" :index="index">
{{ user.name }}
</slot>
</li>
</ul>
</template>
<script>
export default {
data() {
return {
users: [
{ id: 1, name: 'John', age: 25 },
{ id: 2, name: 'Jane', age: 30 }
]
}
}
}
</script>
<!-- 父组件 -->
<user-list>
<template v-slot:default="slotProps">
<strong>{{ slotProps.user.name }}</strong>
<span>年龄: {{ slotProps.user.age }}</span>
</template>
</user-list>
<!-- 简写 -->
<user-list v-slot="{ user }">
<strong>{{ user.name }}</strong>
</user-list>
5. 插槽的使用场景
- 组件封装:如对话框、弹窗、卡片组件
- 列表渲染定制:表格、列表等组件的单元格定制
- 布局组件:页面布局的 header、footer、sidebar
- 表单组件:表单项的额外内容
七、Vue Router 路由
1. Vue Router 概述
定义
Vue Router 是 Vue.js 官方的路由管理器,用于构建单页应用(SPA),实现页面组件的动态切换。
2. 基本使用步骤
javascript
// 1. 定义路由组件
const Home = { template: '<div>Home</div>' }
const About = { template: '<div>About</div>' }
// 2. 定义路由配置
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About }
]
// 3. 创建 router 实例
const router = new VueRouter({
routes
})
// 4. 挂载到 Vue 实例
new Vue({
router,
render: h => h(App)
}).$mount('#app')
3. 路由模式
| 模式 | URL 示例 | 原理 | 特点 |
|---|---|---|---|
| hash 模式 | http://example.com/#/home |
利用 window.location.hash |
兼容性好,不需要服务端配置 |
| history 模式 | http://example.com/home |
利用 HTML5 History API | URL 更美观,需要服务端配置 |
hash 模式原理
javascript
// 监听 hash 变化
window.addEventListener('hashchange', () => {
const hash = window.location.hash.slice(1)
// 根据 hash 渲染对应组件
renderComponent(hash)
})
history 模式原理
javascript
// 监听浏览器前进/后退
window.addEventListener('popstate', () => {
const path = window.location.pathname
// 根据 path 渲染对应组件
renderComponent(path)
})
// pushState 改变 URL 不刷新页面
history.pushState({}, '', '/new-path')
history 模式服务端配置(Nginx)
nginx
location / {
try_files $uri $uri/ /index.html;
}
选择策略
- 需要兼容老浏览器 → hash 模式
- 需要更好 SEO → history 模式(配合 SSR)
- 简单项目 → hash 模式(无需服务端配置)
4. 动态路由
javascript
const routes = [
// 动态路径参数,以冒号开头
{ path: '/user/:id', component: User }
]
// 匹配 /user/1 → $route.params.id = '1'
// 匹配 /user/2 → $route.params.id = '2'
响应路由参数变化
javascript
export default {
watch: {
// 路由变化时重新获取数据
$route(to, from) {
this.fetchUser(to.params.id)
}
},
// 或使用路由守卫
beforeRouteUpdate(to, from, next) {
this.fetchUser(to.params.id)
next()
}
}
5. 路由传参
方式一:动态路由参数
javascript
// 配置
{ path: '/user/:id', component: User }
// 跳转
this.$router.push('/user/123')
// 获取
this.$route.params.id // '123'
方式二:query 参数
javascript
// 跳转
this.$router.push({ path: '/search', query: { keyword: 'vue' } })
// 获取
this.$route.query.keyword // 'vue'
方式三:params(命名路由)
javascript
// 配置
{ path: '/user/:id', name: 'user', component: User }
// 跳转
this.$router.push({ name: 'user', params: { id: '123' } })
// 获取
this.$route.params.id // '123'
6. 嵌套路由
javascript
const routes = [
{
path: '/user/:id',
component: User,
children: [
{
path: 'profile', // 相对路径
component: UserProfile
},
{
path: 'posts',
component: UserPosts
}
]
}
]
html
<!-- User.vue 中需要放置 router-view -->
<template>
<div>
<h2>User {{ $route.params.id }}</h2>
<router-view></router-view>
</div>
</template>
7. 编程式导航
javascript
// 字符串路径
this.$router.push('/home')
// 对象路径
this.$router.push({ path: '/home' })
// 命名路由
this.$router.push({ name: 'user', params: { id: '123' } })
// 带查询参数
this.$router.push({ path: '/search', query: { q: 'vue' } })
// 前进/后退
this.$router.go(1) // 前进一条记录
this.$router.go(-1) // 后退一条记录
this.$router.forward() // 前进
this.$router.back() // 后退
// 替换当前记录(不留下历史)
this.$router.replace('/home')
8. 路由守卫
全局守卫
javascript
const router = new VueRouter({ ... })
// 全局前置守卫
router.beforeEach((to, from, next) => {
// 检查登录状态
const token = localStorage.getItem('token')
if (to.meta.requiresAuth && !token) {
next('/login')
} else {
next()
}
})
// 全局解析守卫
router.beforeResolve((to, from, next) => {
next()
})
// 全局后置钩子
router.afterEach((to, from) => {
// 不能调用 next()
console.log('导航完成')
})
路由独享守卫
javascript
const routes = [
{
path: '/dashboard',
component: Dashboard,
beforeEnter: (to, from, next) => {
// 只在进入该路由时触发
next()
}
}
]
组件内守卫
javascript
export default {
// 进入组件前
beforeRouteEnter(to, from, next) {
// 不能获取组件实例 this
next(vm => {
// 组件实例已创建,可以访问 vm
})
},
// 路由更新时
beforeRouteUpdate(to, from, next) {
// 当前路由改变但组件被复用时调用
next()
},
// 离开组件前
beforeRouteLeave(to, from, next) {
// 可以访问 this
const answer = window.confirm('确定离开吗?')
if (answer) {
next()
} else {
next(false)
}
}
}
守卫执行顺序
markdown
1. 导航被触发
2. 在失活组件中调用 beforeRouteLeave
3. 调用全局 beforeEach
4. 在重用组件中调用 beforeRouteUpdate
5. 在路由配置中调用 beforeEnter
6. 解析异步路由组件
7. 在被激活组件中调用 beforeRouteEnter
8. 调用全局 beforeResolve
9. 导航被确认
10. 调用全局 afterEach
11. 触发 DOM 更新
12. 调用 beforeRouteEnter 的 next 回调
9. 路由懒加载
定义
路由懒加载是将路由对应的组件打包成独立的 chunk,在访问该路由时才加载对应组件,从而减少首屏加载时间。
实现方式
javascript
// Vue CLI 项目(使用动态 import)
const routes = [
{
path: '/about',
component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
},
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue')
}
]
分组懒加载
javascript
// 将多个组件打包到同一个 chunk
const routes = [
{
path: '/user',
component: () => import(/* webpackChunkName: "user" */ './views/User.vue')
},
{
path: '/profile',
component: () => import(/* webpackChunkName: "user" */ './views/Profile.vue')
}
]
10. <math xmlns="http://www.w3.org/1998/Math/MathML"> r o u t e r 与 router 与 </math>router与route 的区别
| 对比维度 | $router | $route |
|---|---|---|
| 类型 | VueRouter 实例 | 当前路由信息对象 |
| 用途 | 导航(跳转、前进、后退) | 获取路由信息(参数、路径等) |
| 常用方法/属性 | push()、replace()、go()、back() |
params、query、path、name、meta |
javascript
// $router:用于导航
this.$router.push('/home')
this.$router.replace('/login')
this.$router.go(-1)
// $route:用于获取信息
this.$route.params.id // 动态参数
this.$route.query.keyword // 查询参数
this.$route.path // 当前路径
this.$route.name // 路由名称
this.$route.meta // 元信息
11. 路由元信息(meta)
javascript
const routes = [
{
path: '/admin',
component: Admin,
meta: {
requiresAuth: true,
title: '管理后台',
roles: ['admin', 'super_admin']
}
}
]
// 使用
router.beforeEach((to, from, next) => {
document.title = to.meta.title || '默认标题'
if (to.meta.requiresAuth) {
// 检查权限
}
next()
})
八、Vuex 状态管理
1. Vuex 概述
定义
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
应用场景
- 多个组件共享状态
- 不同组件需要更改同一状态
- 状态变化逻辑复杂
Vuex 与 localStorage 的区别
| 对比维度 | Vuex | localStorage |
|---|---|---|
| 存储位置 | 内存 | 浏览器本地存储 |
| 响应式 | ✅ 是 | ❌ 否 |
| 持久化 | ❌ 刷新丢失(需配合插件) | ✅ 持久化 |
| 适用场景 | 组件间状态共享 | 数据持久化 |
| 数据量 | 适合复杂状态 | 适合简单键值对 |
2. Vuex 核心概念
scss
┌──────────────────────────────────────────┐
│ Vuex │
│ │
│ Component │
│ │ │
│ │ dispatch │
│ ▼ │
│ ┌─────────┐ ┌─────────────────────┐ │
│ │ Actions │────▶│ Mutations │ │
│ │ (异步) │ │ (同步,修改 state) │ │
│ └─────────┘ └─────────────────────┘ │
│ │ │
│ │ commit │
│ ▼ │
│ ┌─────────┐ │
│ │ State │ │
│ └─────────┘ │
│ │ │
│ │ getters │
│ ▼ │
│ ┌─────────┐ │
│ │Getters │ │
│ └─────────┘ │
│ │ │
│ ▼ │
│ Component │
└──────────────────────────────────────────┘
数据流
组件触发 Action → Action 执行异步操作 → Action 提交 Mutation → Mutation 修改 State → State 变化触发组件更新
3. State
定义
State 是 Vuex 的单一状态树,包含应用中所有的状态数据。
javascript
// store.js
const store = new Vuex.Store({
state: {
count: 0,
user: null,
todos: []
}
})
// 组件中访问
computed: {
count() {
return this.$store.state.count
}
}
// 使用 mapState 辅助函数
import { mapState } from 'vuex'
export default {
computed: {
...mapState(['count', 'user']),
...mapState({
countAlias: 'count',
countPlusLocal(state) {
return state.count + this.localCount
}
})
}
}
4. Getter
定义
Getter 类似于计算属性,用于从 state 中派生出一些状态。
javascript
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '学习 Vue', done: true },
{ id: 2, text: '学习 Vuex', done: false }
]
},
getters: {
// 基础 getter
doneTodos(state) {
return state.todos.filter(todo => todo.done)
},
// 接收其他 getter
doneTodosCount(state, getters) {
return getters.doneTodos.length
},
// 返回函数(可传参)
getTodoById(state) {
return id => state.todos.find(todo => todo.id === id)
}
}
})
// 组件中使用
computed: {
...mapGetters(['doneTodos', 'doneTodosCount'])
}
5. Mutation
定义
Mutation 是更改 Vuex store 中状态的唯一方法,必须是同步函数。
javascript
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
// 基础用法
increment(state) {
state.count++
},
// 接收载荷(参数)
incrementBy(state, payload) {
state.count += payload.amount
},
// 对象风格的提交
incrementBy(state, payload) {
state.count += payload.amount
}
}
})
// 组件中触发
this.$store.commit('increment')
this.$store.commit('incrementBy', { amount: 10 })
// 使用 mapMutations
import { mapMutations } from 'vuex'
export default {
methods: {
...mapMutations(['increment', 'incrementBy'])
}
}
Mutation 必须是同步函数
如果 Mutation 包含异步操作,Devtools 无法正确追踪状态变化。
6. Action
定义
Action 类似于 Mutation,但可以包含异步操作,不能直接修改 state,需要提交 Mutation。
javascript
const store = new Vuex.Store({
state: {
user: null
},
mutations: {
setUser(state, user) {
state.user = user
}
},
actions: {
// 基础用法
async login({ commit }, credentials) {
const user = await api.login(credentials)
commit('setUser', user)
},
// 解构上下文
async fetchData({ commit, state, getters, dispatch }) {
const data = await api.getData()
commit('setData', data)
},
// 组合多个 Action
async checkout({ commit, dispatch }, cart) {
try {
await api.checkout(cart)
dispatch('clearCart')
commit('setOrderSuccess', true)
} catch (error) {
commit('setOrderError', error)
}
}
}
})
// 组件中触发
this.$store.dispatch('login', { username: 'user', password: '123' })
// 使用 mapActions
import { mapActions } from 'vuex'
export default {
methods: {
...mapActions(['login', 'fetchData'])
}
}
7. Module
定义
当应用变得复杂时,store 对象会变得臃肿。Vuex 允许将 store 分割成模块(module)。
javascript
// modules/user.js
const user = {
namespaced: true, // 开启命名空间
state: () => ({
name: '',
token: ''
}),
mutations: {
setName(state, name) {
state.name = name
}
},
actions: {
async login({ commit }, credentials) {
const res = await api.login(credentials)
commit('setName', res.name)
}
},
getters: {
isLoggedIn(state) {
return !!state.token
}
}
}
// modules/cart.js
const cart = {
namespaced: true,
state: () => ({
items: []
}),
mutations: {
addItem(state, item) {
state.items.push(item)
}
}
}
// store/index.js
const store = new Vuex.Store({
modules: {
user,
cart
}
})
// 组件中使用
this.$store.state.user.name
this.$store.getters['user/isLoggedIn']
this.$store.dispatch('user/login', credentials)
this.$store.commit('cart/addItem', item)
// 使用辅助函数
import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('user')
export default {
computed: {
...mapState(['name', 'token'])
},
methods: {
...mapActions(['login'])
}
}
8. Vuex 与全局事件总线的区别
| 对比维度 | Vuex | Event Bus |
|---|---|---|
| 状态追踪 | Devtools 可追踪 | 无法追踪 |
| 响应式 | ✅ 是 | ❌ 否 |
| 调试 | 时间旅行调试 | 难以调试 |
| 适用场景 | 复杂状态管理 | 简单事件通知 |
| 规则 | 严格的单向数据流 | 随意触发 |
9. Vuex 严格模式
javascript
const store = new Vuex.Store({
strict: process.env.NODE_ENV !== 'production',
// ...
})
作用:在严格模式下,任何在 mutation 之外修改 state 的行为都会抛出错误。
九、响应式原理与核心原理
1. Vue 响应式原理(Vue 2)
核心机制
Vue 2 使用 Object.defineProperty 实现数据劫持,结合发布-订阅模式实现响应式。
实现流程
markdown
1. 数据初始化(data 选项)
↓
2. 遍历 data 的每个属性
↓
3. 使用 Object.defineProperty 将属性转为 getter/setter
↓
4. getter 中进行依赖收集(Dep.depend)
↓
5. setter 中派发更新(Dep.notify)
↓
6. Watcher 接收更新通知,执行回调更新视图
简化源码解读
javascript
// 1. Observer:将数据转为响应式
class Observer {
constructor(data) {
this.walk(data)
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
// 2. 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 (newVal === val) return
val = newVal
// 派发更新
dep.notify()
}
})
}
// 3. Dep:依赖收集器
class Dep {
constructor() {
this.subs = []
}
depend() {
if (Dep.target) {
this.subs.push(Dep.target)
}
}
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
// 4. Watcher:订阅者
class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm
this.cb = cb
this.getter = typeof expOrFn === 'function' ? expOrFn : () => vm[expOrFn]
this.value = this.get()
}
get() {
Dep.target = this
const value = this.getter.call(this.vm)
Dep.target = null
return value
}
update() {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
依赖收集流程
scss
组件渲染 → 访问响应式数据 → 触发 getter → Dep.depend() 收集 Watcher → 数据变化 → 触发 setter → Dep.notify() → Watcher.update() → 重新渲染
2. Vue 3 响应式原理(Proxy)
Vue 3 改用 Proxy 的原因
Object.defineProperty只能劫持已有属性,无法检测属性添加/删除- 无法检测数组索引和长度变化
- Proxy 性能更好
Proxy 实现
javascript
function reactive(target) {
return new Proxy(target, {
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
}
})
}
3. Vue 双向绑定原理
实现机制
markdown
1. 响应式系统:Object.defineProperty/Proxy 数据劫持
↓
2. 模板编译:解析模板中的指令和插值表达式
↓
3. 依赖收集:视图与数据建立依赖关系
↓
4. 派发更新:数据变化时自动更新视图
↓
5. v-model:表单元素上实现双向绑定
完整流程
csharp
┌─────────────────────────────────────────────┐
│ 双向绑定原理 │
│ │
│ View ←─── v-model ───→ ViewModel │
│ │ │ │
│ │ │ │
│ ▼ ▼ │
│ DOM 事件 响应式数据 │
│ │ │ │
│ │ │ │
│ ▼ ▼ │
│ input event → 更新数据 → setter → 通知更新 │
│ │
└─────────────────────────────────────────────┘
4. Vue 异步更新队列
原理
Vue 在更新 DOM 时是异步执行的。当侦听到数据变化时,会开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入队列一次。
作用
- 避免不必要的计算和 DOM 操作
- 提升性能
流程
数据变化 → Watcher 入队 → nextTick 执行 → 批量更新 DOM
源码简析
javascript
// 简化的异步更新逻辑
const queue = []
let pending = false
function queueWatcher(watcher) {
queue.push(watcher)
if (!pending) {
pending = true
// 使用微任务优先
Promise.resolve().then(flushQueue)
}
}
function flushQueue() {
queue.forEach(watcher => {
watcher.run()
})
queue = []
pending = false
}
5. Vue.nextTick
定义
nextTick 用于在下次 DOM 更新循环结束之后执行延迟回调。
使用场景
javascript
export default {
data() {
return { message: 'Hello' }
},
methods: {
async updateMessage() {
this.message = 'New message'
// 此时 DOM 还未更新
console.log(this.$refs.msg.textContent) // 还是旧值
// 等待 DOM 更新
await this.$nextTick()
console.log(this.$refs.msg.textContent) // 新值
}
}
}
实现原理
javascript
// Vue 内部实现(降级策略)
let timerFunc
if (typeof Promise !== 'undefined') {
// 优先使用 Promise(微任务)
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
}
} else if (typeof MutationObserver !== 'undefined') {
// 使用 MutationObserver
} else if (typeof setImmediate !== 'undefined') {
// 使用 setImmediate
} else {
// 最后使用 setTimeout(宏任务)
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
6. Vue 虚拟 DOM
定义
虚拟 DOM 是用 JavaScript 对象来描述真实 DOM 结构的一种抽象。
为什么需要虚拟 DOM?
- 减少 DOM 操作,提升性能
- 跨平台(如 Weex、服务端渲染)
- 提供 diff 算法基础
VNode 结构
javascript
class VNode {
constructor(tag, data, children, text, elm, context) {
this.tag = tag // 标签名
this.data = data // 属性、事件等
this.children = children // 子节点
this.text = text // 文本内容
this.elm = elm // 对应的真实 DOM
this.key = data && data.key
}
}
// 示例
{
tag: 'div',
data: {
attrs: { id: 'app' },
class: { active: true }
},
children: [
{
tag: 'span',
text: 'Hello Vue'
}
]
}
7. Vue diff 算法
定义
diff 算法用于比较新旧虚拟 DOM 树的差异,最小化 DOM 操作。
核心策略
- 同层比较,不跨级比较
- 使用 key 优化列表渲染
- 双端比较(Vue 2)
Vue 2 diff 流程
markdown
1. 新旧节点比较
↓
2. 如果是相同节点,递归比较子节点
↓
3. 子节点比较:
- 旧头与新头比较
- 旧尾与新尾比较
- 旧头与新尾比较
- 旧尾与新头比较
- 都不匹配时,用 key 查找
↓
4. 批量更新 DOM
源码简析
javascript
function patch(oldVnode, vnode) {
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
// 不同节点,直接替换
const elm = oldVnode.parentNode
const parentElm = elm.parentNode
createElm(vnode)
parentElm.insertBefore(vnode.elm, elm)
parentElm.removeChild(elm)
}
}
function patchVnode(oldVnode, vnode) {
const oldCh = oldVnode.children
const newCh = vnode.children
if (oldCh && newCh && oldCh !== newCh) {
// 比较子节点列表
updateChildren(oldCh, newCh)
} else if (newCh) {
// 新增子节点
addVnodes(newCh)
} else if (oldCh) {
// 删除子节点
removeVnodes(oldCh)
}
}
8. key 的作用
为什么列表渲染必须使用 key?
- 唯一标识节点:帮助 diff 算法识别节点身份
- 复用节点:提高渲染性能
- 维持状态:避免组件状态混乱
无 key 的问题
html
<!-- ❌ 不推荐:无 key -->
<li v-for="item in list">{{ item.name }}</li>
<!-- ✅ 推荐:使用唯一 key -->
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
key 的最佳实践
- ✅ 使用唯一且稳定的 ID(如数据库主键)
- ❌ 不要使用 index 作为 key(列表顺序变化时会出错)
- ❌ 不要使用随机数(每次渲染都不同)
javascript
// ❌ 错误:使用 index
<li v-for="(item, index) in list" :key="index">
// ❌ 错误:使用随机数
<li v-for="item in list" :key="Math.random()">
// ✅ 正确:使用唯一 ID
<li v-for="item in list" :key="item.id">
9. Vue 模板编译过程
编译三阶段
markdown
1. 解析(Parse):将模板字符串转换为 AST(抽象语法树)
↓
2. 优化(Optimize):标记静态节点,优化渲染性能
↓
3. 生成(Generate):将 AST 转换为 render 函数
源码流程
javascript
// 1. parse:模板 → AST
function parse(template) {
// 词法分析:解析标签、属性、文本等
// 构建 AST 树
return ast
}
// 2. optimize:标记静态节点
function optimize(ast) {
// 遍历 AST,标记静态节点
// 静态节点不会变化,渲染时可跳过
markStatic(ast)
}
// 3. generate:AST → render 函数
function generate(ast) {
const code = generateCode(ast)
return {
render: `with(this){return ${code}}`,
staticRenderFns: []
}
}
AST 节点示例
javascript
{
type: 1,
tag: 'div',
attrsList: [{ name: 'id', value: 'app' }],
attrsMap: { id: 'app' },
parent: undefined,
children: [
{
type: 2,
expression: "'Hello ' + name",
text: 'Hello {{name}}',
tokens: ['Hello ', { '@binding': 'name' }]
}
]
}
10. Vue 渲染过程
markdown
1. 模板编译 → render 函数
↓
2. 执行 render 函数 → VNode
↓
3. patch 函数:VNode → 真实 DOM
↓
4. 数据变化 → 重新执行 render → 新 VNode
↓
5. diff 算法:新旧 VNode 比较
↓
6. 最小化 DOM 操作
十、keep-alive 组件
1. keep-alive 的作用
定义
<keep-alive> 是 Vue 的内置组件,用于缓存组件实例,避免重复渲染。
使用场景
- 列表页→详情页→返回列表页(保留滚动位置、筛选状态)
- 标签页切换
- 需要保留组件状态的场景
2. keep-alive 的属性
| 属性 | 类型 | 说明 |
|---|---|---|
include |
String/RegExp/Array | 只有名称匹配的组件会被缓存 |
exclude |
String/RegExp/Array | 名称匹配的组件不会被缓存 |
max |
Number | 最多可以缓存多少组件实例 |
html
<!-- 缓存指定组件 -->
<keep-alive include="Home,About">
<component :is="currentComponent" />
</keep-alive>
<!-- 使用正则 -->
<keep-alive :include="/^app-/">
<router-view />
</keep-alive>
<!-- 排除某些组件 -->
<keep-alive exclude="NoCache">
<router-view />
</keep-alive>
<!-- 限制最大缓存数 -->
<keep-alive :max="10">
<router-view />
</keep-alive>
3. keep-alive 的生命周期变化
被 keep-alive 缓存的组件:
- 不会触发
beforeDestroy和destroyed - 新增
activated和deactivated钩子
javascript
export default {
activated() {
console.log('组件被激活(进入缓存)')
// 恢复状态
},
deactivated() {
console.log('组件被停用(退出缓存)')
// 保存状态
}
}
4. keep-alive 实现原理
javascript
// 简化版实现
export default {
name: 'KeepAlive',
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: Number
},
created() {
this.cache = Object.create(null) // 缓存对象
this.keys = [] // 缓存的 key
},
destroyed() {
// 销毁所有缓存
Object.keys(this.cache).forEach(key => {
pruneCacheEntry(this.cache[key])
})
},
render() {
const slot = this.$slots.default
const vnode = slot[0]
const name = vnode.componentOptions.name
// 检查是否需要缓存
if (this.shouldCache(name)) {
const key = vnode.key == null
? vnode.componentOptions.Ctor.cid + '::' + vnode.tag
: vnode.key
if (this.cache[key]) {
vnode.componentInstance = this.cache[key].componentInstance
} else {
this.cache[key] = vnode
this.keys.push(key)
// 超过 max 时,清理最久未使用的缓存
if (this.max && this.keys.length > this.max) {
pruneCacheEntry(this.cache[this.keys[0]])
}
}
vnode.data.keepAlive = true
}
return vnode
}
}
十一、性能优化
1. Vue 性能优化总览
| 优化方向 | 具体方法 |
|---|---|
| 代码层面 | 合理使用 computed、v-show/v-if、v-for 绑定 key |
| 组件层面 | 组件懒加载、keep-alive 缓存、函数式组件 |
| 构建层面 | 路由懒加载、代码分割、Tree Shaking |
| 资源层面 | 图片优化、CDN 加速、gzip 压缩 |
| 渲染层面 | 虚拟滚动、防抖节流、避免 v-if 与 v-for 同时使用 |
2. 首屏加载优化
javascript
// 1. 路由懒加载
const routes = [
{
path: '/home',
component: () => import('./views/Home.vue')
}
]
// 2. 组件按需加载
import { Button, Input } from 'element-ui'
Vue.use(Button)
Vue.use(Input)
// 3. CDN 外部化
// vue.config.js
module.exports = {
configureWebpack: {
externals: {
vue: 'Vue',
'vue-router': 'VueRouter'
}
}
}
// 4. 图片懒加载
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload)
<img v-lazy="imageSrc">
3. 长列表优化
虚拟滚动
html
<template>
<div class="list-container" ref="container" @scroll="handleScroll">
<div class="list-spacer" :style="{ height: totalHeight + 'px' }">
<div
v-for="item in visibleItems"
:key="item.id"
:style="{ transform: `translateY(${item.top}px)` }"
class="list-item"
>
{{ item.name }}
</div>
</div>
</div>
</template>
<script>
export default {
props: ['items'],
data() {
return {
itemHeight: 50,
startIndex: 0,
visibleCount: 20
}
},
computed: {
totalHeight() {
return this.items.length * this.itemHeight
},
visibleItems() {
const end = Math.min(this.startIndex + this.visibleCount, this.items.length)
return this.items.slice(this.startIndex, end).map((item, i) => ({
...item,
top: (this.startIndex + i) * this.itemHeight
}))
}
},
methods: {
handleScroll() {
const scrollTop = this.$refs.container.scrollTop
this.startIndex = Math.floor(scrollTop / this.itemHeight)
}
}
}
</script>
4. v-memo 性能优化(Vue 3)
html
<!-- 只有当 item.id 或 item.selected 变化时才重新渲染 -->
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.selected]">
{{ item.name }}
</div>
5. 避免不必要的重新渲染
javascript
// ✅ 使用 Object.freeze() 冻结不需要响应式的大对象
export default {
data() {
return {
// 大量静态数据不需要响应式
hugeList: Object.freeze(largeDataArray)
}
}
}
// ✅ 合理使用 v-if 和 v-show
// 频繁切换用 v-show,不频繁切换用 v-if
// ✅ 组件拆分,减少渲染范围
// 将大组件拆分为小组件,精准控制更新范围
6. 组件懒加载
javascript
// 方式一:异步组件
components: {
HeavyComponent: () => import('./HeavyComponent.vue')
}
// 方式二:带加载状态
const HeavyComponent = () => ({
component: import('./HeavyComponent.vue'),
loading: LoadingSpinner,
error: ErrorComponent,
delay: 200,
timeout: 5000
})
十二、Mixins 混入
1. Mixins 概述
定义
Mixins 是一种分发 Vue 组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项,当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。
2. Mixins 的使用
javascript
// mixin.js
export const myMixin = {
data() {
return {
mixinData: 'hello'
}
},
created() {
console.log('mixin created')
},
methods: {
mixinMethod() {
console.log('mixin method')
}
}
}
// 组件中使用
import { myMixin } from './mixin'
export default {
mixins: [myMixin],
created() {
console.log('component created')
}
}
3. 选项合并策略
| 选项类型 | 合并策略 |
|---|---|
| data | 递归合并,组件数据优先 |
| 生命周期钩子 | 合并为数组,先执行 mixin,再执行组件 |
| methods/components/directives | 对象合并,组件选项优先 |
4. 全局 Mixin
javascript
// main.js
Vue.mixin({
created() {
console.log('全局 mixin')
}
})
⚠️ 全局 mixin 慎用:会影响所有 Vue 实例,可能导致难以追踪的 bug。
5. Mixins 的优缺点
优点
- 代码复用
- 逻辑抽离
缺点
- 命名冲突
- 来源不清晰(不清楚方法来自哪个 mixin)
- 多 mixin 时依赖关系复杂
Vue 3 替代方案:组合式 API(Composition API)
javascript
// Vue 3 中推荐使用 Composables
import { useMouse, useFetch } from './composables'
export default {
setup() {
const { x, y } = useMouse()
const { data, error } = useFetch('/api/data')
return { x, y, data, error }
}
}
十三、自定义指令深入
1. 自定义指令钩子函数
javascript
Vue.directive('my-directive', {
// 只调用一次,指令第一次绑定到元素时
bind(el, binding, vnode) {
// 可以进行一次性的初始化设置
},
// 被绑定元素插入父节点时调用
inserted(el, binding, vnode) {
// 可以操作父节点
},
// 所在组件的 VNode 更新时调用
update(el, binding, vnode, oldVnode) {
// 可能发生在其子 VNode 更新之前
},
// 指令所在组件的 VNode 及其子 VNode 全部更新后调用
componentUpdated(el, binding, vnode, oldVnode) {
// 可以访问更新后的 DOM
},
// 只调用一次,指令与元素解绑时
unbind(el, binding, vnode) {
// 清理工作
}
})
2. 实战示例
防抖指令
javascript
Vue.directive('debounce-click', {
bind(el, binding) {
let timeoutId = null
el.addEventListener('click', (e) => {
if (timeoutId) return
binding.value(e)
timeoutId = setTimeout(() => {
timeoutId = null
}, binding.arg || 500)
})
}
})
// 使用
<button v-debounce-click:1000="handleSubmit">提交</button>
权限指令
javascript
Vue.directive('permission', {
bind(el, binding) {
const permissions = store.getters.permissions
if (!permissions.includes(binding.value)) {
el.parentNode && el.parentNode.removeChild(el)
}
}
})
// 使用
<button v-permission="'admin:delete'">删除</button>
十四、过滤器(Filters)
1. 过滤器概述
定义
过滤器用于文本格式转换,可在插值表达式和 v-bind 表达式中使用。
⚠️ 注意:Vue 3 中已移除过滤器,推荐使用计算属性或方法替代。
2. 使用方式
全局注册
javascript
Vue.filter('capitalize', function(value) {
if (!value) return ''
return value.toString().charAt(0).toUpperCase() + value.slice(1)
})
局部注册
javascript
export default {
filters: {
capitalize(value) {
if (!value) return ''
return value.charAt(0).toUpperCase() + value.slice(1)
},
formatDate(value, format = 'YYYY-MM-DD') {
return dayjs(value).format(format)
}
}
}
使用
html
<!-- 插值表达式 -->
{{ message | capitalize }}
<!-- v-bind -->
<div :title="message | capitalize"></div>
<!-- 串联 -->
{{ message | filterA | filterB }}
<!-- 传参 -->
{{ date | formatDate('YYYY/MM/DD') }}
十五、样式隔离
1. Scoped CSS
原理
Vue 为 scoped 样式添加唯一的属性选择器来实现样式隔离。
html
<style scoped>
.title {
color: red;
}
</style>
<!-- 编译后 -->
<style>
.title[data-v-f3f3eg9] {
color: red;
}
</style>
<!-- 模板编译后 -->
<h1 data-v-f3f3eg9 class="title">标题</h1>
2. 深度选择器
html
<style scoped>
/* 修改子组件样式 */
.parent >>> .child {
color: red;
}
/* SCSS/LESS 使用 /deep/ 或 ::v-deep */
.parent /deep/ .child {
color: red;
}
.parent ::v-deep .child {
color: red;
}
/* Vue 3 */
:deep(.child) {
color: red;
}
</style>
3. CSS Modules
html
<template>
<div :class="$style.container">
<p :class="$style.title">标题</p>
</div>
</template>
<style module>
.container {
padding: 20px;
}
.title {
color: red;
}
</style>
十六、过渡动画
1. 过渡基础
html
<template>
<transition name="fade">
<p v-if="show">Hello</p>
</transition>
</template>
<style>
.fade-enter-active, .fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>
2. 过渡类名
| 类名 | 说明 |
|---|---|
v-enter |
进入过渡的开始状态 |
v-enter-active |
进入过渡的生效状态 |
v-enter-to |
进入过渡的结束状态 |
v-leave |
离开过渡的开始状态 |
v-leave-active |
离开过渡的生效状态 |
v-leave-to |
离开过渡的结束状态 |
3. 使用第三方动画库
html
<transition
enter-active-class="animate__animated animate__fadeIn"
leave-active-class="animate__animated animate__fadeOut"
>
<p v-if="show">Hello</p>
</transition>
十七、实战应用
1. 权限控制系统
路由级权限
javascript
// router/index.js
const routes = [
{
path: '/admin',
component: Admin,
meta: { requiresAuth: true, roles: ['admin'] }
}
]
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.meta.requiresAuth && !token) {
next('/login')
return
}
if (to.meta.roles) {
const userRoles = store.getters.userRoles
const hasPermission = to.meta.roles.some(role => userRoles.includes(role))
if (!hasPermission) {
next('/403')
return
}
}
next()
})
按钮级权限
javascript
// 自定义指令
Vue.directive('permission', {
bind(el, binding) {
const permissions = store.getters.permissions
if (!permissions.includes(binding.value)) {
el.parentNode?.removeChild(el)
}
}
})
// 使用
<button v-permission="'user:delete'">删除用户</button>
2. 国际化(i18n)
javascript
// main.js
import Vue from 'vue'
import VueI18n from 'vue-i18n'
Vue.use(VueI18n)
const i18n = new VueI18n({
locale: 'zh-CN',
messages: {
'zh-CN': {
welcome: '欢迎',
hello: '你好,{name}'
},
'en-US': {
welcome: 'Welcome',
hello: 'Hello, {name}'
}
}
})
new Vue({ i18n }).$mount('#app')
html
<!-- 使用 -->
<template>
<div>
{{ $t('welcome') }}
{{ $t('hello', { name: 'John' }) }}
</div>
</template>
3. 全局错误处理
javascript
// main.js
Vue.config.errorHandler = (err, vm, info) => {
console.error('Vue 错误:', err)
console.error('组件:', vm)
console.error('错误信息:', info)
// 上报错误到监控系统
reportError({
message: err.message,
stack: err.stack,
component: vm.$options.name,
info
})
}
4. 跨域请求处理
开发环境:Vue CLI 代理
javascript
// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
}
生产环境:Nginx 配置
nginx
server {
location /api/ {
proxy_pass http://backend-server:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
5. SEO 优化
服务端渲染(SSR)
javascript
// 使用 Nuxt.js 实现 SSR
// nuxt.config.js
export default {
target: 'server',
head: {
title: 'My App',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: 'My Vue App' }
]
}
}
预渲染
javascript
// 使用 prerender-spa-plugin
const PrerenderSPAPlugin = require('prerender-spa-plugin')
module.exports = {
configureWebpack: {
plugins: [
new PrerenderSPAPlugin({
staticDir: path.join(__dirname, 'dist'),
routes: ['/', '/about', '/contact']
})
]
}
}
6. SPA 的优缺点
优点
- 用户体验好(页面无刷新)
- 前后端分离,开发效率高
- 减轻服务器压力
缺点
- 首屏加载慢
- SEO 不友好
- 前进/后退需要额外处理
- 浏览器兼容性有限
7. 异步请求处理
html
<template>
<div>
<div v-if="loading">加载中...</div>
<div v-else-if="error">{{ error }}</div>
<div v-else>{{ data }}</div>
</div>
</template>
<script>
export default {
data() {
return {
data: null,
loading: false,
error: null
}
},
async created() {
try {
this.loading = true
this.data = await this.fetchData()
} catch (err) {
this.error = err.message
} finally {
this.loading = false
}
}
}
</script>
十八、Vue API 参考
1. 全局 API
| API | 说明 | 示例 |
|---|---|---|
Vue.extend |
创建子类构造器 | const Profile = Vue.extend({...}) |
Vue.set |
添加响应式属性 | Vue.set(obj, 'newProp', value) |
Vue.delete |
删除响应式属性 | Vue.delete(obj, 'prop') |
Vue.directive |
注册指令 | Vue.directive('focus', {...}) |
Vue.filter |
注册过滤器 | Vue.filter('format', fn) |
Vue.component |
注册组件 | Vue.component('my-comp', {...}) |
Vue.use |
安装插件 | Vue.use(VueRouter) |
Vue.mixin |
全局混入 | Vue.mixin({...}) |
Vue.compile |
编译模板 | const res = Vue.compile(template) |
Vue.nextTick |
延迟回调 | Vue.nextTick(cb) |
Vue.observable |
创建响应式对象 | const state = Vue.observable({}) |
2. Vue.config 配置
javascript
Vue.config.silent = true // 取消所有日志和警告
Vue.config.devtools = true // 启用 devtools
Vue.config.errorHandler = fn // 全局错误处理
Vue.config.warnHandler = fn // 自定义警告处理
Vue.config.productionTip = false // 取消生产环境提示
Vue.config.performance = true // 启用性能追踪
3. 实例属性
| 属性 | 说明 |
|---|---|
vm.$data |
响应式数据对象 |
vm.$props |
当前组件接收到的 props |
vm.$el |
根 DOM 元素 |
vm.$options |
初始化选项 |
vm.$parent |
父实例 |
vm.$root |
根实例 |
vm.$children |
子实例数组 |
vm.$refs |
注册过 ref 的所有元素和组件 |
vm.$attrs |
未被 prop 识别的属性 |
vm.$listeners |
监听器对象 |
vm.$slots |
插槽内容 |
vm.$scopedSlots |
作用域插槽 |
vm.$route |
当前路由信息 |
vm.$router |
路由实例 |
4. 实例方法
| 方法 | 说明 |
|---|---|
vm.$watch |
监听数据变化 |
vm.$set |
添加响应式属性 |
vm.$delete |
删除响应式属性 |
vm.$on |
监听事件 |
vm.$once |
监听一次事件 |
vm.$off |
移除事件监听 |
vm.$emit |
触发事件 |
vm.$mount |
手动挂载 |
vm.$forceUpdate |
强制重新渲染 |
vm.$nextTick |
DOM 更新后回调 |
vm.$destroy |
销毁实例 |
十九、Vue CLI 与项目构建
1. Vue CLI 创建项目
bash
# 安装 Vue CLI
npm install -g @vue/cli
# 创建项目
vue create my-project
# 选择预设
> Default ([Vue 2] babel, eslint)
Default ([Vue 3] babel, eslint)
Manually select features
# 手动选择特性
(*) Babel
(*) TypeScript
(*) Progressive Web App (PWA) Support
(*) Router
(*) Vuex
(*) CSS Pre-processors
(*) Linter / Formatter
(*) Unit Testing
(*) E2E Testing
2. Vue 项目结构
csharp
my-project/
├── public/
│ ├── favicon.ico
│ └── index.html
├── src/
│ ├── assets/ # 静态资源
│ ├── components/ # 公共组件
│ ├── views/ # 页面组件
│ ├── router/ # 路由配置
│ ├── store/ # Vuex 状态管理
│ ├── utils/ # 工具函数
│ ├── api/ # API 接口
│ ├── styles/ # 全局样式
│ ├── App.vue # 根组件
│ └── main.js # 入口文件
├── tests/ # 测试文件
├── .env.development # 开发环境变量
├── .env.production # 生产环境变量
├── vue.config.js # Vue CLI 配置
├── package.json
└── README.md
3. 环境变量
bash
# .env.development
VUE_APP_API_URL=http://localhost:3000/api
VUE_APP_TITLE=开发环境
# .env.production
VUE_APP_API_URL=https://api.example.com
VUE_APP_TITLE=生产环境
javascript
// 使用
console.log(process.env.VUE_APP_API_URL)
4. 打包优化
javascript
// vue.config.js
module.exports = {
// 关闭生产环境 Source Map
productionSourceMap: false,
configureWebpack: {
// 代码分割
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
}
}
}
}
},
// 开启 gzip 压缩
chainWebpack: config => {
config.plugin('compression-webpack-plugin')
.use(require('compression-webpack-plugin'), [{
algorithm: 'gzip',
test: /\.js$|\.css$|\.html$/,
threshold: 10240,
minRatio: 0.8
}])
}
}
二十、常见问题补充
1. 如何在 Vue 中操作 DOM?
html
<template>
<div ref="myElement">Hello</div>
</template>
<script>
export default {
mounted() {
// 使用 ref 操作 DOM
this.$refs.myElement.style.color = 'red'
// 原生 API
this.$refs.myElement.addEventListener('click', this.handleClick)
},
beforeDestroy() {
// 清理事件监听
this.$refs.myElement.removeEventListener('click', this.handleClick)
},
methods: {
handleClick() {
console.log('clicked')
}
}
}
</script>
2. Vue 中如何避免事件冒泡?
html
<!-- 使用 .stop 修饰符 -->
<div @click="parentHandler">
<button @click.stop="childHandler">点击</button>
</div>
<!-- 或使用原生方式 -->
<button @click="childHandler($event)">
<script>
childHandler(e) {
e.stopPropagation()
}
</script>
</button>
3. 如何以编程方式导航?
javascript
// 方式一:路径字符串
this.$router.push('/home')
// 方式二:命名路由
this.$router.push({ name: 'home' })
// 方式三:带参数
this.$router.push({ name: 'user', params: { id: '123' } })
// 方式四:带查询参数
this.$router.push({ path: '/search', query: { keyword: 'vue' } })
// 替换当前记录(不留历史)
this.$router.replace('/home')
// 前进/后退
this.$router.go(-1)
this.$router.back()
this.$router.forward()
4. 如何确定组件的加载状态?
html
<template>
<div>
<div v-if="isLoading">加载中...</div>
<div v-else-if="isLoaded">加载完成</div>
<div v-else>准备加载</div>
</div>
</template>
<script>
export default {
data() {
return {
isLoading: false,
isLoaded: false
}
},
async mounted() {
this.isLoading = true
try {
await this.fetchData()
this.isLoaded = true
} finally {
this.isLoading = false
}
}
}
</script>
5. Vue 项目的热重载原理
热重载(Hot Module Replacement, HMR)通过以下方式实现:
- Webpack Dev Server 建立 WebSocket 连接
- 文件变更时,Webpack 重新编译模块
- 通过 WebSocket 通知浏览器
- 浏览器接收新模块代码,替换旧模块
- Vue 组件通过
module.hotAPI 更新
javascript
// Vue Loader 中的 HMR 处理
if (module.hot) {
const api = require('vue-hot-reload-api')
api.compatible = true
module.hot.accept()
if (!module.hot.data) {
api.createRecord(id, component)
} else {
api.reload(id, component)
}
}
6. Vue 中如何捕获全局错误?
javascript
// 全局错误处理
Vue.config.errorHandler = (err, vm, info) => {
console.error('Vue Error:', err)
console.error('Component:', vm?.$options?.name)
console.error('Info:', info)
// 上报错误
if (process.env.NODE_ENV === 'production') {
reportToSentry({ error: err, component: vm?.$options?.name })
}
}
// 全局 Promise 错误
window.addEventListener('unhandledrejection', event => {
console.error('Unhandled Promise Rejection:', event.reason)
})
7. Vue 中如何使用外部库(如 jQuery)?
javascript
// 方式一:直接引入(不推荐)
import $ from 'jquery'
// 方式二:在 mounted 中初始化
export default {
mounted() {
$(this.$refs.el).somePlugin()
}
}
// 方式三:封装为指令
Vue.directive('jquery-plugin', {
inserted(el, binding) {
$(el)[binding.value]()
},
unbind(el) {
$(el).pluginDestroy()
}
})
8. 动态绑定 Class 与 Style
html
<template>
<div>
<!-- 对象语法 -->
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
<!-- 数组语法 -->
<div :class="[activeClass, errorClass]"></div>
<!-- 对象数组语法 -->
<div :class="[{ active: isActive }, errorClass]"></div>
<!-- 动态 Style -->
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<!-- Style 对象 -->
<div :style="styleObject"></div>
</div>
</template>
<script>
export default {
data() {
return {
isActive: true,
hasError: false,
activeClass: 'active',
errorClass: 'text-danger',
activeColor: 'red',
fontSize: 16,
styleObject: {
color: 'red',
fontSize: '13px'
}
}
}
}
</script>
9. Vue 中如何合并对象?
javascript
export default {
methods: {
mergeObjects() {
// 方式一:Object.assign
const merged = Object.assign({}, this.defaults, this.options)
// 方式二:展开运算符
const merged2 = { ...this.defaults, ...this.options }
// 方式三:深拷贝合并(使用 lodash)
const merged3 = _.merge({}, this.defaults, this.options)
}
}
}
10. 在 Vue 中使用 watch 侦听多个数据源
javascript
// 方式一:分别侦听
watch: {
firstName() { this.updateFullName() },
lastName() { this.updateFullName() }
}
// 方式二:computed 中转
computed: {
fullName() {
return `${this.firstName} ${this.lastName}`
}
},
watch: {
fullName(newVal) {
console.log('Full name changed:', newVal)
}
}
// 方式三:组合值侦听
computed: {
watchSource() {
return `${this.firstName}-${this.lastName}`
}
},
watch: {
watchSource(newVal) {
const [first, last] = newVal.split('-')
// 处理
}
}
11. 如何实现组件的单例模式?
javascript
// 方式一:全局注册
import SingletonComponent from './Singleton.vue'
const SingletonPlugin = {
install(Vue) {
const instance = Vue.extend(SingletonComponent)
const singleton = new instance()
singleton.$mount()
document.body.appendChild(singleton.$el)
// 通过全局方法调用
Vue.prototype.$showSingleton = () => {
singleton.show()
}
}
}
Vue.use(SingletonPlugin)
12. 如何实现组件销毁时的资源清理?
html
<script>
export default {
data() {
return {
timer: null,
eventHandlers: []
}
},
mounted() {
// 定时器
this.timer = setInterval(() => {
console.log('tick')
}, 1000)
// 事件监听
const handler = () => { console.log('resize') }
window.addEventListener('resize', handler)
this.eventHandlers.push({ el: window, event: 'resize', handler })
},
beforeDestroy() {
// 清理定时器
if (this.timer) {
clearInterval(this.timer)
}
// 清理事件监听
this.eventHandlers.forEach(({ el, event, handler }) => {
el.removeEventListener(event, handler)
})
}
}
</script>
13. 如何封装可复用组件?
html
<!-- 通用按钮组件 Button.vue -->
<template>
<button
:class="['btn', `btn-${type}`, `btn-${size}`, { 'btn-block': block }]"
:disabled="disabled"
@click="$emit('click', $event)"
>
<slot></slot>
</button>
</template>
<script>
export default {
name: 'AppButton',
props: {
type: {
type: String,
default: 'default',
validator: v => ['default', 'primary', 'success', 'danger'].includes(v)
},
size: {
type: String,
default: 'medium',
validator: v => ['small', 'medium', 'large'].includes(v)
},
disabled: Boolean,
block: Boolean
}
}
</script>
14. 如何编写单元测试?
javascript
// tests/unit/HelloWorld.spec.js
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
it('renders props.msg when passed', () => {
const msg = 'new message'
const wrapper = shallowMount(HelloWorld, {
propsData: { msg }
})
expect(wrapper.text()).toMatch(msg)
})
it('emits click event when button clicked', async () => {
const wrapper = shallowMount(HelloWorld)
await wrapper.find('button').trigger('click')
expect(wrapper.emitted('click')).toBeTruthy()
})
})
二十一、Vue.observable
定义
Vue.observable 创建一个响应式对象,可用于简单的跨组件状态共享。
javascript
// state.js
import Vue from 'vue'
export const state = Vue.observable({
count: 0,
user: null
})
export const mutations = {
increment() {
state.count++
},
setUser(user) {
state.user = user
}
}
// 组件中使用
import { state, mutations } from './state'
export default {
computed: {
count() {
return state.count
}
},
methods: {
increment() {
mutations.increment()
}
}
}
二十二、常见面试问题速查表
1. 生命周期速查
| 钩子 | 时机 | 可访问 | 常用场景 |
|---|---|---|---|
| beforeCreate | 实例初始化后 | 无 | - |
| created | 实例创建后 | data/methods | API 请求 |
| beforeMount | 挂载前 | 模板 | 最后修改数据 |
| mounted | 挂载后 | DOM | DOM 操作 |
| beforeUpdate | 更新前 | 新旧数据 | 更新前处理 |
| updated | 更新后 | 新 DOM | DOM 更新后处理 |
| beforeDestroy | 销毁前 | 完整实例 | 清理资源 |
| destroyed | 销毁后 | - | 最终清理 |
2. 组件通信速查
| 方式 | 关系 | 方向 | 说明 |
|---|---|---|---|
| props | 父子 | 父→子 | 常用 |
| $emit | 父子 | 子→父 | 常用 |
| $refs | 父子 | 父→子 | 直接访问实例 |
| $parent | 父子 | 子→父 | 直接访问父实例 |
| provide/inject | 祖先后代 | 祖先→后代 | 跨层级 |
| <math xmlns="http://www.w3.org/1998/Math/MathML"> a t t r s / attrs/ </math>attrs/listeners | 隔代 | 双向 | 属性透传 |
| Event Bus | 任意 | 双向 | 事件总线 |
| Vuex | 任意 | 单向 | 状态管理 |
3. 路由守卫速查
| 守卫 | 作用范围 | 触发时机 |
|---|---|---|
| beforeEach | 全局 | 每次导航前 |
| beforeResolve | 全局 | 导航解析完成前 |
| afterEach | 全局 | 导航完成后 |
| beforeEnter | 路由 | 进入路由前 |
| beforeRouteEnter | 组件 | 进入组件前 |
| beforeRouteUpdate | 组件 | 路由更新时 |
| beforeRouteLeave | 组件 | 离开组件前 |
二十三、实战项目难点示例
题目:描述一个 Vue 项目开发中遇到的技术难点及解决方案
示例答案:
难点一:大型表单性能问题
- 问题:一个页面有上百个表单项,数据变化时页面明显卡顿
- 原因:每个表单项都是响应式的,数据变化触发大量组件重新渲染
- 解决方案 :
- 使用
Object.freeze()冻结不需要响应式的静态数据 - 将大表单拆分为多个子组件,缩小更新范围
- 使用防抖处理输入事件
- 长列表使用虚拟滚动
- 使用
难点二:权限控制复杂
- 问题:不同角色看到不同的页面、菜单和按钮
- 解决方案 :
- 路由级:使用路由守卫 + meta.roles 控制页面访问
- 菜单级:根据用户角色动态生成菜单
- 按钮级:使用自定义指令
v-permission控制按钮显示 - 数据级:后端接口返回用户权限列表
难点三:首屏加载慢
- 问题:首屏加载时间超过 5 秒
- 解决方案 :
- 路由懒加载:
() => import('./views/Home.vue') - 组件按需引入:只引入使用的 Element UI 组件
- CDN 外部化:将 Vue、VueRouter 等通过 CDN 引入
- 图片压缩和懒加载
- 开启 gzip 压缩
- 最终首屏加载时间降低到 1.5 秒
- 路由懒加载: