核心,reactivity,runtime,compile,响应式系统,渲染系统,编译系统
一、生命周期钩子
主要变化
- setup() 函数 替代 beforeCreate 和 created
<KeepAlive>组件相关钩子 onActivated/onDeactivated,替代activated/deactivated- onBeforeMount和onMounted和onBeforeUpdate和onUpdated,替代beforeMount和mounted和beforeUpdate和updated
- onBeforeUnmount和onUnmounted,替代beforeDestroy和destroyed
- 新增服务器端渲染钩子 onServerPrefetch
- 新增调试钩子 onRenderTracked(跟踪响应式依赖)和 onRenderTriggered(响应式变更触发)
- 新增错误捕获钩子 onErrorCaptured
父子组件生命周期执行顺序
挂载阶段:父onBeforeMount → 子onBeforeMount → 子onMounted → 父onMounted
更新阶段:父onBeforeUpdate → 子onBeforeUpdate → 子onUpdated → 父onUpdated
卸载阶段:父onBeforeUnmount → 子onBeforeUnmount → 子onUnmounted → 父onUnmounted
异步组件加载时机
- 客户端:在
onMounted中加载,确保 DOM 已挂载 - 服务端:在
onServerPrefetch中加载,确保 SSR 渲染完整性
nextTick
把代码放到任务队列中,优先使用promise,如果不支持再使用mutationobserver或者settimeout。vue中监听数据变化后dom是异步更新的,这个异步更新也是用的$nexttick做的,就是把dom的更新放到任务队列中,我们在数据改变的后边使用nexttick,就会生成新的异步任务放到dom更新的任务后边等待执行,所以执行的时候就能获取到更新后的dom了。
import { nextTick } from 'vue';
async function batchUpdate() {
state.a = 1;
state.b = 2;
await nextTick() // 等待一次更新
// DOM已更新
}
二、组件通信方式
1. Props & Emits(基础通信)
// 子组件
defineProps(['name', 'age'])
defineEmits(['update:name', 'updateAge']
2. v-model(双向绑定)
支持多个 v-model 绑定,语法更简洁:
// 父组件
<Child v-model="val" v-model:name="name" v-model:age="age">
// 子组件
defineProps(['modelValue', name', 'age'])
defineEmits(['update:modelValue', update:name', 'update:age'])
3. ref & expose(组件方法调用)
// 父组件
<Child ref="childRef">
const childRef = ref(null)
childRef.value.method()
// 子组件
defineExpose(['method'])
// 原理:编译时会编译defineExpose宏函数,编译后执行会将里面的属性方法,绑定到子组件实例的exposed属性上,父组件访问时访问的就是这个exposed上的validate方法
注:宏
一种特殊函数,提供特定的功能,在编译时会被转换成浏览器能直接运行的代码 不需要从vue中import 编译时只处理最外层的宏
4. provide/inject(跨层级通信)
基础示例
// 祖先组件
const provideData = ref('initial')
provide("provideData", provideData)
// 后代组件
const injectedData = inject("provideData")
注意如下情况
// 祖先组件
const provideData = ref('provideDataRef')
onBeforeMount(() => {
provide("provideData", provideData)
setTimeout(() => {
provideData.value = 'provideDataRef1'
}, 2000)
})
// 后代组件
const ipd = inject("provideData")
const pd = computed(() => {
console.log("child computed");
return ipd?.value
})
watch(pd, (newValue, oldValue) => {
console.log("child watch", newValue, oldValue);
}, { immediate: true })
5. 状态管理(Pinia/Vuex)
适用于复杂应用状态管理。
6. 属性透传($attrs)
父组件:
<Child v-bind="$attrs">
子组件:
<template>
{{$attrs.params}}
</template>
<script>
// 禁用自动继承
defineOptions({ inheritAttrs: false }) // defineOptions,v3.3新增,用来声明组件选项,包括name、inheritAttrs、Options
const attrs = useAttrs()
</script>
问题
父子组件通信
- props和emits
- v-model
- ref/expose
provide/inject能代替状态管理?
不能,用于层级明确的组件间的数据传递。兄弟组件不能传。子传父不能
为什么移除事件总线eventbus?
全局事件写的多了不好维护
如何实现兄弟组件传参
- props/emits通过父组件中转
- pinia状态管理
动态组件如何保持状态?
<KeepAlive>
<component :is="currentComponent" />
</KeepAlive>
三、修饰符
事件修饰符
.prevent:阻止默认行为.stop:阻止事件冒泡,还可以连用,如@click.stop.prevent.once:只触发一次.passive:优化滚动性能,如<div @scroll.passive="onScroll">滚动优化
键盘修饰符
.enter:按下回车键时触发,如@keyup.enter="handleSubmit".exact:精确匹配,防止组合按键时触发,如<button @click.ctrl.exact="onlyCtrl">仅 Ctrl 按下时触发
鼠标修饰符
.left:左键点击.right:右键点击,如<div @mousedown.right="handleRightClick">右键点击.middle:中键点击
v-model 修饰符
.lazy:输入框失焦后更新,延迟数据同步(替代input为change事件).trim:去除首尾空格.number:转换为数字类型
自定义修饰符
通过 modelModifiers实现自定义逻辑:
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) }
})
// 通过modelModifiers判断修饰符存在,并调整数据逻辑。
vue2到vue3的修饰符改动
-
.sync修饰符的替代,改用v-model:propName+emit('update:propName') 或 defineModel
// defineModel, v3.4新增,它用来声明一个双向绑定的prop // 父组件使用v-model:page="queryParams.pageNum" // 子组件使用defineModel接收,返回一个ref,修改子组件中prop的值,父组件会同步更新 const currentPage = defineModel("page", { type: Number, required: true, default: 1, }); // 父组件直接使用默认的 v-model="value" // 子组件接收modelValue const value = defineModel("modelValue", { type: Number, default: 1, }); // 原理:它接收一个prop,返回一个ref,ref的值与prop同步更新,同时监听ref的变化,如果变化,触发update:prop的事件去更新父组件的v-model的绑定值 // 等同代码:const props = defineProps({ prop: {} }), const emit = defineEmits(['update:prop']), emit('update:prop', val) -
.native修饰符移除,Vue2组件上绑定原生事件需用.native,Vue3默认不绑定到根元素,需通过emits声明或手动绑定(子组件手动绑定并emit事件,使用v-bind="$attrs"将事件绑定到内部元素)。
四、指令
内置指令
-
v-bind 同名属性合并策略:Vue3中后绑定的属性会覆盖前面的,而Vue2会合并。
-
v-on
-
v-if/v-else-if/v-else
-
v-for, vue3中v-if优先级高。key用来优化虚拟DOM复用,使算法精确找到修改的项,避免渲染错误和重复渲染未改变的项
-
v-model 可以绑定多个属性,可以自定义修饰符然后用modelModifiers访问
-
v-show:display:none, 初始渲染成本高,频繁切换性能好
-
v-html:渲染原始HTML(警惕XSS攻击)
-
v-text:替代 {{ }} 插值
-
v-pre:跳过编译,保留原始内容
-
v-cloak:隐藏未编译的模版(配合css使用)
-
v-once:仅渲染一次,后续数据变化不更新。
<div v-once>{{ staticContent }}</div>
-
v-for中的ref 获取循环后的dom
<div v-for="item in list" :ref="setItemRef" :key="item">{{item}}</div> const itemRefs = []; const setItemRef = el => { if (el) itemRefs.push(el); }; -
v-for为什么要有key
标识遍历的每个节点:key必须是唯一的,帮助 Vue 识别哪些节点是新的、哪些是已存在的
最小化 DOM 操作:当数据顺序变化时,Vue 可以复用已有 DOM 节点而不是重新创建
自定义指令
// 全局
app.directive('focus', {
mounted(el) { el.focus(); }
});
// 局部
const vFocus = { mounted: (el) => el.focus() };
指令生命周期钩子
created:元素属性初始化前 beforeMount:元素插入DOM前(Vue2的bind) mounted:元素插入DMO后(Vue2的inserted) beforeUpdate:组件更新前 update:组件更新后 beforeUnmount:组件卸载前(Vue2的unbind) unMounted:组件卸载后
指令参数
el:绑定的DOM元素 binding:绑定以下属性:
- value:指令的绑定值(如v-dir="value")
- oldValue:旧值(仅在beforeUpdate和update可用)
- arg:指令参数(如v-dir:arg)
- modifiers:修饰符对象(如v-dir.modif->{ modif: true })
- instance:组件实例(替代Vue2的vnode.context)
const vPermission = {
mounted(el, binding) {
const roles = store.getters.roles;
if (!roles.includes(binding.value)) {
el.parentNode?.removeChild(el);
}
}
};
五、插槽
定义
默认插槽 具名插槽
v-slot:header <slot name="header"></slot>
**作用域插槽 **
<slot :user="user" :data="data"></slot>
<template #default="{ user, data }">
{{ user.age }}
</template>
问题
1.作用域插槽的作用是什么?
反向传递数据
2.如何传递多个插槽?
多个template标签用具名插槽
3.slot的name属性是否支持动态绑定?
支持,但父组件用的时候的插槽名也需要动态
4.作用域插槽实现原理
插槽编译后是一个渲染函数,将子组件数据作为函数参数传递给插槽
5.如何实现插槽的动态切换?
v-if v-show
使用动态插槽名
6.插槽性能优化
避免在插槽中进行复杂计算
使用v-once缓存插槽内容
六、计算属性&监听
定义
computed:计算属性,缓存计算结果,依赖变化时自动更新。不支持异步操作
watch:显式监听属性,属性变化时执行回调。返回停止监听的函数。默认在组件更新前执行,flush:pre/post/sync。pre可以访问旧的dom状态,post访问更新后的dom,sync在依赖变更时同步执行(影响性能)
watchEffect:监听属性,属性变化时执行回调。返回停止监听的函数。默认在组件更新前执行
watch&watchEffect
- watch是一个选项api,在组件选项中使用,watchEffect是一个函数api,可以在setup和生命周期中使用
- watch需要监听指定属性,可以监听多个,watchEffect不需要指定,可以自动追踪函数内响应式数据的变化
- watch如果不设置immediate为true,就不会在页面加载的时候就执行一次,只有监听的属性变化才执行。watchEffect在组件初始化时就执行一次
- watch可以获取到监听属性的旧值,watchEffect则不能
- 应用场景,watch适合更细粒度的监听
七、响应式系统
ref vs reactive
| 特性 | ref | reactive |
|---|---|---|
| 基础类型 | ✅ 推荐 | ❌ 不适用 |
| 引用类型 | ✅ 可用 | ✅ 推荐(需要深层嵌套的复杂引用类型) |
| 解构响应式 | 保持响应性 | 需要 toRefs |
| 语法复杂度 | 需要 .value | 直接访问 |
响应式原理
ref:通过 getter/setter 封装 value 属性
// ref为什么要.vlaue
// 基础类型数据不能用proxy代理,ref封装了一个对象,对象里用get和set来监听value属性,可以直接用.value来访问,get的收集依赖,返回value值,set的时候赋值,触发依赖变化,执行响应式的一些逻辑
// 对引用类型,内部转为reactive处理
// 底层实现简化
function ref(value) {
return {
__v_isRef: true,
get value() { track(this, 'value'); return value; },
set value(newVal) { value = newVal; trigger(this, 'value'); }
};
}
reactive:使用 Proxy 代理整个对象
响应式系统简化实现
reactive
简单实现reactive,核心有几部分
effect:响应式的依赖收集和触发的机制的体现
reactive函数:提供出去要使用的函数
track:收集/追踪依赖
trigger:触发依赖
// effect,接收一个回调函数,执行
let activeEffect = null
class reactiveEffect {
constructor(fn) {
this.fn = fn
}
run() {
activeEffect = this
return this.fn()
}
}
const effect = (fn) => {
const re = new reactiveEffect(fn)
re.run()
}
// reactive,接收一个对象,返回它的代理对象
function reactiveFn(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key)
return target[key]
},
set(target, key, value, receiver) {
target[key] = value
trigger(target, key)
}
})
}
// track,收集依赖
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(activeEffect)
}
// trigger,执行依赖
function trigger(target, key) {
let depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) dep.forEach(eff => {
eff.fn()
})
}
// 使用
const student = {
name: 'Adam',
age: '18'
}
const proxyStudent = reactiveFn(student)
effect(() => {
document.querySelector('.wrap').innerHTML = proxyStudent.name + proxyStudent.age
})
proxyStudent.age = 19
ref
**reactive 只能构建复杂数据类型的响应性,proxy就是这样。**vue为我们提供了ref来解决这个问题。比较不熟悉的是 gett和set
class RefC {
constructor(value) {
this._value = value
}
get value() {
if (activeEffect) {
console.error('reffff', ref)
const dep = ref.dep || (ref.dep = new Set())
dep.add(activeEffect)
}
return this._value
}
set value(newVal) {
this._value = newVal
if (ref.dep) ref.dep.forEach(eff => eff.fn())
}
}
function ref(value) {
return new RefC(value)
}
// 使用
const v = '小明'
const refVar = ref(v)
effect(() => {
document.querySelector('.wrap').innerHTML = refVar.value
})
refVar.value = '小红'
解构保持响应性toRefs
为什么解构reactive对象会失去响应性? 解构得到的是普通值,非响应式引用;使用toRefs转化为ref
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state)
reactive的局限性
无法替换整个对象,整个对象替换,响应性丢失
// 响应性丢失
let obj = reactive({ count: 0, name: 'Vue' });
obj = { count: 1, name: 'Vue' };
// 解决方案
使用Object.assign或ref包裹对象
Object.assign(obj, { count: 1, name: 'Vue' });
let obj1 = ref({ count: 0, name: 'Vue' })
obj1.value = { count: 1, name: 'Vue' };
使用ref实现防抖功能
function debouncedRef(value, delay = 200) {
let timeout;
return customRef((track, trigger) => ({
get(){
track();
return value;
}
set(newVal){
clearTimeout(timeout);
timeout = setTimeout(() => {
value = newVal;
tigger();
}, delay);
}
}))
}
// 使用
const text = debouncedRef('', 500);
八、keep-alive
介绍
主要是缓存组件实例,ssr不兼容
- 频繁切换组件时可以利用缓存提高性能
- 可以保存组件状态
使用方式
<template>
<keep-alive :include="['ComponentA', 'ComponentB']" :max="5">
<component :is="currentComponent"></component>
</keep-alive>
</template>
keep-alive用include(组件名) key属性,通过key强制重新渲染(改变key会销毁旧实例)
生命周期钩子变化
新增onActivated和onDeactivated 顺序 onMounted -> onActivated -> onUpdated -> onDeactivated -> 再次进入onActivated -> onUnmounted
属性
include
支持字符串、数组、正则
exclude
排除指定组件,优先级高于include
max
最大缓存实例数,避免内存使用过大,超出时按LRU(最近最少使用)策略淘汰旧实例 LUR原理:有限淘汰最久未访问的实例
实现原理
通过Map或Object缓存组件vnode实例,渲染时直接从缓存中取,dom会在文档中移除,激活时跟进缓存的vnode实例重新在文档中插入dom
配合路由使用
不用include可以用路由里面的自定义字段,如meta里的字段,来选择性的使用keepalive
缓存组件刷新数据
在onActivated钩子中刷新数据
九、异步组件
介绍
异步组件defineAsyncComponent,返回Promise
- 异步组件会被分割成独立的chunk,按需加载,减少主包体积,所以能优化首屏,也要避免过度分割,导致http请求过多
- 配合代码分割
- 可以配置异步组件的加载提示、错误提示等
异步组件加载失败后重试
const asyncComponent = defineAsyncComponent({
loader: () => import('./asyncComponent.vue').catch(err => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(import('./asyncComponent.vue')), 3000)
})
}),
})
生命周期
执行loader,下载组件,初始化组件,缓存组件
vue3的异步组件和vue2的区别
vue2语法是vue.component('async-comp', () => import('./async-comp.vue')) vue3语法是defineAsyncComponent,vue3支持更细粒度的控制,如加载提示、错误提示等
应用场景
1.路由懒加载 2.首页的图表组件
Suspense(异步组件)
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
十、状态管理(Pinia)
示例代码
<script>
export const useStore = defineStore('storeId', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
</script>
<template>
<div>{{ counterStore.count }}</div>
<button @click="counterStore.increment()">+1</button>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter';
const counterStore = useCounterStore();
</script>
优势特性
- 去除 mutations,使用actions处理同步/异步逻辑,简化 API,体积更小
- 完整的 TypeScript 支持
- 模块化设计,支持热更新
十一、Vue Router 4.x 新特性
路由模式
import {
createRouter,
createWebHistory, // History模式(需服务器支持)
createWebHashHistory, // Hash模式
createMemoryHistory, // SSR或测试环境
} from 'vue-router'
// vue router 3.x是new Router()
const router = createRouter({
history: createWebHistory(), // History 模式
routes: [...]
})
组合式 API 支持
import { useRoute, useRouter } from 'vue-router'
const route = useRoute() // 当前路由信息
const router = useRouter() // 路由实例
导航守卫
组件内守卫
//onBeforeRouteUpdate:路由参数变化
//onBeforeRouteLeave:离开组件前
import { onBeforeRouteLeave } from 'vue-router';
export default {
setup() {
onBeforeRouteLeave((to, from, next) => {
// 清理逻辑
next();
});
}
};
onBeforeRouteLeave((to, from) => {
// 返回 false 阻止离开
return false
})
全局守卫
router.beforeEach((to, from, next) => { ... }) // beforeEach中不再强制调用next,可以直接返回值控制导航
router.afterEach((to, from) => { ... })
router.beforeResolve()
路由独享守卫
{
path: '/admin',
component: Admin,
beforeEnter: (to, from, next) => { ... }
}
动态路由
// 添加路由
router.addRoute({ path: '/new', component: New })
// 删除路由
router.removeRoute('route-name')
路由组件传参
{ path: '/user/:id', component: User, props: true }
// 组件通过props:['id'] 接收
4.x和3.x的区别
- 创建路由实例 new Router() 改为 createRouter()
- 组合式api支持,useRoute和useRouter
- 动态添加路由:router.addRoute() removeRoute()
路由权限控制
router.beforeEach((to, from, next) => {
if(to.meta.requireAuth && !isAuthenticated) next('/login');
else next();
});
或者动态添加路由,有权限的路由
// 动态添加路由(权限控制,可以后端返回)
const dynamicRoutes = [
{ path: '/admin', component: Admin, meta: { role: 'admin' } }
];
if (user.role === 'admin') {
dynamicRoutes.forEach(route => router.addRoute(route));
}
如何处理动态路由加载顺序问题
router.isReady().then(() => app.mount('#app'));
如何捕获导航错误
router.onError((error) => {
console.error('导航错误', error);
});
路由组件如何复用并响应参数变化
onBeforeRouteUpdate((to, form, next) => {
fetchData(to.params.id);
next();
})
导航守卫执行顺序
全局 beforeEach -> 路由 beforeEnter -> 组件 beforeRouteEnter -> 全局 beforeResolve -> 全局 afterEach -> 组件 beforeRouteUpdate
十二、在 Vue 3 中添加全局变量
1. 使用 app.config.globalProperties
这是 Vue 3 推荐的添加全局属性的方式:
// main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// 添加全局变量
app.config.globalProperties.$myGlobalVariable = '这是一个全局变量'
app.config.globalProperties.$apiUrl = 'https://api.example.com'
app.mount('#app')
在组件中使用:
// 在 setup 中使用
import { getCurrentInstance } from 'vue'
export default {
setup() {
const { proxy } = getCurrentInstance()
console.log(proxy.$myGlobalVariable)
}
}
// 在选项式 API 中直接使用
export default {
mounted() {
console.log(this.$myGlobalVariable)
}
}
2. 使用 provide/inject
Vue 3 提供了依赖注入系统,适合在组件树中共享数据:
// main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// 提供全局变量
app.provide('myGlobalVariable', '这是一个全局变量')
app.provide('apiUrl', 'https://api.example.com')
app.mount('#app')
在组件中使用:
import { inject } from 'vue'
export default {
setup() {
const myGlobalVariable = inject('myGlobalVariable')
const apiUrl = inject('apiUrl')
return {
myGlobalVariable,
apiUrl
}
}
}
3. 使用全局状态管理(如 Pinia)
对于更复杂的全局状态,推荐使用 Pinia(Vue 的官方状态管理库):
// stores/global.js
import { defineStore } from 'pinia'
export const useGlobalStore = defineStore('global', {
state: () => ({
myGlobalVariable: '这是一个全局变量',
apiUrl: 'https://api.example.com'
})
})
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
在组件中使用:
import { useGlobalStore } from '@/stores/global'
export default {
setup() {
const globalStore = useGlobalStore()
return {
myGlobalVariable: globalStore.myGlobalVariable,
apiUrl: globalStore.apiUrl
}
}
}
建议
- 对于简单的全局变量,使用
app.config.globalProperties或provide/inject - 对于需要响应式或复杂状态的全局数据,使用 Pinia
- 避免滥用全局变量,只在真正需要全局访问的数据上使用
- 为全局变量添加前缀(如
$)以避免命名冲突
十三、Vue3对比Vue2的改动
迁移
@vue/compat是一个vue3的构建版本,允许写vue2语法,然后逐步重构旧组件
生命周期
见第一节
Vue实例化
Vue2 用new Vue()
Vue3 用createApp
const app = createApp(App)
app.mount("#app")
// createApp做了两件事
//(1)创建vue实例
//(2)扩展实例中的mount方法,将根组件挂载到容器上
// mount:
// 创建vnode对象
// 渲染vnode
原因:
- 按需引入,不用把vue对象全部引入,更好的treeshaking支持
- 可以创建和使用多个vue实例
Vue2中的data
data为什么是个函数
每个组件都有个data,都应该是独立的互不影响的 使用函数可以在每次复用组件的时候都返回一个新的数据对象 使用对象的话就会多个组件用同一个对象的引用,导致互相影响
Vue3的模版编译优化
Patch Flags(动态节点标记) 静态节点提升(静态节点提升到渲染函数外部,减少重复创建) Block Tree(跳过静态子树对比)
架构设计与区别
-
proxy和defineProperty
- 初始化性能提升(对引用类型无需递归遍历,2是递归遍历给每个属性加上getter和setter)
- 解决对象动态属性/数组监听问题,vue2的机制只能监听指定对象的、指定属性的 getter 和 setter。所以当对象或数组新增属性时,vue2无法为新增的属性增加响应性。(vue2对于新增的属性用vue.set,dp是重写了数组的方法来达到push后的响应式效果。vue3是proxy直接代理对象)
- 内存占用更低
注:Vue.set方法内部做了以下工作:
- 如果目标是数组,使用 splice 方法添加元素
- 如果目标是对象,添加属性并触发依赖通知
- 如果目标不是响应式对象,直接添加属性
-
composition api和options api
- 改进了optionsapi的逻辑碎片化的缺点,将相关逻辑放在一起,按功能而非选项组织代码,提高可读性和可维护性。
- ts友好,更好的类型推断和类型安全。
-
ts重写
-
按需引入,treeshaking后减少40%体积
hooks
hooks是为了在函数式组件中能使用生命周期和状态管理还有一些其他特性,他提供一些方法,可以让你更方便的使用这些特性
fragment
支持多根节点
teleport
传送门,可以将元素移动到其他位置
<teleport to="#modal-container">
<div class="modal">模态框内容</div>
</teleport>
Suspense
用于组件异步加载
自定义渲染器API
import { createRenderer } from 'vue';
const { render } = createRenderer({ /* 自定义节点操作 */ })
十四、Vue中涉及的性能优化策略
1. 组件级别优化
v-once:静态内容只渲染一次v-memo:依赖变化时才重新渲染。v-memo="[value]",类似computed,精确控制子组件更新条件v-if/v-show:合理使用computed:合理使用异步组件defineAsyncComponent:按需加载非关键组件shallowRef/shallowReactive:非深度响应式,如大型对象/数组(如1000+条目的列表数据),避免深度监听大对象markRaw:永远不需要响应式的数据,const staticData = markRaw({ ... })
2. 列表渲染优化
<!-- 必须设置 key ,避免使用index作为key-->
<div v-for="item in list" :key="item.id">
{{ item.name }}
</div>
<!-- 避免 v-if 和 v-for 一起使用 -->
<!-- 不推荐 -->
<div v-for="item in list" v-if="item.visible">
3. 编译时优化
Vue3 的编译优化包括:
- 静态节点提升:减少重复创建
- Patch Flags:精准 DOM 差异比对,在虚拟DOM中标记动态绑定的类型(如class、style、props),减少Diff对比范围
- Block Tree:将模版划分为动态和静态区块,跳过静态子树对比,仅追踪动态区块的变化
- 缓存事件处理程序:如@click的时间处理函数会被缓存,避免重复生成
4. 资源加载优化
-
路由懒加载:分割代码块
-
Tree Shaking:消除未引用代码
// webpack.config.js module.exports = { optimization: { usedExports: true, // 启用 Tree Shaking }, } // Vite 默认支持 Tree Shaking,无需额外配置。 // rollup.config.js export default { // ... treeshake: true, // 启用 Tree Shaking // ... } -
预加载关键资源:提升首屏速度。
// 预加载首屏组件 <link rel="preload" as="script" href="/src/components/Critical.vue"> // 早于常规 <script>标签的加载 // 早于浏览器默认的资源发现机制 // 甚至在 CSSOM 构建完成前就会开始加载(除非遇到更高优先级的资源) -
静态资源:CDN加速
-
生产构建优化:
// vite.config.js export default { build: { minify: 'terser', // 代码压缩 brtliSize: true, // 压缩分析 chunkSizeWarningLimit: 1000 // 调整块大小警告 } }
5. 监控
webpack-bundle-analyzer来检查 Tree Shaking 是否按预期工作
Chorme DevTools Performance 面板 Vue DevTools 性能追踪 Lighthouse 性能评分
...