一、Vue基础概念
1.1 MVVM模式
MVVM 是 Model-View-ViewModel 缩写,也就是把 MVC 中的 Controller 演变成 ViewModel。Model 层代表数据模型,View 代表 UI 组件,ViewModel 是 View 和 Model 层的桥梁,数据会绑定到 viewModel 层并自动将数据渲染到页面中,视图变化的时候会通知 viewModel 层更新数据。
| 组成部分 | 说明 |
|---|---|
| Model | 数据模型层,存放业务数据 |
| View | 视图层,负责UI展示 |
| ViewModel | 视图模型层,作为View和Model的桥梁 |
核心特点:
- 数据驱动视图:数据变化自动更新视图
- 双向绑定:视图变化自动同步到数据
- 低耦合:各层职责清晰,易于维护
1.2 单向数据流
父级 prop 的更新会向下流动到子组件,但反向不行。
优点:
- 防止子组件意外修改父组件状态
- 数据流向清晰,易于追踪
二、Vue核心机制
2.1 生命周期
每个 Vue 实例在创建时都会经过一系列的初始化过程,Vue 的生命周期钩子,就是说在达到某一阶段或条件时去触发的函数,目的就是为了完成一些动作或者事件
Vue实例从创建到销毁的完整过程,包含多个钩子函数:
- create阶段:
Vue实例被创建beforeCreate:创建前,此时data和methods中的数据都还没有初始化created:创建完毕,data中有值,未挂载
- mount阶段:
Vue实例被挂载到真实DOM节点beforeMount:可以发起服务端请求,获取数据mounted:此时可以操作DOM,也可以操作data中的数据
- update阶段:当
Vue实例里面的data数据变化时,触发组件的重新渲染beforeUpdate:组件重新渲染前,此时可以操作data中的数据updated:组件重新渲染完成
- destroy/unmount阶段:
Vue实例被销毁- Vue2:
beforeDestroy/destroyed - Vue3:
beforeUnmount/unmounted - 作用:实例被销毁/卸载前和销毁/卸载后,可以执行清理工作(如清除定时器、取消订阅等)
- Vue2:
2.2 父子组件生命周期顺序
1. 挂载阶段
该过程主要涉及 beforeCreate、created、beforeMount、mounted 4 个钩子函数。执行顺序为:
父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted
一定得等子组件挂载完毕后,父组件才能挂载完毕,所以父组件的 mounted 在最后。
2. 更新阶段
该过程主要涉及 beforeUpdate、updated 2 个钩子函数。注意,当父子组件有数据传递时,才有这个更新阶段执行顺序的比较。执行顺序为:
父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated
3. 销毁阶段
该过程主要涉及 beforeDestroy、destroyed 2 个钩子函数。执行顺序为:
父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed
4. 总结
Vue 父子组件生命周期钩子的执行顺序遵循:从外到内创建,从内到外销毁。
2.3 new Vue初始化流程
2.3.1 Vue2初始化流程
initProxy → initLifecycle → initEvents → initRender → initInjections → initState → initProvide → vm.$mount(vm.options.el)
关键步骤:
| 步骤 | 说明 |
|---|---|
initProxy |
开发环境代理,拦截未定义属性访问并给出警告提示 |
initLifecycle |
建立父子组件关系,在当前组件实例上添加一些属性和生命周期标识。如 $parent, $refs, $children, _isMounted 等 |
initEvents |
初始化事件监听系统,处理父组件通过 $emit 触发的自定义事件监听 |
initRender |
声明 $slots 和 createElement() 等渲染相关的方法 |
initInjections |
注入数据,初始化 inject,一般用于组件更深层次之间的通信 |
initState |
(重要)数据响应式:初始化状态。很多选项初始化的汇总:data、methods、props、computed 和 watch |
initProvide |
提供数据,初始化 provide,一般用于组件更深层次之间的通信 |
vm.$mount(vm.options.el) |
挂载实例到DOM |
2.3.2 Vue3初始化流程
createApp → createComponent → setup() → initProps → initSlots → setupRenderEffect → mount → mounted
关键步骤:
| 步骤 | 说明 |
|---|---|
createApp |
创建应用实例,包含 app 对象及其配置 |
createComponent |
创建根组件实例 |
setup() |
Composition API 入口,执行 setup 选项或 <script setup> |
initProps |
初始化 props,将父组件传递的 props 转换为响应式 |
initSlots |
初始化插槽,处理 <slot> 和作用域插槽 |
setupRenderEffect |
设置渲染副作用,建立响应式系统和组件更新机制 |
mount |
将虚拟 DOM 挂载到真实 DOM |
mounted |
挂载完成,触发 onMounted 钩子 |
为什么先inject后provide(初始化顺序)?
注入(inject)先于状态(state)初始化:
initInjections在initState之前执行,这样注入的数据可以在data、computed、methods等中被使用- 例如:可以在
data()中使用this.injectedValue来初始化响应式数据
提供(provide)在状态初始化之后:
initProvide在initState之后执行,因为provide通常需要访问组件实例上的响应式数据- 例如:
provide: { foo: () => this.count },这里的this.count需要在initState之后才能访问
父子组件的初始化顺序:
- 父组件先执行
initInjections注入祖辈提供的数据 - 然后父组件执行
initState初始化自己的状态 - 接着子组件开始初始化,执行自己的
initInjections和initState - 子组件完成后,父组件才执行
initProvide - 这样设计的目的是确保子组件的
inject能获取到父组件provide的数据,同时避免循环依赖
- 父组件先执行
三、Vue响应式原理
3.1 Vue2双向绑定原理
当一个 Vue 实例创建时,Vue 会遍历 data 选项的属性,用 Object.defineProperty 将它们转为 getter/setter 并且在内部追踪相关依赖,在属性被访问和修改时通知变化。每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。
实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器 Observer ,用来监听所有属性。如果属性发生变化了,就需要告诉订阅者 Watcher 看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器 Dep 来专门收集这些订阅者,然后在监听器 Observer 和订阅者 Watcher 之间进行统一管理的。
使用 Object.defineProperty 实现数据劫持:
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
// 依赖收集
Dep.target && dep.addSub(Dep.target);
return val;
},
set(newVal) {
if (newVal !== val) {
val = newVal;
// 通知更新
dep.notify();
}
},
});
}
核心三要素:
- Observer:监听数据变化
- Dep:依赖收集器
- Watcher:订阅者,接收更新通知
3.2 Vue3响应式原理
Vue 3 使用 Proxy 代替 Object.defineProperty 来实现响应式系统:
const proxy = new Proxy(obj, {
get(target, key) {
// 依赖收集
track(target, key);
return target[key];
},
set(target, key, value) {
target[key] = value;
// 触发更新
trigger(target, key);
return true;
},
});
3.3 Proxy vs Object.defineProperty
Vue2 使用 Object.defineProperty 实现响应式,Vue3 使用 Proxy 重写了响应式系统。两者的核心差异如下:
3.3.1 核心特性对比
| 特性 | Object.defineProperty | Proxy |
|---|---|---|
| 监听范围 | 单个属性,需要遍历对象 | 整个对象,无需遍历 |
| 数组支持 | 不支持数组API(push、pop等) |
原生支持数组变化 |
| 新增属性 | 不支持,需手动调用 Vue.set |
自动支持新增属性 |
| 删除属性 | 不支持,需手动调用 Vue.delete |
自动支持删除属性 |
| 性能 | 较差(递归遍历+逐个劫持) | 更好(代理一次即可) |
| 兼容性 | IE9+ | IE不支持 |
| 返回值 | 无返回值,直接修改原对象 | 返回代理对象,不修改原对象 |
| 嵌套对象 | 需要递归劫持,性能开销大 | 惰性代理,访问时才劫持 |
总结:
- Object.defineProperty 的作用是劫持一个对象的属性,劫持属性的
getter和setter方法,在对象的属性发生变化时进行特定的操作。而Proxy劫持的是整个对象。 - Proxy 会返回一个代理对象,我们只需要操作新对象即可,而
Object.defineProperty只能遍历对象属性直接修改。 - Object.defineProperty 不支持数组,更准确的说是不支持数组的各种 API,因为如果仅仅考虑
arr[i] = value这种情况,是可以劫持的,但是这种劫持意义不大。而Proxy可以支持数组的各种 API。
3.3.2 实现原理差异
Object.defineProperty 的局限性:
// Vue2响应式实现
function defineReactive(obj, key, val) {
// 递归处理嵌套对象
if (typeof val === "object") {
observe(val);
}
Object.defineProperty(obj, key, {
get() {
// 依赖收集
Dep.target && dep.addSub(Dep.target);
return val;
},
set(newVal) {
if (newVal !== val) {
// 新值是对象时需要重新劫持
if (typeof newVal === "object") {
observe(newVal);
}
val = newVal;
dep.notify();
}
},
});
}
问题:
- 初始化性能差:需要递归遍历所有属性
- 新增属性无法监听:
obj.newKey = value无法触发响应 - 数组API无法监听:
arr.push()、arr.pop()等操作不会触发setter - 嵌套对象重复劫持:每次赋值新对象都需要重新递归
Proxy 的优势:
// Vue3响应式实现
function reactive(target) {
const handler = {
get(target, key, receiver) {
// 依赖收集
track(target, key);
// 惰性代理:访问时才处理嵌套对象
const result = Reflect.get(target, key, receiver);
if (isObject(result)) {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
// 仅在值真正改变时触发更新
if (hasChanged(value, oldValue)) {
trigger(target, key, value, oldValue);
}
return result;
},
deleteProperty(target, key) {
const hadKey = hasOwn(target, key);
const result = Reflect.deleteProperty(target, key);
if (hadKey) {
trigger(target, key, undefined);
}
return result;
},
};
return new Proxy(target, handler);
}
优势:
- 性能更好:无需递归遍历,惰性代理按需处理
- 自动监听新增/删除:直接拦截对象操作
- 原生支持数组:所有数组API都能被拦截
- 返回代理对象:不修改原对象,更安全
3.3.3 数组处理特殊方案
由于 Object.defineProperty 无法监听数组API,Vue2采用了重写数组方法的方案:
// Vue2重写数组方法
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
// 重写7个会改变数组的方法
["push", "pop", "shift", "unshift", "splice", "sort", "reverse"].forEach(method => {
arrayMethods[method] = function (...args) {
const result = arrayProto[method].apply(this, args);
// 手动触发更新
dep.notify();
return result;
};
});
局限性:
- 仅支持7种方法,直接修改数组索引(
arr[0] = value)仍无法监听 - 需要额外维护数组方法的重写逻辑
3.3.4 为什么Vue3 选择Proxy
| 原因 | 说明 |
|---|---|
| 性能提升 | 惰性代理避免了初始化时的递归开销 |
| 功能完善 | 原生支持新增/删除属性、数组操作 |
| 代码简洁 | 无需特殊处理数组,逻辑更统一 |
| 未来兼容 | Proxy是ES6标准,代表未来发展方向 |
3.3.5 兼容性权衡
| 场景 | 推荐方案 |
|---|---|
| 现代浏览器 | 使用Proxy(Vue3默认) |
| 需要IE支持 | 使用Object.defineProperty(Vue2) |
| 混合场景 | 提供polyfill或降级方案 |
总结:
Proxy在功能和性能上全面优于Object.defineProperty,是响应式系统的更优选择。Vue3选择Proxy是技术演进的必然结果。
3.4 v-model原理
v-model是语法糖,用于在表单元素和组件上创建双向数据绑定。其本质是属性绑定和事件监听的组合。
3.4.1 核心原理
| 版本 | 绑定方式 | 组件实现 | 编译后原理 |
|---|---|---|---|
| Vue2 | :value + @input |
props: ['value'] + this.$emit('input', val) |
:value="msg" @input="msg = $event.target.value" |
| Vue3 | :modelValue + @update:modelValue |
defineProps(['modelValue']) + emit('update:modelValue', val) |
:modelValue="msg" @update:modelValue="msg = $event" |
3.4.2 Vue2中的v-model
表单元素使用:
<!-- 输入框 -->
<input v-model="message" />
<!-- 等价于 -->
<input :value="message" @input="message = $event.target.value" />
<!-- 复选框 -->
<input type="checkbox" v-model="checked" />
<!-- 等价于 -->
<input type="checkbox" :checked="checked" @change="checked = $event.target.checked" />
自定义组件实现:
3.4.3 Vue3中的v-model
表单元素使用:
<!-- 输入框 -->
<input v-model="message" />
<!-- 等价于 -->
<input :modelValue="message" @update:modelValue="message = $event" />
自定义组件实现:
3.4.4 Vue3多v-model支持
Vue3允许在同一个组件上使用多个v-model:
<!-- 父组件 -->
<ChildComponent v-model:title="title" v-model:content="content" />
<!-- 等价于 -->
<ChildComponent
:title="title"
@update:title="title = $event"
:content="content"
@update:content="content = $event"
/>
子组件实现:
3.4.5 Vue3自定义修饰符
Vue3支持自定义v-model修饰符:
<!-- 使用自定义修饰符 -->
<ChildComponent v-model.capitalize="message" />
子组件实现:
内置修饰符:
.trim:去除首尾空格.number:转换为数字.lazy:失去焦点时触发更新
3.4.6 v-model的本质
v-model本质上是Vue编译器提供的语法糖,它在编译阶段被转换为属性绑定和事件监听的组合。
3.4.6.1 编译过程解析
Vue编译器对v-model的处理分为两个阶段:
1. 模板解析阶段:识别v-model指令并提取绑定的表达式
2. 代码生成阶段:将v-model转换为对应的属性绑定和事件监听
3.4.6.2 编译转换示例
输入模板:
<input v-model="message" />
Vue2编译输出:
// 编译后的渲染函数
with (this) {
return _c("input", {
directives: [{ name: "model", rawName: "v-model", value: message, expression: "message" }],
domProps: { value: message },
on: {
input: function ($event) {
message = $event.target.value;
},
},
});
}
Vue3编译输出:
// 编译后的渲染函数
function render(_ctx, _cache) {
return (
_openBlock(),
_createElementBlock(
"input",
{
modelValue: _ctx.message,
"onUpdate:modelValue": _cache[1] || (_cache[1] = $event => (_ctx.message = $event)),
},
null,
8 /* PROPS */,
["modelValue", "onUpdate:modelValue"]
)
);
}
3.4.6.3 双向绑定的实现机制
┌─────────────────────────────────────────────────────────────┐
│ v-model双向绑定流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 数据更新 (data → view) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ message = "Hello" │ │
│ │ ↓ │ │
│ │ Vue响应式系统检测到变化 │ │
│ │ ↓ │ │
│ │ 触发视图更新 │ │
│ │ ↓ │ │
│ │ input.value = "Hello" ← 属性绑定 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 用户输入 (view → data) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 用户在输入框中输入 "World" │ │
│ │ ↓ │ │
│ │ 触发 input 事件 │ │
│ │ ↓ │ │
│ │ 事件处理函数执行: message = $event.target.value │ │
│ │ ↓ │ │
│ │ Vue响应式系统检测到变化 │ │
│ │ ↓ │ │
│ │ 触发依赖的视图更新 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘3.4.6.4 v-model vs 手动实现对比
| 对比维度 | 使用v-model | 手动实现 |
|---|---|---|
| 代码量 | 简洁,一行搞定 | 繁琐,需要绑定属性和事件 |
| 可读性 | 语义清晰,意图明确 | 需要理解事件处理逻辑 |
| 维护性 | 易于维护和修改 | 修改需要同时改属性和事件 |
| 一致性 | 统一的语法风格 | 容易出现不一致的实现 |
手动实现示例:
<!-- 手动实现双向绑定 -->
<input :value="message" @input="message = $event.target.value" />
<!-- 使用v-model -->
<input v-model="message" />
3.4.6.5 为什么需要v-model
- 简化代码:将复杂的双向绑定逻辑简化为一个指令
- 统一接口:为表单元素和自定义组件提供一致的绑定方式
- 减少错误:避免手动编写事件处理时出现的错误
- 框架优化:Vue可以针对v-model进行特定的性能优化
- 扩展性强:支持自定义组件和自定义修饰符
3.4.6.6 v-model的设计理念
关注点分离:
- 声明式编程:开发者只需要声明”数据应该绑定到这个输入框”
- 底层细节隐藏:Vue负责处理事件监听、值提取、响应式更新等细节
- 可组合性:可以与其他指令(如
v-if、v-show)组合使用
响应式核心:
- v-model依赖Vue的响应式系统
- 数据变化自动触发视图更新
- 视图变化自动同步回数据
总结:v-model是Vue响应式系统和模板语法的完美结合,它让双向数据绑定变得简单、直观、可靠。
3.5 nextTick的实现原理
nextTick 是 Vue 提供的异步执行机制,用于在 DOM 更新完成后执行回调函数。
3.5.1 核心原理
| 特性 | 说明 |
|---|---|
| 作用 | 在下次 DOM 更新循环结束后执行延迟回调 |
| 本质 | 利用 JavaScript 的异步任务队列机制 |
| 异步优先级 | Promise.then(微任务)> MutationObserver(微任务)> setTimeout(宏任务) |
| 适用场景 | 获取更新后的 DOM、执行依赖 DOM 的操作、等待批量更新完成 |
3.5.2 为什么需要 nextTick
Vue 的 DOM 更新是异步的,这意味着数据变化后不会立即更新 DOM:
- 批量更新优化:同一事件循环中的多次数据变更只会触发一次
DOM更新 - 性能优化:避免频繁的
DOM操作,减少重排重绘 - 一致性保证:确保所有数据变更都处理完毕后再更新
DOM
3.5.3 使用场景
| 场景 | 说明 |
|---|---|
| 获取更新后的 DOM | 修改数据后立即读取 DOM 属性(如 offsetHeight) |
| 滚动定位 | 数据更新后滚动到特定位置 |
| 第三方插件初始化 | DOM 更新后重新初始化依赖 DOM 的插件 |
| 生命周期钩子 | created 钩子中操作 DOM(此时 DOM 尚未渲染) |
使用场景详细描述:
- 当你修改数据后,需要立即获取更新后的
DOM进行操作(如滚动定位、测量尺寸) - 当你需要在数据更新后执行一些依赖
DOM的逻辑 - 当你需要等待
Vue完成批量更新后再执行操作 - Vue 生命周期的
created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中,原因是在created()钩子函数执行的时候DOM其实并未进行任何渲染,而此时进行DOM操作无异于徒劳。与之对应的是mounted钩子函数,因为该钩子函数执行时所有的DOM挂载已完成。 - 当项目中你想在改变
DOM元素的数据后基于新的DOM做点什么,对新DOM一系列的 JS 操作都需要放进Vue.nextTick()的回调函数中;通俗的理解是:更改数据后当你想立即使用 JS 操作新的视图的时候需要使用它。 - 在使用某个第三方插件时 ,希望在
Vue生成的某些DOM动态发生变化时重新应用该插件,也会用到该方法,这时候就需要在$nextTick的回调函数中执行重新应用插件的方法。
使用示例:
// 场景1:获取更新后的 DOM 尺寸
this.message = "Hello";
await nextTick();
const height = this.$refs.container.offsetHeight;
// 场景2:滚动到指定位置
this.items.push(newItem);
await nextTick();
this.$refs.list.scrollTop = this.$refs.list.scrollHeight;
// 场景3:第三方插件重新初始化
this.data = newData;
await nextTick();
this.chart.update();
// 场景4:created 钩子中操作 DOM
created() {
this.$nextTick(() => {
this.$refs.header.style.color = "red";
});
}
3.5.4 Vue2 vs Vue3 使用方式
| 版本 | 全局调用 | 实例调用 | Promise支持 |
|---|---|---|---|
| Vue2 | Vue.nextTick(callback) |
this.$nextTick(callback) |
不支持 await |
| Vue3 | nextTick(callback) |
推荐全局导入 | 支持 await |
Vue2 使用示例:
// 实例调用
this.$nextTick(() => {
console.log("DOM updated");
});
// 全局调用
Vue.nextTick(() => {
console.log("DOM updated");
});
Vue3 使用示例:
import { nextTick } from "vue";
// 回调方式
nextTick(() => {
console.log("DOM updated");
});
// Promise方式(推荐)
await nextTick();
console.log("DOM updated");
3.5.5 实现原理与机制
实现原理:
- 异步队列:
Vue内部维护一个callbacks队列,用于存储需要延迟执行的回调函数 - 数据更新:当数据发生变化时,
Vue会将更新操作放入队列,而不是立即执行 - 批量处理:同一事件循环中的多次数据变更会被合并,只触发一次
DOM更新 - 异步执行:使用
timerFunc函数实现异步执行,优先级为:- 优先使用
Promise.then(微任务,执行时机早) - 降级使用
MutationObserver(微任务,兼容性更好) - 最后使用
setTimeout(宏任务,兼容性最好)
- 优先使用
- 执行回调:当
DOM更新完成后,按顺序执行callbacks队列中的所有回调函数
核心流程:
┌─────────────────────────────────────────────────────────────┐
│ nextTick 执行流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 数据变化 │
│ ┌──────────────────────┐ │
│ │ this.message = "Hi" │ │
│ └──────────┬───────────┘ │
│ ↓ │
│ 2. 触发响应式更新 │
│ ┌──────────────────────┐ │
│ │ 依赖收集系统检测变化 │ │
│ │ 触发 watcher 更新 │ │
│ └──────────┬───────────┘ │
│ ↓ │
│ 3. 异步队列机制 │
│ ┌──────────────────────┐ │
│ │ 更新操作入队 │ │
│ │ 调用 nextTick 调度 │ │
│ └──────────┬───────────┘ │
│ ↓ │
│ 4. 异步任务执行 │
│ ┌──────────────────────┐ │
│ │ Promise.then │ ← 微任务(优先) │
│ │ MutationObserver │ ← 微任务(降级) │
│ │ setTimeout(fn, 0) │ ← 宏任务(兜底) │
│ └──────────┬───────────┘ │
│ ↓ │
│ 5. DOM 更新 │
│ ┌──────────────────────┐ │
│ │ 执行 patch 过程 │ │
│ │ 更新真实 DOM │ │
│ └──────────┬───────────┘ │
│ ↓ │
│ 6. 执行回调 │
│ ┌──────────────────────┐ │
│ │ 执行 callbacks 队列 │ │
│ │ 用户回调获得最新DOM │ │
│ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘异步策略优先级:
| 优先级 | 方案 | 类型 | 优势 |
|---|---|---|---|
| 1 | Promise.then |
微任务 | 执行时机最早,性能最好 |
| 2 | MutationObserver |
微任务 | 兼容性好,可观测 DOM 变化 |
| 3 | setTimeout(fn, 0) |
宏任务 | 兼容性最好,兜底方案 |
3.5.6 核心源码逻辑
简化版实现:
// Vue3 nextTick 核心实现
let callbacks = [];
let pending = false;
function flushCallbacks() {
pending = false;
const copies = callbacks.slice(0);
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}
let timerFunc;
// 优先使用 Promise
if (typeof Promise !== "undefined") {
timerFunc = () => {
Promise.resolve().then(flushCallbacks);
};
}
// 降级使用 MutationObserver
else if (typeof MutationObserver !== "undefined") {
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode("1");
observer.observe(textNode, { characterData: true });
timerFunc = () => {
textNode.data = "2";
};
}
// 最后使用 setTimeout
else {
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
export function nextTick(fn) {
return new Promise(resolve => {
callbacks.push(() => {
if (fn) fn();
resolve();
});
if (!pending) {
pending = true;
timerFunc();
}
});
}
关键设计点:
- 队列机制:使用
callbacks数组收集所有待执行的回调 - 防抖机制:通过
pending标志确保同一事件循环只触发一次异步任务 - 优雅降级:根据浏览器环境选择最优的异步方案
- Promise 支持:Vue3 返回 Promise,支持 async/await
3.5.7 nextTick 与事件循环
在事件循环中的执行顺序:
┌─────────────────────────────────────────────────────────────┐
│ JavaScript 事件循环 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 同步任务执行 │
│ ┌───────────────────────────────────────────────────┐ │
│ │ this.message = "Hello" │ │
│ │ nextTick(callback) │ │
│ │ ↓ │ │
│ │ callback 入队 │ │
│ │ Promise.resolve().then(flushCallbacks) 入队 │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ 微任务队列执行 │
│ ┌───────────────────────────────────────────────────┐ │
│ │ flushCallbacks() 执行 │ │
│ │ ↓ │ │
│ │ Vue 执行 DOM 更新 │ │
│ │ callback() 执行 │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ 宏任务队列执行 │
│ ┌───────────────────────────────────────────────────┐ │
│ │ setTimeout、setInterval 等 │ │
│ └───────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘总结:nextTick 利用 JavaScript 的微任务机制,确保回调在 DOM 更新完成后执行,是 Vue 异步更新策略的核心组成部分。
四、Vue虚拟DOM
4.1 VNode概念
VNode(Virtual Node) 是虚拟 DOM 节点,是对真实 DOM 的抽象表示。它是一个普通的 JavaScript 对象,并且最少包含 tag、props/attr 和 children 三个属性,描述了 DOM 节点的结构、属性和子节点。
4.1.1 VNode 的核心属性
| 属性 | 类型 | 说明 |
|---|---|---|
tag |
string | Function |
标签名(如 'div')或组件构造函数 |
data |
VNodeData | undefined |
节点数据,包含属性、样式、事件等 |
children |
VNode[] | undefined |
子虚拟节点数组 |
text |
string | undefined |
文本节点内容 |
elm |
Node | undefined |
对应的真实 DOM 元素(渲染后赋值) |
key |
string | number | undefined |
节点唯一标识,用于优化 diff 算法 |
ns |
string | undefined |
XML 命名空间 |
context |
Component | undefined |
组件上下文 |
4.1.2 VNode 对象结构示例
const vnode = {
tag: "div",
data: {
class: "container",
style: { color: "red" },
on: { click: handleClick },
},
children: [{ tag: "span", text: "Hello" }],
text: null,
elm: null,
key: "unique-key",
context: componentInstance,
};
4.1.3 虚拟 DOM 的渲染原理
虚拟 DOM 的核心思想是用 JavaScript 对象来模拟 DOM 树,通过 Diff 算法 高效地找出新旧虚拟 DOM 之间的差异,然后只更新发生变化的真实 DOM 节点。
工作流程:
- 生成虚拟 DOM:将模板或渲染函数转换为
VNode树 - 首次渲染:将
VNode树转换为真实DOM树 - 数据更新:生成新的
VNode树 - Diff 比较:对比新旧
VNode树,找出差异 - 打补丁:将差异应用到真实
DOM
4.1.4 Vue 中使用虚拟 DOM 的优势(为什么需要虚拟DOM?)
| 优势 | 说明 |
|---|---|
| 性能优化 | 通过减少直接 DOM 操作(因为 DOM 操作比较消耗性能,渲染也比较慢),虚拟 DOM 能够显著提高应用的响应速度和性能。 |
| JS开发 | 通过 JS 将原有的 DOM树 转换为 JS对象树 ,对 DOM节点 的操作就变成了对 JS对象 的操作。 |
| Diff 算法 | 虚拟 DOM 通过 Diff 算法 高效地找出新旧虚拟 DOM 之间的差异,只更新发生变化的真实 DOM 节点,最小化重排重绘。 |
| 跨平台能力 | 虚拟 DOM 可以渲染到 DOM、SSR、Weex 等不同平台,实现 SSR、同构渲染这些高级特性,为应用提供了更多的部署选项。 |
| 组件抽象 | 虚拟 DOM 把渲染过程抽象化了,从而使得组件的抽象能力也得到提升,并且使组件的逻辑与渲染目标解耦,更符合组件化开发的模式。 |
| AOT 优化 | 虚拟 DOM 支持模板编译优化,不再依赖 HTML 解析器进行模版解析,可以进行更多的 AOT 工作提高运行时效率,通过模版 AOT 编译,Vue 的运行时体积可以进一步压缩,运行时效率可以进一步提升。 |
4.2 Diff 算法
在 Vue 2.x 中, JS渲染真实 DOM 的开销是非常大的, 比如我们修改了某个数据,如果直接渲染到真实 DOM,会引起整个 DOM 树的重绘和重排。那么有没有可能实现只更新我们修改的那一小块 DOM 而不要更新整个 DOM 呢?
此时我们就需要先根据真实 DOM 生成虚拟 DOM,当虚拟 DOM 某个节点的数据改变后会生成有一个新的 Vnode, 然后新的 Vnode 和旧的 Vnode 作比较,发现有不一样的地方就直接修改在真实 DOM 上,然后使旧的 Vnode 的值为新的 Vnode。
diff 的过程就是调用 patch 函数,比较新旧节点,一边比较一边给真实的 DOM 打补丁。在采取 diff 算法比较新旧节点的时候,比较只会在同层级进行。
Diff 算法是虚拟 DOM 的核心,用于高效地比较新旧 VNode 树并找出差异。Vue 2.x 采用了双端比较策略,具有 O(n) 的时间复杂度。
4.2.1 核心策略
- 同层级比较:只比较同一层级的节点,不跨层级比较
- 双端比较:从两端向中间比较,提高匹配效率
- key 优化:通过唯一 key 快速定位可复用节点
- 最小化操作:优先进行移动操作而非删除重建
4.2.2 Diff 比较流程
旧 VNode 树 ──┐
├─→ patch() ──→ 差异计算 ──→ 真实 DOM 更新
新 VNode 树 ──┘
patch 方法的树级别比较:
节点不存在处理:
new VNode不存在 → 删除old VNode对应的真实DOMold VNode不存在 → 创建新DOM并插入
节点类型判断(都存在时):
- tag 不同 → 直接替换整个节点(删除旧节点,创建新节点)
- 均为文本节点 → 直接更新文本内容
- 其他情况 → 执行深度
diff更新
patchVnode 深度比较:
- 更新节点属性、样式、事件监听器
- 处理子节点差异,调用
updateChildren
子节点处理策略:
- 新老节点均有子节点 → 调用
updateChildren进行精细化比较 - 老节点无子节点,新节点有子节点 → 清空文本后新增子节点
- 新节点无子节点,老节点有子节点 → 移除所有子节点
- 两者均无子节点 → 直接替换文本内容
- 新老节点均有子节点 → 调用
步骤详解:
树级别比较:判断是否需要替换整棵树
- 新旧节点
tag不同 → 直接替换 - 新旧节点均为文本节点 → 直接更新文本
- 其他情况 → 继续深度比较
- 新旧节点
节点属性比较:调用
patchVnode更新属性、样式、事件子节点比较:调用
updateChildren处理子节点差异
4.2.3 updateChildren 双端比较算法
updateChildren 是 Diff 算法的核心,采用双指针 + 四种比较策略:
旧节点:[A] [B] [C] [D]
↑ ↑
oldStart oldEnd
新节点:[D] [A] [B] [C]
↑ ↑
newStart newEnd
updateChildren 双端比较机制:
updateChildren 是 Vue Diff 算法的核心,其工作流程如下:
初始化指针:提取新旧节点的子节点列表
oldCh和newCh,并设置四个指针:oldStartIdx:旧子节点列表头指针oldEndIdx:旧子节点列表尾指针newStartIdx:新子节点列表头指针newEndIdx:新子节点列表尾指针
双端比较策略:按优先级依次尝试四种比较方式:
- 旧头 vs 新头
- 旧尾 vs 新尾
- 旧头 vs 新尾
- 旧尾 vs 新头
key 映射查找:若四种比较均未匹配,则通过
key建立的映射表(key → index)在oldCh中快速查找对应节点:- 找到匹配节点 → 复用该节点并移动到正确位置
- 未找到 → 创建新 DOM 节点
循环结束条件:
oldStartIdx > oldEndIdx:旧列表遍历完毕,将剩余新节点追加到 DOMnewStartIdx > newEndIdx:新列表遍历完毕,删除剩余旧节点
四种比较方式(按优先级):
| 比较方式 | 条件 | 操作 |
|---|---|---|
| 旧头 vs 新头 | oldStart.key === newStart.key |
复用节点,双指针均右移 |
| 旧尾 vs 新尾 | oldEnd.key === newEnd.key |
复用节点,双指针均左移 |
| 旧头 vs 新尾 | oldStart.key === newEnd.key |
复用节点,旧头右移,新尾左移,节点移动到尾部 |
| 旧尾 vs 新头 | oldEnd.key === newStart.key |
复用节点,旧尾左移,新头右移,节点移动到头部 |
未匹配处理:
- 如果四种比较都未匹配,使用
key建立的映射表快速查找 - 找到可复用节点 → 移动到对应位置
- 未找到 → 创建新节点
结束条件:
oldStart > oldEnd:旧节点遍历完,新增剩余新节点newStart > newEnd:新节点遍历完,删除剩余旧节点
4.2.4 key 的重要性
key 是 Diff 算法优化的关键,正确使用 key 可以:
- 避免不必要的 DOM 操作:通过 key 快速复用已有节点
- 保持组件状态:复用组件实例,保留其内部状态
- 提升列表渲染性能:减少节点创建和销毁开销
注意事项:
key必须唯一且稳定,不要使用数组索引作为 keykey应与数据绑定,避免随机值或动态计算值
4.2.5 Diff 算法示例
假设有如下列表更新:
// 旧列表
const oldList = [
{ id: "A", text: "Apple" },
{ id: "B", text: "Banana" },
{ id: "C", text: "Cherry" },
];
// 新列表(将 C 移到首位)
const newList = [
{ id: "C", text: "Cherry" },
{ id: "A", text: "Apple" },
{ id: "B", text: "Banana" },
];
Diff 过程:
- 旧头(A) vs 新头(C) → 不匹配
- 旧尾(C) vs 新尾(B) → 不匹配
- 旧头(A) vs 新尾(B) → 不匹配
- 旧尾(C) vs 新头(C) → 匹配!将 C 移动到头部
- 继续比较,依次匹配 A、B
- 完成,仅需移动操作,无节点销毁和重建
五、Vue组件
5.1 computed与watch
5.1.1 computed与watch的定义
watch 属性监听:是一个对象,键是需要观察的属性,值是对应回调函数,主要用来监听某些特定数据的变化,从而进行某些具体的业务逻辑操作,监听属性的变化,需要在数据变化时执行异步或开销较大的操作时使用
computed 计算属性:属性的结果会被缓存,当 computed 中的函数所依赖的属性没有发生改变的时候,那么调用当前函数的时候结果会从缓存中读取。除非依赖的响应式属性变化时才会重新计算,主要当做属性来使用,computed 中的函数必须用 return 返回最终的结果。
5.1.2 computed与watch的区别
| 特性 | computed | watch |
|---|---|---|
| 缓存 | 依赖不变时缓存结果 | 无缓存 |
| 执行时机 | 访问时计算 | 数据变化时执行 |
| 返回值 | 必须 return |
无需 return |
| 适用场景 | 多属性影响一个值,例:购物车商品结算功能 | 一个值影响多操作或数据变化,例:搜索数据 |
示例:
// computed - 购物车总价
computed: {
totalPrice() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
// watch - 搜索
watch: {
keyword(newVal) {
this.fetchData(newVal);
}
}
5.2 v-for中key的作用
作用:
高效更新虚拟DOM:
key的作用主要是为了高效的更新虚拟DOM,其原理是Vue在patch过程中通过key可以精准判断两个节点是否是同一个,从而避免频繁更新不同元素,使得整个patch过程更加高效,减少DOM操作量,提高性能。避免列表更新bug:若不设置
key还可能在列表更新时引发一些隐蔽的bug,例如当列表项顺序变化时,Vue可能会复用错误的元素。支持过渡效果:在使用相同标签名元素的过渡切换时,也会使用到key属性,其目的也是为了让Vue可以区分它们,否则Vue只会替换其内部属性而不会触发过渡效果。
使用建议:
- 渲染一组列表时,
key往往是唯一标识(如ID),而不是使用索引 - 如果不定义
key的话,Vue只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch过程比较低效 - 从源码中可以知道,
Vue判断两个节点是否相同时主要判断两者的key和元素类型等,因此如果不设置key,它的值就是undefined,则可能永远认为这是两个相同的节点,只能去做更新操作,这造成了大量的DOM更新操作,明显是不可取的。
5.3 keep-alive组件缓存
作用:缓存组件实例,避免重复渲染。
配置属性:
| 属性 | 说明 |
|——|——|
| include | 匹配的组件名(字符串/正则) |
| exclude | 排除的组件名 |
| max | 最大缓存数量 |
生命周期钩子:
activated:被keep-alive缓存的组件激活时调用(组件从缓存中恢复显示)deactivated:被keep-alive缓存的组件失活时调用(组件被缓存隐藏)
原理:Vue.js 内部将 DOM 节点抽象成了一个个的 VNode 节点,keep-alive 组件的缓存也是基于 VNode 节点的而不是直接存储 DOM 结构。它将满足条件(pruneCache 与 pruneCacheEntry)的组件在 cache 对象中缓存起来,在需要重新渲染的时候再将 vnode 节点从 cache 对象中取出并渲染。
5.4 组件通信方式
5.4.1 父子组件通信
- 父->子:
props - 子->父:
$emit(Vue2)/defineEmits(Vue3) - 获取组件实例:
$parent、$children(Vue2)ref:获取实例的方式调用组件的属性或者方法(Vue2/Vue3)
- 依赖注入:
provide、inject(Vue2中官方不推荐在应用中使用,但写组件库时很常用;Vue3中推荐使用,是组合式API的重要部分)
父子组件传值使用场景
| 方式 | 方向 | 说明 | 适用场景 |
|---|---|---|---|
| props | 父 → 子 | 最常用的父子通信方式,父组件通过属性传递数据给子组件 | 常规父子数据传递 |
| $emit | 子 → 父 | 子组件触发事件,父组件监听事件获取数据 | 子组件向父组件传递消息 |
| ref | 双向 | 获取子组件实例,直接访问其属性和方法 | 需要直接调用子组件方法 |
| v-model | 双向 | 语法糖,结合value属性和input事件实现双向绑定 | 表单组件双向数据同步 |
5.4.2 兄弟组件通信
- Event Bus:
- Vue2:
Vue.prototype.$bus = new Vue() - Vue3:使用
mitt或tiny-emitter等第三方库
- Vue2:
- 状态管理:
Vuex:Vue2官方状态管理库Pinia:Vue3官方推荐的状态管理库,比Vuex更轻量、更易于使用
兄弟组件通信使用场景
| 方式 | 说明 | 适用场景 |
|---|---|---|
| Event Bus | 事件总线模式,通过中间对象传递事件 | 兄弟组件间通信 |
| 状态管理 | 通过全局状态管理实现共享 | Vuex、Pinia |
5.4.3 跨级组件通信
$attrs、$listeners:- Vue2:
$attrs包含未被 props 接收的属性,$listeners包含未被处理的事件 - Vue3:
$listeners被合并到$attrs中
- Vue2:
provide、inject:适用于深度嵌套组件间的通信,Vue3中推荐使用- Pinia/Vuex:通过全局状态管理实现跨级通信
跨级组件通信使用场景
| 方式 | 说明 | 适用场景 |
|---|---|---|
| provide / inject | 祖先组件提供数据,后代组件注入使用 | 深度嵌套组件通信 |
| $attrs / $listeners | 透传未被props接收的属性和事件 | 中间组件透传数据 |
| Pinia/Vuex | 全局状态管理 | 复杂应用全局状态共享 |
5.4.4 其他方式通信
| 方式 | 说明 | 特点 |
|---|---|---|
| localStorage / sessionStorage | 通过浏览器本地存储传递数据 | 持久化存储,跨页面共享 |
| 原型挂载 | 通过Vue原型挂载全局变量 | 全局共享,不推荐频繁使用 |
5.4.5 推荐使用策略
- 优先使用:
props+$emit(简单直接) - 跨层级:
provide / inject(Vue3推荐) - 复杂应用:
Pinia(状态管理) - 兄弟通信:
Event Bus或Pinia - 表单组件:
v-model(双向绑定)
六、Vue指令
6.1 常用指令
| 指令 | 作用 |
|---|---|
v-if |
条件渲染(销毁/重建) |
v-show |
条件显示(display切换) |
v-for |
列表渲染 |
v-bind |
属性绑定 |
v-on |
事件绑定 |
v-model |
双向绑定 |
v-text |
文本内容 |
v-html |
HTML内容 |
v-pre |
跳过编译 |
v-cloak |
防止闪烁 |
v-once |
只渲染一次 |
七、Vue状态管理
7.1 Vuex核心概念
Vuex 是一个专门为 Vue.js 应用程序开发的状态管理库,采用集中式存储管理应用所有组件的状态。
说的简单一点就是类似于一个全局变量,可以实现任意组件关系之间的数据读取操作。
它有四个对象:state,getters,mutations 和actions。
const store = new Vuex.Store({
state: { count: 0 }, // 状态
getters: { doubleCount: state => state.count * 2 }, // 计算属性
mutations: {
increment(state) {
state.count++;
},
}, // 同步修改
actions: {
asyncIncrement({ commit }) {
commit("increment");
},
}, // 异步操作
modules: {
/* 模块化 */
},
});
| 概念 | 说明 |
|---|---|
| state | 单一状态树,存储变量(类似 data) |
| getters | 派生状态(类似 computed) |
| mutations | 定义方法,同步且显式修改 state,唯一可修改数据的途径,调用方法是使用 commit 调用 |
| actions | 定义方法,异步操作,提交 mutations,可包含任意异步操作(如请求数据),调用方法是使用 dispatch 调用 |
| modules | 模块化拆分,分类存储避免臃肿 |
如果我们的数据是后端提供过来的,需要缓存,我们可以在
actions请求数据,然后在actions中拿到数据调用mutations,由mutations去存储数据 |
7.2 map辅助函数
mutations 的调用方法是使用 commit 调用, actions 的调用方法是用 dispatch 调用,也可以使用 map 的辅助函数,可以直接解构调用。 比如:当组件需要获取多个状态时,使用辅助函数避免计算属性重复声明。
map 辅助函数有:mapState, mapGetters, mapActions, mapMutations。
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
computed: {
...mapState(['count']),
...mapGetters(['doubleCount'])
},
methods: {
...mapMutations(['increment']),
...mapActions(['asyncIncrement'])
}
当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性。mapMutations 其实跟 mapState 的作用是类似的,将组件中的 methods 映射为 store.commit 调用。
mapState 示例:
// store/index.js
const store = new Vuex.Store({
state: {
count: 0,
name: "张三",
age: 25,
},
});
// 组件中使用
import { mapState } from "vuex";
export default {
computed: {
// 方式一:数组形式
...mapState(["count", "name", "age"]),
// 方式二:对象形式(支持重命名)
...mapState({
countAlias: "count",
nameAlias: state => state.name,
ageWithSuffix(state) {
return state.age + "岁";
},
}),
},
mounted() {
console.log(this.count); // 0
console.log(this.countAlias); // 0
console.log(this.ageWithSuffix); // 25岁
},
};
mapGetters 示例:
// store/index.js
const store = new Vuex.Store({
state: { count: 0 },
getters: {
doubleCount: state => state.count * 2,
tripleCount: state => state.count * 3,
},
});
// 组件中使用
import { mapGetters } from "vuex";
export default {
computed: {
...mapGetters(["doubleCount", "tripleCount"]),
...mapGetters({
double: "doubleCount",
}),
},
mounted() {
console.log(this.doubleCount); // 0
console.log(this.double); // 0
},
};
mapMutations 示例:
// store/index.js
const store = new Vuex.Store({
state: { count: 0 },
mutations: {
increment(state) {
state.count++;
},
add(state, n) {
state.count += n;
},
},
});
// 组件中使用
import { mapMutations } from "vuex";
export default {
methods: {
...mapMutations(["increment", "add"]),
...mapMutations({
addAlias: "add",
}),
handleClick() {
this.increment(); // 等同于 this.$store.commit('increment')
this.add(5); // 等同于 this.$store.commit('add', 5)
this.addAlias(10); // 等同于 this.$store.commit('add', 10)
},
},
};
mapActions 示例:
// store/index.js
const store = new Vuex.Store({
state: { count: 0 },
mutations: {
increment(state) {
state.count++;
},
},
actions: {
asyncIncrement({ commit }) {
setTimeout(() => {
commit("increment");
}, 1000);
},
asyncAdd({ commit }, n) {
setTimeout(() => {
commit("add", n);
}, 1000);
},
},
});
// 组件中使用
import { mapActions } from "vuex";
export default {
methods: {
...mapActions(["asyncIncrement", "asyncAdd"]),
...mapActions({
asyncAddAlias: "asyncAdd",
}),
handleClick() {
this.asyncIncrement(); // 等同于 this.$store.dispatch('asyncIncrement')
this.asyncAdd(5); // 等同于 this.$store.dispatch('asyncAdd', 5)
},
},
};
mapState/mapGetters:生成计算属性mapMutations/mapActions:映射为store.commit/store.dispatch调用
7.3 项目实践
采用模块拆分方式(如 token 模块、用户信息模块),调用时需带上模块名称。
模块化使用示例:
// store/modules/user.js
export default {
namespaced: true, // 开启命名空间
state: {
name: "张三",
age: 25,
},
getters: {
userInfo: state => `${state.name} - ${state.age}`,
},
mutations: {
SET_NAME(state, name) {
state.name = name;
},
},
actions: {
updateName({ commit }, name) {
commit("SET_NAME", name);
},
},
};
// store/index.js
import Vue from "vue";
import Vuex from "vuex";
import user from "./modules/user";
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
user,
},
});
// 组件中使用带命名空间的模块
import { mapState, mapGetters, mapMutations, mapActions } from "vuex";
export default {
computed: {
...mapState("user", ["name", "age"]),
...mapGetters("user", ["userInfo"]),
},
methods: {
...mapMutations("user", ["SET_NAME"]),
...mapActions("user", ["updateName"]),
handleUpdate() {
this.SET_NAME("李四");
this.updateName("王五");
},
},
};
7.4 Vuex解决了浏览器存储什么问题
Vuex可以监听数据的变化。当Vuex数值发生变化时,其他组件处可以响应式地监听到该数据的变化,当数据改变时,项目中引用到该数据(并且正在监听的)的地方都会发生改变。- 可以存储任意形式的数据。浏览器存储中,数据只能以字符串的形式传入,对于不是
string格式的数据需要采用 JSON.parse() 和 JSON.stringify() 去读写。 - 可以进行模块化存储。使用
module模块化开发,可以对存储数据进行归类,避免存储内容过于臃肿。 - 没有数据存储大小限制。
Vuex是存储在内存中的,而 storage 存储在本地中,有一定的存储大小限制(cookie 4K,localStorage、sessionStorage 5M)存储内容过多会消耗内存空间,导致页面变卡。
7.5 Vuex vs localStorage
| 特性 | Vuex | localStorage |
|---|---|---|
| 响应式 | ✓ | ✗ |
| 数据类型 | 任意 | 仅字符串 |
| 存储位置 | 内存 | 本地 |
| 容量限制 | 无 | 5MB |
| 持久化 | ✗ | ✓ |
| 数据监听 | 支持响应式监听 | 需轮询 |
八、Pinia状态管理
Pinia 是 Vue3 官方推荐的新一代状态管理库,是 Vuex 的替代方案。它比 Vuex 更轻量、更易用,且完全兼容 Vue3 的 Composition API。
Pinia 核心概念:state、getters、actions,取消了 mutations(actions 直接修改 state)。
// stores/counter.js
import { defineStore } from "pinia";
export const useCounterStore = defineStore("counter", {
state: () => ({
count: 0,
name: "张三",
}),
getters: {
doubleCount: state => state.count * 2,
},
actions: {
increment() {
this.count++;
},
async fetchData() {
const res = await fetch("/api/data");
this.count = res.data;
},
},
});
8.1 Pinia 核心概念
| 概念 | 说明 |
|---|---|
| state | 状态数据,类似于 data,使用箭头函数返回初始值 |
| getters | 计算属性,类似于 computed,用于派生状态 |
| actions | 操作方法,同步/异步均可,可直接修改 state,替代Vuex的mutations |
8.2 Pinia setup 语法
Pinia 支持两种语法:Options API 风格和 setup 风格(推荐)。
setup 语法示例:
// stores/counter.js
import { defineStore } from "pinia";
import { ref, computed } from "vue";
export const useCounterStore = defineStore("counter", () => {
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
return { count, doubleCount, increment };
});
Count: {{ counter.count }}
Double: {{ counter.doubleCount }}
setup 语法优势:
- 更直观,与 Vue3 Composition API 一致
- 更好的 TypeScript 类型推导
- 支持在同一个地方组织相关逻辑
- 可以在 setup 外部调用 store
8.3 Pinia模块化
Pinia 通过不同文件定义不同 store,实现模块化。
user store:
// stores/user.js
import { defineStore } from "pinia";
export const useUserStore = defineStore("user", {
state: () => ({
name: "张三",
age: 25,
}),
getters: {
userInfo: state => `姓名:${state.name},年龄:${state.age}`,
},
actions: {
setName(newName) {
this.name = newName;
},
},
});
counter store:
// stores/counter.js
import { defineStore } from "pinia";
import { useUserStore } from "./user";
export const useCounterStore = defineStore("counter", {
state: () => ({
count: 0,
}),
getters: {
doubleCount: state => state.count * 2,
},
actions: {
increment() {
this.count++;
},
// 跨store调用
incrementAndSetUser() {
this.increment();
const userStore = useUserStore();
userStore.setName("李四");
},
},
});
store直接调用:
// 组件中调用不同store
import { useUserStore } from "@/stores/user";
import { useCounterStore } from "@/stores/counter";
const userStore = useUserStore();
const counterStore = useCounterStore();
8.4 Pinia vs Vuex
Pinia 是 Vue3 官方推荐的状态管理库,相比 Vuex 更加轻量和简洁。
| 特性 | Pinia | Vuex |
|---|---|---|
| API设计 | 简洁直观 | 相对复杂 |
| TypeScript | 更好的类型推导 | 需要手动标注类型 |
| Mutations | 无,actions直接修改state | 必须通过mutations修改 |
| 语法风格 | Options API + setup语法 | 仅Options API |
| 模块化 | 不同文件定义不同store | 通过modules配置 |
| 体积 | ~1KB | ~15kb |
| Vue2兼容 | 需要额外适配 | 官方支持 |
| DevTools | 支持 | 支持 |
| 异步处理 | actions中直接处理 | actions中处理 |
Pinia 优势:
- 轻量:体积小巧,对项目构建影响小
- 简洁:API 设计直观,学习成本低
- 类型安全:完美支持 TypeScript,类型推导更智能
- 模块化:通过函数返回store,模块边界更清晰
- 去除mutations:actions可以直接修改state,简化代码
Vuex 适用场景:
- Vue2 项目(Pinia 对 Vue2 支持不友好)
- 需要严格遵循单向数据流
- 大型复杂项目,有严格的代码规范要求
Pinia 适用场景:
- Vue3 项目(官方推荐)
- 需要快速开发迭代
- 使用 Composition API 的项目
- 中小型项目
九、Vue路由
vue-router 是 Vue 官方的路由管理器。
9.1 vue-router核心概念
| 概念 | 说明 |
|---|---|
| router | 路由器实例 |
| route | 当前路由对象 |
| routes | 路由配置数组 |
| navigation guards | 导航守卫 |
9.2 路由模式
| 模式 | 说明 | URL特点 |
|---|---|---|
| hash | 使用URL hash | 包含# |
| history | 使用history.pushState() API | 无# |
9.3 路由跳转方式
| 方式 | 说明 |
|---|---|
router-link |
声明式导航 |
router.push() |
编程式导航,添加历史记录,history 栈中添加一个记录,点击后退会返回到上一个页面 |
router.replace() |
编程式导航,替换当前记录,history 栈中不会有记录,点击返回会跳转到上个页面 (就是直接替换了当前页面) |
router.go(n) |
前进/后退n页,n 可为正整数或负整数 |
9.4 query与params
| 特性 | query | params |
|---|---|---|
| URL显示 | 显示在URL中 | 不显示 |
| 路由匹配 | path/name均可 | 仅支持name |
| 刷新保留 | 保留 | 丢失 |
9.5 动态路由
9.5.1 动态路由权限
根据用户角色或权限动态生成路由配置,实现不同用户看到不同菜单和页面的效果。
实现动态路由需要解决三个核心问题:
| 问题 | 解决方案 |
|---|---|
| 路由动态添加 | 使用 router.addRoute() 方法动态注册路由 |
| 组件懒加载 | 使用 () => import('@/pages/${view}') 动态导入组件 |
| 刷新后路由丢失 | 将路由数据持久化到 localStorage,刷新后重新加载 |
9.5.2 实现步骤
- 登录后获取后端路由配置
- 使用
router.addRoute()动态添加 - 路由数据持久化到localStorage
- 路由守卫处理刷新场景
1. 定义路由数据结构
后端返回的路由数据通常包含以下字段:
// 示例:后端返回的路由结构
const routesFromServer = [
{
path: "/dashboard",
name: "Dashboard",
component: "Dashboard.vue",
meta: { requiresAuth: true, roles: ["admin", "editor"] },
},
{
path: "/users",
name: "Users",
component: "Users.vue",
meta: { requiresAuth: true, roles: ["admin"] },
children: [{ path: "list", name: "UserList", component: "UserList.vue" }],
},
];
2. 动态添加路由
// router/index.js
import { createRouter, createWebHistory } from "vue-router";
const router = createRouter({
history: createWebHistory(),
routes: [{ path: "/login", name: "Login", component: () => import("@/pages/Login.vue") }],
});
// 动态添加路由的方法
export function addDynamicRoutes(routes) {
routes.forEach(route => {
// 动态导入组件
route.component = () => import(`@/pages/${route.component}`);
// 如果有子路由,递归处理
if (route.children) {
route.children.forEach(child => {
child.component = () => import(`@/pages/${child.component}`);
});
}
// 添加路由
router.addRoute(route);
});
}
export default router;
3. 登录后获取并注册路由
// store/modules/user.js
import { addDynamicRoutes } from "@/router";
const actions = {
async login({ commit }, userInfo) {
// 1. 调用登录接口
const response = await api.login(userInfo);
const { token, routes } = response.data;
// 2. 保存token到localStorage
localStorage.setItem("token", token);
// 3. 保存路由到localStorage(用于刷新后恢复)
localStorage.setItem("routes", JSON.stringify(routes));
// 4. 动态注册路由
addDynamicRoutes(routes);
// 5. 跳转首页
router.push("/dashboard");
},
};
4. 路由守卫处理刷新场景
// router/index.js
router.beforeEach(async (to, from, next) => {
const token = localStorage.getItem("token");
// 如果没有token且不是登录页,跳转到登录
if (!token && to.path !== "/login") {
return next("/login");
}
// 如果有token但路由未注册(刷新后)
if (token && !router.hasRoute(to.name)) {
// 从localStorage获取路由
const routes = JSON.parse(localStorage.getItem("routes") || "[]");
// 动态注册路由
addDynamicRoutes(routes);
// 重新跳转,确保路由已注册
return next({ ...to, replace: true });
}
next();
});
9.5.3 关键要点
- 路由数据持久化:登录成功后将路由数据保存到 localStorage,防止刷新后丢失
- 递归处理子路由:如果路由包含嵌套子路由,需要递归处理组件导入
- 路由守卫重定向:刷新后首次访问时,需要重新注册路由并重新跳转
- 权限校验:结合路由的
meta字段,在路由守卫中进行角色权限校验
9.5.4 注意事项
- 安全性:后端返回的路由数据需要进行校验,防止恶意数据注入
- 性能优化:路由数据较大时,可以考虑压缩存储
- 用户体验:刷新后重新注册路由可能会有短暂延迟,可以添加加载状态
- 路由去重:注册前检查路由是否已存在,避免重复注册
9.6 登录拦截
- token
- 路由拦截需要用到导航守卫 router.beforeEach
- router.beforeEach((to, from, next) => {})
- to: Route: 即将要进入的目标 路由对象
- from: Route: 当前导航正要离开的路由
- next: Function: 一定要调用该方法来 resolve 这个钩子。
十、Vue版本对比
10.1 Vue3 vs Vue2
| 特性 | Vue2 | Vue3 |
|---|---|---|
| 响应式 | Object.defineProperty | Proxy |
| API风格 | Options API | Composition API |
| 性能 | 相对较慢 | 更快(重写VDOM) |
| TypeScript | 支持有限 | 全面支持 |
| Tree Shaking | 较差 | 优秀 |
| Fragment | 不支持 | 支持 |
10.2 Vue3 新特性与优势
核心特性
| 特性 | 说明 |
|---|---|
| Composition API | 更好的逻辑复用,通过 setup 函数或 <script setup> 组织逻辑 |
| 响应式系统 | 基于 Proxy,替代 Object.defineProperty,支持 Map、Set、数组索引等 |
| Fragments | 多根节点组件,组件可以拥有多个根元素 |
| Suspense | 异步组件处理,支持异步加载状态展示 |
| Teleport | 组件传送,可以将子组件渲染到 DOM 树的其他位置 |
| Tree Shaking | 更友好的摇树优化,打包时去除无用代码 |
性能提升
- virtual DOM 完全重写,mounting patching 提速 100%
- 组件实例初始化速度提高 100%
- 提速一倍/内存使用降低一半
- 新的 core runtime:~10kb gzipped
- 重写虚拟 DOM:编译时优化更多,减少运行时开销
- 静态提升:将静态节点提升到渲染函数外,只创建一次,降低渲染成本
- 优化插槽生成:可以单独渲染父组件和子组件
- 支持自定义渲染器
API 变动
- 模板语法 99% 保持不变
- 更好的 TypeScript 支持:源码使用 TypeScript 重写,提供完整的类型推断
- 基于 Proxy 的观察者机制:满足全语言覆盖以及更好的性能
项目结构变化
- 移除了配置文件目录:config 和 build 文件夹
- 移除了 static 文件夹,新增 public 文件夹,index.html 移动到 public 中
- 在 src 文件夹中新增 views 文件夹,用于分类视图组件和公共组件
命令变化
| 命令 | Vue2 | Vue3 |
|---|---|---|
| 全局安装 | npm install -g vue@cli |
npm install -g @vue/cli |
| 创建项目 | vue create |
vue create |
| 启动项目 | npm run serve |
npm run serve |
| 列表查看 | vue list |
已删除 |
不兼容 IE11:检测机制更加全面、精准、高效,具备可调试式的响应跟踪
十一、Vue性能优化
11.1 编码阶段
| 优化项 | 说明 |
|---|---|
| 减少响应式数据 | 尽量减少 data 中的数据,data 中的数据都会增加 getter 和 setter,会收集对应的 watcher |
| 避免 v-if 和 v-for 连用 | Vue2:v-for 优先级高于 v-if,每次渲染都会先遍历整个列表,再判断条件Vue3: v-if 优先级高于 v-for,但官方仍不建议连用解决方案:使用计算属性提前过滤数据,或将 v-if 放到外层容器 |
| 事件代理 | 使用 v-for 给每项元素绑定事件时,使用事件代理减少监听器数量 |
| 组件缓存 | SPA 页面采用 keep-alive 缓存组件,避免重复渲染 |
| 合理使用 v-if 和 v-show | 频繁切换使用 v-show,条件很少改变使用 v-if |
| key 保证唯一 | 使用唯一且稳定的 key 值,避免使用索引作为 key |
| 路由懒加载 | 使用 import() 动态导入路由组件 |
| 异步组件 | 使用 defineAsyncComponent(Vue3)或 () => import()(Vue2)实现组件按需加载 |
| 防抖、节流 | 对高频触发的事件(如滚动、输入)使用防抖或节流 |
| 第三方模块按需导入 | 如 import { Button } from 'element-ui' 替代全量导入 |
| 长列表优化 | 使用虚拟滚动(如 vue-virtual-scroller)只渲染可视区域内容 |
| 图片懒加载 | 使用 loading="lazy" 或第三方库实现图片延迟加载 |
| 图片格式优化 | 使用 webp 格式,或使用图片云服务器 |
11.2 打包优化
| 优化项 | 说明 |
|---|---|
| 代码压缩 | 使用 terser-webpack-plugin 或 esbuild 压缩 JS/CSS |
| Tree Shaking | 去除未使用的代码,确保使用 ES Module 语法 |
| Scope Hoisting | 将模块合并到一个作用域,减少闭包和内存占用 |
| CDN 加载 | 将 Vue、VueRouter、Vuex、axios 等第三方库通过 CDN 引入,减少打包体积 |
| 多线程打包 | 使用 thread-loader 或 happypack 加速构建(Webpack4) |
| 代码分割 | 使用 splitChunks 抽离公共文件、第三方库、动态导入模块 |
| SourceMap 优化 | 生产环境关闭或简化 SourceMap,减少构建时间和文件体积 |
| 打包分析 | 使用 npm run build --report 生成打包分析图,分析优化方向 |
11.3 SEO优化
| 优化项 | 说明 |
|---|---|
| 预渲染(Prerender) | 针对少量页面的应用,构建时生成静态 HTML |
| 服务端渲染(SSR) | 使用 Nuxt.js 或自行搭建 SSR 服务,提升首屏渲染速度和 SEO |
11.4 用户体验
| 优化项 | 说明 |
|---|---|
| 骨架屏 | 在内容加载前展示页面结构,减少白屏时间 |
| PWA | 使用 Service Worker 实现离线访问、推送通知等功能 |
| 缓存策略 | 合理使用浏览器缓存、CDN 缓存、服务端缓存 |
| Gzip 压缩 | 服务端开启 Gzip/Brotli 压缩(如 nginx 配置),减少传输体积 |
十二、Vue设计模式
12.1 单例模式
定义:一个类只有一个实例,并提供一个全局访问点。
Vue中的应用:
- Vuex/Pinia 状态管理:整个应用只有一个 store 实例
- 事件总线(EventBus):全局事件管理
- Vue Router:路由实例全局唯一
实现示例:
// 事件总线单例模式
class EventBus {
constructor() {
if (!EventBus.instance) {
this.events = {};
EventBus.instance = this;
}
return EventBus.instance;
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
emit(event, ...args) {
if (this.events[event]) {
this.events[event].forEach(cb => cb(...args));
}
}
off(event, callback) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
}
}
const eventBus = new EventBus();
export default eventBus;
12.2 工厂模式
定义:定义一个创建对象的接口,让子类决定实例化哪个类。
Vue中的应用:
- h() 函数:创建 VNode 的工厂函数
- 异步组件工厂:() => import(‘./AsyncComponent.vue’)
- createElement 渲染函数
实现示例:
// VNode 工厂函数
function createVNode(tag, data, children) {
return {
tag,
data,
children,
key: data?.key,
attrs: data?.attrs,
nativeOn: data?.on,
componentOptions: data?.componentOptions,
};
}
// 异步组件工厂
function createAsyncComponent loader) {
return {
setup() {
const { loaded, error, loading } = useLoader(loader);
return () => {
if (loading.value) {
return createVNode('div', {}, 'Loading...');
}
if (error.value) {
return createVNode('div', {}, 'Error');
}
if (loaded.value) {
return loaded.value;
}
};
}
};
}
12.3 观察者模式
定义:定义对象间的一种一对多依赖关系,当一个对象状态改变时,所有依赖它的对象都会收到通知。
Vue中的应用:
- 响应式系统:数据变化时自动更新视图
- Watcher:订阅数据变化
- Dep:管理订阅者
实现示例:
class Dep {
constructor() {
this.subscribers = [];
}
addSub(subscriber) {
this.subscribers.push(subscriber);
}
notify() {
this.subscribers.forEach(sub => sub.update());
}
}
class Watcher {
constructor(vm, exp, callback) {
this.vm = vm;
this.exp = exp;
this.callback = callback;
Dep.target = this;
this.value = vm[exp];
Dep.target = null;
}
update() {
const newValue = this.vm[this.exp];
if (this.value !== newValue) {
this.callback(newValue, this.value);
this.value = newValue;
}
}
}
12.4 发布-订阅模式
定义:发布者和订阅者之间解耦,发布者不需要知道订阅者的存在。
Vue中的应用:
- EventBus:组件间通信
- Vue.observable:跨组件状态共享
- 自定义事件:$emit / $on
实现示例:
// 发布-订阅模式实现
class PubSub {
constructor() {
this.events = {};
}
subscribe(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
const token = Symbol();
this.events[event].push({ token, callback });
return token;
}
publish(event, data) {
if (this.events[event]) {
this.events[event].forEach(({ callback }) => {
callback(data);
});
}
}
unsubscribe(token) {
for (const event in this.events) {
this.events[event] = this.events[event].filter(item => item.token !== token);
}
}
}
12.5 装饰器模式
定义:动态地给对象添加一些额外的职责。
Vue中的应用:
- 高阶组件(HOC):增强组件功能
- mixins:混入复用逻辑
- 自定义指令:增强DOM元素能力
实现示例:
// 高阶组件 - 日志装饰器
function withLogger(WrappedComponent) {
return {
template: '<wrapped-component @click="log" />',
components: { WrappedComponent },
methods: {
log(...args) {
console.log("Component clicked:", args);
this.$refs.wrappedComponent.$emit("click", ...args);
},
},
};
}
// mixin 装饰器
const loggingMixin = {
created() {
console.log("Component created:", this.$options.name);
},
methods: {
log(action) {
console.log(`[${this.$options.name}] ${action}`);
},
},
};
// 自定义指令装饰器
const loadingDirective = {
mounted(el, binding) {
if (binding.value) {
el.classList.add("loading");
}
},
updated(el, binding) {
if (!binding.value) {
el.classList.remove("loading");
}
},
};
12.6 策略模式
定义:定义一系列算法,把它们一个个封装起来,使它们可以相互替换。
Vue中的应用:
- computed 不同策略:缓存 vs 非缓存
- render 函数:不同 render 策略
- Validator 表单验证:多种验证策略
实现示例:
// 表单验证策略
const validators = {
required(value) {
return value !== null && value !== undefined && value !== "";
},
email(value) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
},
minLength(value, length) {
return value && value.length >= length;
},
maxLength(value, length) {
return value && value.length <= length;
},
pattern(value, regex) {
return new RegExp(regex).test(value);
},
};
// 验证器类
class Validator {
constructor() {
this.rules = [];
}
addRule(strategy, errorMsg) {
this.rules.push({ strategy, errorMsg });
}
validate(value) {
for (const rule of this.rules) {
if (!rule.strategy(value)) {
return { valid: false, message: rule.errorMsg };
}
}
return { valid: true };
}
}
12.7 代理模式
定义:为其他对象提供一种代理以控制对这个对象的访问。
Vue中的应用:
- Object.defineProperty / Proxy:响应式数据代理
- $refs 代理:模板 refs 访问
- computed 属性代理:计算属性访问
实现示例:
// 响应式代理
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key);
const value = target[key];
return typeof value === "object" ? reactive(value) : value;
},
set(target, key, value) {
target[key] = value;
trigger(target, key);
return true;
},
deleteProperty(target, key) {
delete target[key];
trigger(target, key);
return true;
},
});
}
// $refs 代理
function proxyRefs(target) {
return new Proxy(target, {
get(target, key) {
return target.value[key];
},
set(target, key, value) {
target.value[key] = value;
return true;
},
});
}
12.8 组合模式
定义:将对象组合成树形结构以表示”部分-整体”的层次结构。
Vue中的应用:
- 组件树:父子组件组合
- Slot 插槽:内容分发组合
- 递归组件:自身引用组合
实现示例:
// 递归组件 - 树形菜单
const TreeNode = {
name: "TreeNode",
props: {
node: {
type: Object,
required: true,
},
},
template: `
<div>
<div @click="toggle">
{{ node.label }}
</div>
<div v-if="isOpen && node.children">
<tree-node
v-for="child in node.children"
:key="child.id"
:node="child"
/>
</div>
</div>
`,
data() {
return { isOpen: false };
},
methods: {
toggle() {
this.isOpen = !this.isOpen;
},
},
};
// 插槽组合
const Layout = {
template: `
<div class="layout">
<header><slot name="header" /></header>
<main><slot /></main>
<footer><slot name="footer" /></footer>
</div>
`,
};
12.9 设计模式总结
| 设计模式 | Vue中的应用场景 |
|---|---|
| 单例模式 | Vuex/Pinia、EventBus、Vue Router |
| 工厂模式 | h()函数、异步组件、createElement |
| 观察者模式 | 响应式系统、Watcher、Dep |
| 发布-订阅模式 | EventBus、$emit/$on、跨组件通信 |
| 装饰器模式 | HOC、mixins、自定义指令 |
| 策略模式 | 表单验证、computed、render策略 |
| 代理模式 | Proxy响应式、$refs、数据代理 |
| 组合模式 | 组件树、Slot、递归组件 |