对MVVM的理解
MVVM 是 Model-View-ViewModel 的缩写
- Model 代表数据模型,也可以在 Model 中定义数据修改和操作的业务逻辑。
- View 代表 UI 组件, 它负责将数据模型转化成 UI 展现出来。
- ViewModel 监听模型数据的改变和控制视图行为 、处理用户交互, 简单理解就是⼀个同步View 和 Model 的对象, 连接 Model 和 View
这种架构下,开发者只需关注业务逻辑,不需要手动操作DOM,不需要关注数据状态的同步问题, 复杂的数据状态维护完全由 MVVM 来统⼀管理
常用vue命令
| 指令 | 作用说明 | 典型用法示例 |
|---|---|---|
| v-text | 将元素的文本内容替换为表达式的值(替代 {``{}}) |
<p v-text="msg"></p> |
| v-html | 将元素的 innerHTML 更新为表达式的内容(会有 XSS 风险) | <div v-html="rawHtml"></div> |
| v-show | 根据表达式的真假切换元素的 display CSS 属性实现显示隐藏 |
<div v-show="isVisible"></div> |
| v-if | 条件渲染,表达式为真时渲染该元素,假时销毁元素(不占 DOM) | <p v-if="show">显示内容</p> |
| v-else | v-if 的反向条件,必须紧接 v-if 或 v-else-if 之后 |
<p v-else>隐藏内容</p> |
| v-else-if | v-if 的"else if"分支,用于链式条件判断 |
<p v-else-if="otherCondition">内容</p> |
| v-for | 列表渲染,用于遍历数组或对象生成一组元素 | <li v-for="item in items" :key="item.id">{``{ item.text }}</li> |
| v-bind | 动态绑定 HTML 属性或组件 prop,简写为 : |
<img :src="imgSrc"> |
| v-on | 监听 DOM 事件或组件自定义事件,简写为 @ |
<button @click="handleClick">点我</button> |
| v-model | 双向数据绑定,通常用于表单控件(input、select、textarea 等) | <input v-model="username"> |
| v-pre | 跳过该元素和子元素的编译,进行原样输出(提高性能,调试时用) | <span v-pre>{``{ rawMustache }}</span> |
| v-cloak | 保证 Vue 编译完成之后再显示元素,避免闪烁(通常配合 CSS 使用) | <div v-cloak>内容</div> |
| v-once | 元素和组件只渲染一次,之后的更新不会渲染该元素 | <span v-once>{``{ message }}</span> |
如何自定义vue命令
| 项目 | Vue 2 | Vue 3 |
|---|---|---|
| 注册方式 | Vue.directive() / 组件内 directives |
app.directive() / 组件内 directives |
| 生命周期钩子名称 | bind, inserted, update, componentUpdated, unbind |
created, beforeMount, mounted, beforeUpdate, updated, beforeUnmount, unmounted |
| 生命周期更多钩子 | 较少,只匹配关键阶段 | 更细粒度,更符合组件生命周期 |
| 指令 API 对象格式 | vs Vue 3 类似,只是钩子不同 | 同 Vue 2,但钩子名称改进 |
| 参数和修饰符 | 支持 binding.value, binding.arg, binding.modifiers |
同 Vue 2 |
vue的生命周期
1. Vue 2 生命周期钩子
Vue 2 的生命周期可以分为四个阶段:
| 阶段 | 钩子函数 | 说明 |
|---|---|---|
| 创建阶段 | beforeCreate |
实例初始化时调用,数据观测和事件未初始化 |
created |
实例已创建,数据观测和事件已初始化 | |
| 挂载阶段 | beforeMount |
挂载开始之前,相关 DOM 还未渲染 |
mounted |
挂载完成,DOM 已插入页面 | |
| 更新阶段 | beforeUpdate |
数据更新后,DOM 重新渲染前调用 |
updated |
组件 DOM 重新渲染并更新后调用 | |
| 销毁阶段 | beforeDestroy |
实例销毁前调用 |
destroyed |
实例销毁完成,解绑所有事件,清理资源 |
2. Vue 3 生命周期钩子
Vue 3 基于组合式 API 升级,仍保留选项式生命周期,同时对生命周期做了更多补充。整体钩子和 Vue 2 类似,具体列表:
| 阶段 | 钩子函数 | 说明 |
|---|---|---|
| 创建阶段 | beforeCreate |
Vue 3 依然支持,但在组合式 API 不常用 |
created |
同上 | |
| 挂载阶段 | beforeMount |
挂载开始之前 |
mounted |
挂载完成 | |
| 更新阶段 | beforeUpdate |
数据更新前,DOM 渲染之前调用 |
updated |
DOM 更新完成后调用 | |
| 卸载阶段 | beforeUnmount |
实例卸载前调用(Vue 3 新增) |
unmounted |
实例卸载后调用(Vue 3 新增) |
3. Vue 3 组合式 API 中的生命周期钩子
Vue 3 新增了组合式 API(setup 函数),通过如下函数注册生命周期钩子:
| 生命周期阶段 | 组合式 API 钩子函数 | 说明 |
|---|---|---|
| 创建 | onBeforeMount(() => {}) |
挂载之前 |
onMounted(() => {}) |
挂载完成 | |
| 更新 | onBeforeUpdate(() => {}) |
更新之前 |
onUpdated(() => {}) |
更新完成 | |
| 卸载 | onBeforeUnmount(() => {}) |
卸载之前 |
onUnmounted(() => {}) |
卸载完成 | |
| 激活和停用 | onActivated(() => {}) |
keep-alive 组件激活 |
onDeactivated(() => {}) |
keep-alive 组件停用 |
vue的响应式原理
1.vue2
核心是依赖收集 + 发布订阅模式,使用 Object.defineProperty 对对象的属性进行 getter/setter 拦截。
javascript
function defineReactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
if (Dep.target) { // Dep.target 指当前扫描的 watcher
dep.addDep(Dep.target)
}
return val
},
set(newVal) {
if (newVal !== val) {
val = newVal
dep.notify() // 通知所有 watcher 更新
}
}
})
}
限制和缺点:
- 只能监听对象已有属性,新增新属性不会自动响应式(需要用 Vue.set)
- 数组的变化通过变异方法(push、pop等)重写实现响应式
- 只能监听到对象的一级属性,深层嵌套需要递归监听
2.vue3
基于 ES6 的 Proxy 实现响应式。代理整个对象,拦截所有属性的访问/修改/删除,不需要递归每个子属性。原理:对目标对象做代理,所有访问通过 get、set 拦截完成。
javascript
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// 依赖收集
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
// 触发依赖
trigger(target, key)
return result
}
})
}
3.总结
| 方面 | Vue 2 (Object.defineProperty) |
Vue 3 (Proxy) |
|---|---|---|
| 技术实现 | 对对象属性逐个用 defineProperty 拦截 |
用 Proxy 包裹整个对象进行拦截 |
| 支持新增/删除属性 | 不支持,需要用 Vue.set |
天生支持 |
| 数组响应式 | 通过重写数组变异方法实现 | 数组作为对象代理自然支持 |
| 依赖收集粒度 | 只能对属性做依赖收集 | 基于操作符拦截,更精准,按需追踪 |
| 性能 | 需要递归遍历,性能压力大 | 性能更好,按需依赖追踪 |
| 实现复杂度 | 需要递归遍历、手动处理数组 | 简洁,统一代理所有操作 |
vue的diff算法
Vue 基于虚拟 DOM 实现渲染,每次状态变化产生新 VNode 树,会通过 Diff 算法比较新旧 VNode 树的差异,根据差异调用最少的 DOM 操作,实现 DOM 最小化更新。
| 处理方式 | 意义 | 操作 | 指针变化 |
|---|---|---|---|
| 旧前 vs 新前 | 顺序未变,头部对比 | patch,指针+1 | oldStart++, newStart++ |
| 旧后 vs 新后 | 顺序未变,尾部对比 | patch,指针-1 | oldEnd--, newEnd-- |
| 旧后 vs 新前 | 右移节点至左 | patch + 移动旧尾节点 | oldEnd--, newStart++ |
| 旧前 vs 新后 | 左移节点至右 | patch + 移动旧前节点 | oldStart++, newEnd-- |
| 其它(key查找) | 复用已有节点或新增节点 | 移动或插入节点 | 根据具体操作调整指针 |
| 新节点剩余 | 新增节点 | 创建新节点 | newStart++ 或 newEnd-- |
| 旧节点剩余 | 删除多余节点 | 删除节点 | oldStart++ 或 oldEnd-- |

vue的Optional API 和Composition API
1. Options API(选项式 API)
这是 Vue 2 以及 Vue 3 依然支持的传统写法,开发者通过预定义的组件选项(Options)来组织代码,比如 data、methods、computed、watch、props 等。
特点:
- 代码组织结构清晰,基于 Vue 约定的选项划分功能。
- 易于初学者理解,适合小中型项目。
- 组件逻辑往往按功能划分,但可能导致同一功能分散在多个选项内,随着组件变复杂难以维护。
示例代码
javascript
export default {
data() {
return {
count: 0
}
},
computed: {
doubled() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
}
},
watch: {
count(newVal, oldVal) {
console.log('count changed:', newVal)
}
}
}
2. Composition API(组合式 API)
Vue 3 引入的新方式,开发者通过在 setup() 函数内使用函数调用和响应式 API(如 ref、reactive、computed、watch 等)组合逻辑代码。
特点:
- 逻辑复用和组合更灵活:可以把相关代码片段封装成可复用的函数(composables)。
- 更适合大型项目和复杂组件,避免 Options API 中不同选项间逻辑分散问题。
- 代码布局更灵活,方便按业务功能组织代码而非选项划分。
- 需要了解一定的响应式原理,对初学者有一定门槛。
javascript
import { ref, computed, watch } from 'vue'
export default {
setup() {
const count = ref(0)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
watch(count, (newVal, oldVal) => {
console.log('count changed:', newVal)
})
return {
count,
doubled,
increment
}
}
}
vue中的逻辑复用
1. Options API 的逻辑复用方式及局限
主要手段
- mixins(混入)
- extends(继承)
这些机制允许把一段逻辑写在一个 mixin 对象或基类组件中,然后被多个组件复用。
javascript
// mixin.js
export const myMixin = {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}
javascript
import { myMixin } from './mixin'
export default {
mixins: [myMixin],
mounted() {
this.increment()
}
}
局限和问题
-
**命名冲突:**不同 mixin 可能有相同的 data、methods、computed 名称,产生冲突难以管理。
-
**隐式依赖和状态来源混乱:**mixin 里面有状态和方法,组件使用后状态来自何处不直观,增加理解和维护成本。
-
**逻辑难以拆分和组合:**mixin 是扁平的配置,无法像函数那样灵活接收参数、返回结果、依赖注入。
-
**追踪调用关系困难:**逻辑分散在多个 mixin,调试时难以跟踪和维护。
-
扩展继承复杂度: 使用
extends形式继承 父组件逻辑时,继承链越长越复杂,状态管理困难。
2.Composition API 的逻辑复用 ------ Composable 函数
核心思想
- 将一块响应式逻辑封装到一个普通函数中(称为 composable 函数)
- 函数内部使用
ref、reactive、computed、watch等 API - 通过返回对象暴露响应式数据和方法给调用组件使用
javascript
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
return {
count,
doubled,
increment
}
}
javascript
import { useCounter } from './useCounter'
export default {
setup() {
const { count, doubled, increment } = useCounter(5)
return {
count,
doubled,
increment
}
}
}
Vue递归组件
html
<script setup>
import { defineProps } from 'vue'
const props = defineProps({
items: {
type: Array,
required: true
}
})
</script>
<template>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.label }}
<!-- 如果有子项,则递归调用自身 -->
<RecursiveMenu v-if="item.children && item.children.length" :items="item.children" />
</li>
</ul>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'RecursiveMenu'
})
</script>
vue异步组件
javascript
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent({
// 必须是返回 Promise 的函数
loader: () => import('./YourComponent.vue'),
// 加载超时时间,超过时展示错误组件
timeout: 3000,
// 组件加载时显示的占位组件
loadingComponent: LoadingSpinner,
// 加载失败时显示的错误组件
errorComponent: ErrorComponent,
// 重试次数(默认 0)
retries: 1
})
javascript
<template>
<div>
<h2>异步组件示例</h2>
<AsyncComp />
</div>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
import LoadingSpinner from './LoadingSpinner.vue'
import ErrorComponent from './ErrorComponent.vue'
const AsyncComp = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorComponent,
timeout: 5000
})
</script>
在vue2中
javascript
export default {
components: {
AsyncExample: () => import('./AsyncExample.vue')
}
}
vue的keep-alive
- keep-alive 是一个 Vue全局组件
- keep-alive 本身不会渲染出来,也不会出现在父组件链中
- keep-alive 包裹动态组件时,会缓存不活动的组件,而不是销毁它们
keep-alive 接收三个参数:
- include :可传字符串、正则表达式、数组 ,名称匹配成功的组件会被缓存
- exclude :可传字符串、正则表达式、数组 ,名称匹配成功的组件不会被缓存
- max :可传数字 ,限制缓存组件的最大数量
include 和 exclude ,传数组 情况居多。
keep-alive 主要做了以下部分:
- created :初始化一个 cache、keys ,前者用来存缓存组件的虚拟dom集合,后者用来存缓组件的key集合
- mounted :实时监听 include、exclude 这两个的变化,并执行相应操作
- destroyed :删除掉所有缓存相关的东西
使用场景:
html
<keep-alive :include="allowList" :exclude="noAllowList" :max="amount">
<router-view></router-view>
</keep-alive>
html
<keep-alive :include="allowList" :exclude="noAllowList" :max="amount">
<component :is="currentComponent"></component>
</keep-alive>
当组件被<keep-alive>包裹时,会触发两个额外的生命周期钩子:
activated
当被缓存的组件重新激活时调用。适用于需要重新触发数据更新或DOM操作的场景,例如恢复定时器或重新请求数据。
deactivated
当被缓存的组件失活时调用。适用于清理工作,例如清除定时器或取消未完成的请求。
vuex和pinia
1. Vuex
简介
- Vuex 是 Vue 官方推出的状态管理库,面向 Vue 2 设计,也支持 Vue 3。
- 强调集中式存储管理,所有组件的状态集中到 store 中,使用"单一状态树"。
- 基于 Flux 架构,采用 mutations 进行状态同步修改,actions 处理异步逻辑。
核心概念
- State(状态):集中维护的响应式数据。
- Mutations:唯一修改 state 的方法,必须是同步函数。
- Actions :处理异步逻辑,通过
commit触发 mutations。 - Getters:类似计算属性,对 state 数据进行包装和过滤。
- Modules:当应用复杂时,支持模块化拆分 store。
javascript
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
},
getters: {
doubleCount(state) {
return state.count * 2
}
}
})
export default store
2. Pinia
简介
- Pinia 是 Vue 官方推荐的新一代状态管理库,设计更简洁友好,完美支持 Vue 3 和 Composition API。
- 语法更接近普通的模块化 JS,更灵活且易学,支持直接调用操作状态。
核心概念
- Store 是一个组合式的状态容器。
- 通过
defineStore定义 store。 - 支持直接修改 state(因为内部用 Proxy 劫持,状态依然响应式)。
- 支持类似计算属性的 getters,以及内置 actions 处理异步和同步逻辑。
javascript
import { defineStore } from 'pinia'
// 定义 store
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
},
incrementAsync() {
setTimeout(() => {
this.increment()
}, 1000)
}
}
})
javascript
import { useCounterStore } from './stores/counter'
export default {
setup() {
const counter = useCounterStore()
counter.increment()
return { counter }
}
}
3.对比
| 特性/维度 | Vuex | Pinia |
|---|---|---|
| API 设计 | 需定义 state, mutations, actions | 定义 state、getters、actions,更简洁 |
| 异步处理 | 通过 actions | 直接在 actions 中处理异步 |
| 使用便利性 | 比较繁琐 | 更现代,学习曲线低 |
| TS 支持 | 支持但复杂 | 原生 TypeScript 支持友好 |
| Vue 版本 | Vue 2/3 都支持 | 主要面向 Vue 3,Vue 2 有社区支持 |
| 模块化 | 支持模块拆分 | 自然模块化,多个 store 独立管理 |
| 生态和社区 | 成熟且庞大 | 新兴且快速增长 |
| 功能支持 | 完整、稳定 | 轻量、现代、更灵活 |
vue-router
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About }
]
const router = createRouter({
history: createWebHistory(), // 使用 HTML5 History 模式
routes
})
export default router
| 钩子函数 | 触发时机 | 是否可阻止导航 | 是否可访问组件实例 |
|---|---|---|---|
beforeEach |
全局路由开始切换 | 可 | 否 |
beforeResolve |
导航流程最后阶段 | 可 | 否 |
afterEach |
导航完成 | 否 | 否 |
beforeEnter |
路由独享守卫 | 可 | 否 |
beforeRouteEnter |
进入路由组件之前 | 可 | 通过 next 访问 |
beforeRouteUpdate |
路由参数变化,组件复用 | 可 | 是 |
beforeRouteLeave |
离开路由组件时 | 可 | 是 |