Vue3 的 Composition API 和 Options API 有哪些区别?举例说明 Composition API 的优势。
组合式api允许开发者更灵活地组合和重用逻辑,同时提高了可维护性和可读性。
选项api逻辑代码的分散
Composition API?它解决了什么问题?
Vue 3 中引入的一种新的 API,它提供了一种新的方式来组织和重用组件的逻辑。
解决问题:
- vue3引入
ref、reactive和computed等函数,Composition API 允许开发者将相关逻辑组合在一起,提高代码的可维护性和可读性 - vue2的逻辑分散在不同的生命周期钩子和属性中,难以维护和阅读
- 逻辑重用方面 Options API 依赖 Mixins,但 Mixins 存在命名冲突、来源不明等问题,vue3 而组合式API可以通过编写多个函数就很好的解决了
- 组合式API中见不到this的使用,减少了this指向不明的情况;
如何在 Vue3 中使用 ref 和 reactive?它们的区别是什么?
- ref 用于创建一个响应式的基本类型或对象引用。
- reactive 用于创建一个响应式的对象。
- 读取ref创建的数据,需要.value
ref底层还是使用reactive来做,ref是在reactive上进行了封装,增强了其能力,使其支持了对原始数据类型的处理;
Vue3 中的 <script setup> 是什么?它的作用是什么?
<script setup> 是一种编译时语法糖,可以在单文件组件(SFC)中更简洁地使用 Composition API。
- vue3.2后时候set up标签中的 所有属性和方法无需return,模板可直接使用;
- 引入组件的时候,会自动注册;
- 组件通信方面:使用
defineProps接收父组件传递的值; 使用useAttrs获取属性,useSlots获取插槽,defineEmits获取自定义事件; - 默认不会对外暴露任何属性,如果有需要使用
defineExpose;
<template>
<div>{{ count }}</div>
</template>
<script setup>
import { ref } from 'vue';
const count = ref(0);
</script>
Vue 3 中 setup 函数为何不能使用 this
1、tup 函数在组件实例创建之前执行
- 在组件初始化过程中,setup 是第一个被调用的函数
- 此时组件实例(this)尚未创建完成
- 在 setup 内部,this 指向 undefined
2、支持TypeScript类型推导和逻辑组合
// 组件初始化顺序
1. 调用 setup() // ← 此时 this 不存在
2. 创建响应式数据
3. 创建组件实例 // ← 此时 this 才被创建
4. 执行其他生命周期钩子
Vue3 的 Teleport 是什么?在什么场景下会用到?
Teleport 允许将组件的渲染内容移动到 DOM 的其他位置。常用于模态框、弹出菜单等需要脱离组件树布局的场景。
基本用法 to
<template>
<div>
<teleport to="body">
<div class="modal">This is a modal</div>
</teleport>
</div>
</template>
禁用 disabled
在某些场景下可能需要视情况禁用 <Teleport>
<Teleport :disabled="isMobile"> ... </Teleport>
多个 Teleport 共享目标
就是简单的顺次追加,后挂载的将排在目标元素下更后面的位置上
<Teleport to="#modals">
<div>A</div>
</Teleport>
<Teleport to="#modals">
<div>B</div>
</Teleport>
结果
<div id="modals">
<div>A</div>
<div>B</div>
</div>
延迟解析的 Teleport
Vue 3.5 及更高版本中,我们可以使用 `defer` prop 推迟 Teleport 的目标解析,直到应用的其他部分挂载。
<Teleport defer to="#late-div">...</Teleport>
<!-- 稍后出现于模板中的某处 -->
<div id="late-div"></div>
Vue3 中如何使用 provide 和 inject 实现依赖注入?它们的作用是什么?
- provide 用于在上级组件中提供数据,inject 用于在下级组件中注入数据,
- 适用于跨组件传递数据而无需通过 props 和 emits。
// 父组件
import { provide } from 'vue';
export default {
setup() {
provide('message', 'Hello from parent');
}
}
// 子组件
import { inject } from 'vue';
export default {
setup() {
const message = inject('message');
console.log(message); // Hello from parent
}
}
Vue3 中的 Suspense 是什么?如何使用它来处理异步组件?
- 用于处理异步组件加载,提供了一个优雅的加载状态处理机制。
- 使用defineAsyncComponent方法把动态导入的组件变成异步组件
- 在模板上包裹对应的组件,并配置好
default与fallback
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
<script>
import { defineAsyncComponent } from 'vue';
const AsyncComponent = defineAsyncComponent(() =>
import('./AsyncComponent.vue')
);
export default {
components: {
AsyncComponent
}
}
</script>
异步数据获取
<template>
<div class="suspense-data-demo">
<h2>Suspense 示例 - 异步数据获取</h2>
<Suspense @pending="onPending" @resolve="onResolve" @fallback="onFallback">
<template #default>
<UserData />
</template>
<template #fallback>
<div class="skeleton">
<div class="skeleton-avatar"></div>
<div class="skeleton-info">
<div class="skeleton-line"></div>
<div class="skeleton-line short"></div>
<div class="skeleton-line"></div>
</div>
</div>
</template>
</Suspense>
</div>
</template>
<script setup>
import { ref } from 'vue';
import UserData from './UserData.vue';
// Suspense 事件处理
const onPending = () => {
console.log('组件进入 pending 状态');
};
const onResolve = () => {
console.log('异步依赖已解析');
};
const onFallback = () => {
console.log('显示 fallback 内容');
};
</script>
<style scoped>
.suspense-data-demo {
max-width: 400px;
margin: 0 auto;
padding: 20px;
}
.skeleton {
display: flex;
gap: 16px;
padding: 20px;
}
.skeleton-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
.skeleton-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
}
.skeleton-line {
height: 16px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
border-radius: 4px;
}
.skeleton-line.short {
width: 60%;
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
</style>
在 setup 函数中使用 await 时,组件会自动成为异步依赖。
请解释 Vue3 中响应式系统的工作原理?
-
Vue 3 的响应式系统分为三个核心环节:依赖收集、触发更新和副作用管理
-
当我们用
reactive包装一个对象时,Vue 内部实际上是用 ES6 的 Proxy 把这个对象给包装了一层。Proxy 有拦截器get和set -
当我们的组件开始渲染,或者一个计算属性开始计算时,读取这些响应式数据就会触发 Proxy 的
get拦截器 -
在
get拦截器里,执行一个track 函数收集依赖,主要负责把副作用函数Effect 添加到依赖集合中 -
当数据发生变化时,或者修改了某个属性值就触发 Proxy 的
set拦截器,在set 拦截器中有个trigger函数, 找到所有依赖了这个数据的副作用函数Effect ,并通知它们重新执行。
1.响应式化阶段 - reactive/ref
// 核心:Proxy 代理
const proxy = new Proxy(target, {
get(obj, key) { /* 依赖收集 */ },
set(obj, key, value) { /* 触发更新 */ }
})
reactive:用 Proxy 包装对象,拦截所有操作ref:通过对象的.value属性包装原始值
2. 依赖收集阶段 - effect/track
当组件渲染或计算属性计算时:
- 执行副作用函数 (
effect) - 访问响应式数据触发
get拦截器 - 调用
track建立 数据属性 → 副作用函数 的映射关系
3. 触发更新阶段 - trigger
当数据变更时:
- 修改数据触发
set拦截器 - 调用
trigger查找所有依赖该数据的副作用函数 - 重新执行这些函数,完成视图更新
Proxy只会代理对象的第⼀层,那么Vue3⼜是怎样处理这个问题的呢?
- 判断当前Reflect.get的返回值是否为Object,如果是则再通过 reactive ⽅法做代理, 这样就实现了深度观测。
Proxy监测数组的时候可能触发多次get/set,那么如何防⽌触发多次呢?
- 我们可以判断key是否为当前被代理对象target⾃身属性,也可以判断旧值与新值是否相等,只有满⾜以上两个条件之⼀时,才有可能执⾏trigger。
Vue3中为什么采用 Proxy 代替 defineProperty
defineProperty :
-
劫持的是对象的属性,遍历每个属性添加
getter和setter,实现响应式,但是存在以下问题:-
检测不到对象属性的添加和删除;
-
数组API方法无法监听到;
-
需要对每个属性进行深度遍历,如果是嵌套对象,需要深层次监听,造成性能问题;
- 当调用
Object.defineProperty(obj, prop, descriptor)时,需要三个参数 obj:要在其上定义或修改属性的对象;prop待劫持(即定义或修改)的属性名;descriptor:描述该属性特性和行为的对象,包含如可枚举性、可配置性、可写性以及getter和setter;Object.defineProperty()是用来劫持(即定义或修改)对象的特定属性,赋予其特定的行为和特性,而不会直接影响对象本身或其他未被显式操作的属性;- 如果需要对对象的所有属性进行类似的操作,通常需要遍历对象的属性并分别调用
Object.defineProperty();
-
-
Proxy:
- 监听整个对象,那么整个对象的所有操作都会进入监听操作;
总结:
- vue3响应式系统初始化遍历所有的属性,如果是多层属性嵌套时候,只有访问属性时候才会递归下一层的属性 ,Object.defineProperty需要一次性递归所有属性
Object.defineProperty无法监听数组的索引和length属性,需要用7种方法重写;同时不支持 Map、Set、WeakMap 和 WeakSet等缺陷 ;Proxy直接劫持整个对象,能监听动态添加、删除属性操作Proxy可以单独的模块使用;- Proxy有13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等,这是Object.defineProperty不具备的;
Proxy 的 13 种拦截方法
1. 基础操作拦截
-
get(target, property, receiver)- 拦截操作: 读取属性,如
obj.prop或obj['prop'] - 参数:
target(目标对象),property(属性名),receiver(Proxy实例或继承它的对象) - 典型用途: 惰性加载、计算属性、访问校验、实现私有变量(通过判断property)
- 拦截操作: 读取属性,如
-
set(target, property, value, receiver)- 拦截操作: 设置属性值,如
obj.prop = value - 参数: 比
get多一个value(要设置的值) - 典型用途: 数据验证(如类型检查)、数据绑定/通知(Vue 3响应式核心)、设置私有属性时的报错
- 拦截操作: 设置属性值,如
-
has(target, property)- 拦截操作:
in运算符,如'prop' in obj - 典型用途: 隐藏对象上的某些属性,使其不被
in运算符发现
- 拦截操作:
-
deleteProperty(target, property)- 拦截操作:
delete运算符,如delete obj.prop - 典型用途: 防止某些重要属性被删除,或执行关联的清理操作
- 拦截操作:
2. 函数调用与构造拦截
-
apply(target, thisArg, argumentsList)- 拦截操作: 函数调用,如
proxy(...args)、proxy.call(...)、proxy.apply(...) - 参数:
target(必须是函数对象),thisArg(this上下文),argumentsList(参数列表) - 典型用途: 函数包装、日志记录、性能监控、实现函数节流/防抖
- 拦截操作: 函数调用,如
-
construct(target, argumentsList, newTarget)- 拦截操作:
new运算符,如new proxy(...args) - 参数:
target(目标类),argumentsList(参数列表),newTarget(最初被调用的构造函数,即Proxy本身) - 典型用途: 验证实例化参数、返回单例、自动绑定方法
- 拦截操作:
3. 原型与对象扩展拦截
-
getPrototypeOf(target)- 拦截操作:
Object.getPrototypeOf(proxy) - 典型用途: 返回一个伪造的原型对象
- 拦截操作:
-
setPrototypeOf(target, prototype)- 拦截操作:
Object.setPrototypeOf(proxy) - 典型用途: 锁定原型,禁止修改,或记录原型变更
- 拦截操作:
-
isExtensible(target)- 拦截操作:
Object.isExtensible(proxy) - 典型用途: 让一个本可扩展的对象看起来不可扩展
- 拦截操作:
-
preventExtensions(target)
- **拦截操作:** `Object.preventExtensions(proxy)`
- **典型用途:** 模拟 `preventExtensions` 的行为,或阻止其生效
4. 属性描述相关拦截
-
getOwnPropertyDescriptor(target, property)- 拦截操作:
Object.getOwnPropertyDescriptor(proxy, property) - 典型用途: 为不存在的属性返回一个描述符,或隐藏某些属性的真实描述符
- 拦截操作:
-
defineProperty(target, property, descriptor)- 拦截操作:
Object.defineProperty(proxy, property, descriptor) - 典型用途: 阻止定义某些属性,或验证属性描述符
- 拦截操作:
-
ownKeys(target)- 拦截操作:
Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy) - 典型用途: 过滤掉某些私有属性(如以下划线
_开头的),使其在for...in或Object.keys()中不可见
- 拦截操作:
Vue3 中如何创建和使用自定义指令?请举例说明。
自定义指令可以通过 app.directive 创建,并在组件中使用。
// 创建自定义指令
const app = createApp(App);
app.directive('focus', {
mounted(el) {
el.focus();
}
});
// 使用自定义指令
<template>
<input v-focus />
</template>
在 Vue3 中,如何使用 emits 选项来定义组件事件?它与 Vue2 的事件处理有何不同?
emits 选项用于显式定义组件可以发出的事件。相比于 Vue2 中的隐式事件,在 Vue3 中定义事件更加明确。
export default {
emits: ['custom-event'],
setup(props, { emit }) {
const triggerEvent = () => {
emit('custom-event');
}
}
}
如果你从 Vue 2 迁移到 Vue 3,建议:
- 使用
emits显式声明组件的事件。 - 对于复杂组件,使用对象语法对事件参数进行验证。
- 利用 Vue 3 的类型检查功能,提升代码的健壮性和可维护性。
示例对比
Vue 2:
export default {
methods: {
handleClick() {
this.$emit('my-event', 'Hello from child');
}
}
}
Vue 3:
<script setup>
// 数组形式声明(简单)
const emit = defineEmits(['update', 'submit'])
// 对象形式声明(带验证器)
const emit = defineEmits({
// 验证器函数
'update:value': (value) => {
return typeof value === 'string' || typeof value === 'number'
},
// 带多个参数的验证
'custom-event': (arg1, arg2) => {
return typeof arg1 === 'string' && typeof arg2 === 'number'
}
})
const handleEvent = () => {
emit('update')
emit('submit', { data: 'test' })
emit('update:value', '新值')
emit('custom-event', '字符串', 123)
}
</script>
TS语法:
<script setup lang="ts">
// 类型字面量形式 - 最常用的方式
const emit = defineEmits<{
(e: 'update'): void
(e: 'update:modelValue', value: string): void
(e: 'submit', payload: FormData): void
(e: 'change', value: string, index: number): void
}>()
// 或使用接口定义
interface Emits {
(e: 'update'): void
(e: 'update:modelValue', value: string): void
(e: 'submit', payload: FormData): void
}
const emit = defineEmits<Emits>()
// 触发事件
const handleSubmit = () => {
const formData: FormData = {
name: '张三',
age: 25
}
emit('submit', formData)
}
</script>
Vue3 中如何使用 v-model 在组件中实现双向数据绑定?请解释 v-model 的工作机制。
在 Vue 2 和 Vue 3 中,v-model 的使用方式有一些区别,尤其是在自定义组件中。此外,Vue 3.3 引入了 defineModel 宏,进一步简化了 v-model 的实现。以下是详细对比:
一、v-model 的区别
1. Vue 2 中的 v-model
在 Vue 2 中,v-model 是 v-bind:value 和 v-on:input 的语法糖,默认绑定到 value 属性和 input 事件。
在表单元素中使用:
<template>
<input v-model="message" />
<p>{{ message }}</p>
</template>
<script>
export default {
data() {
return {
message: ''
};
}
};
</script>
在自定义组件中使用:
Vue 2 中,自定义组件需要通过 model 选项显式定义 v-model 的绑定属性和事件。
<!-- 父组件 -->
<template>
<CustomInput v-model="message" />
</template>
<script>
export default {
data() {
return {
message: ''
};
}
};
</script>
<!-- 子组件 CustomInput.vue -->
<template>
<input :value="value" @input="$emit('input', $event.target.value)" />
</template>
<script>
export default {
props: ['value'],
model: {
prop: 'value', // 绑定的属性
event: 'input' // 触发的事件
}
};
</script>
- 默认绑定
value属性和监听input事件。 - 示例:
等价于:<MyComponent v-model="message" /><MyComponent :value="message" @input="message = $event" />
2. Vue 3 中的 v-model
在 Vue 3 中,v-model 的机制进行了改进,默认绑定到 modelValue 属性和 update:modelValue 事件。此外,Vue 3 支持多个 v-model 绑定。
在表单元素中使用:
与 Vue 2 相同。
<template>
<input v-model="message" />
<p>{{ message }}</p>
</template>
<script>
export default {
data() {
return {
message: ''
};
}
};
</script>
在自定义组件中使用:
Vue 3 中,自定义组件默认使用 modelValue 和 update:modelValue。
<!-- 父组件 -->
<template>
<CustomInput v-model="message" />
</template>
<script>
export default {
data() {
return {
message: ''
};
}
};
</script>
<!-- 子组件 CustomInput.vue -->
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
<script>
export default {
props: ['modelValue'],
emits: ['update:modelValue']
};
</script>
- 默认绑定
modelValue属性和监听update:modelValue事件。 - 示例:
等价于:<MyComponent v-model="message" /><MyComponent :modelValue="message" @update:modelValue="message = $event" />
多个 v-model 绑定:
Vue 3 支持在同一个组件上绑定多个 v-model。
<!-- 父组件 -->
<template>
<CustomInput v-model:firstName="firstName" v-model:lastName="lastName" />
</template>
<script>
export default {
data() {
return {
firstName: '',
lastName: ''
};
}
};
</script>
<!-- 子组件 CustomInput.vue -->
<template>
<input :value="firstName" @input="$emit('update:firstName', $event.target.value)" />
<input :value="lastName" @input="$emit('update:lastName', $event.target.value)" />
</template>
<script>
export default {
props: ['firstName', 'lastName'],
emits: ['update:firstName', 'update:lastName']
};
</script>
- 移除了
model选项,改为通过v-model的参数和修饰符实现自定义。 - 示例:
等价于:<MyComponent v-model:checked="isChecked" /><MyComponent :checked="isChecked" @update:checked="isChecked = $event" />
变化分析:
- Vue 3 通过参数化语法取代了
model选项,使得自定义v-model更加直观和灵活。 - 这种变化简化了 API,同时保持了功能的强大性。
二、defineModel 的区别
1. Vue 3.3 引入的 defineModel
defineModel 是 Vue 3.3 新增的一个宏,用于简化自定义组件中 v-model 的实现。它自动处理 props 和 emits,无需手动定义。
使用 defineModel:
<!-- 子组件 CustomInput.vue -->
<template>
<input v-model="model" />
</template>
<script setup>
const model = defineModel();
</script>
解释:
defineModel()返回一个响应式引用,可以直接绑定到模板中。- 它自动处理
modelValue属性和update:modelValue事件。
多个 v-model 绑定:
<!-- 子组件 CustomInput.vue -->
<template>
<input v-model="firstNameModel" />
<input v-model="lastNameModel" />
</template>
<script setup>
const firstNameModel = defineModel('firstName');
const lastNameModel = defineModel('lastName');
</script>
2. defineModel 的优势
- 简化代码:无需手动定义
props和emits。 - 类型安全:与 TypeScript 集成良好,支持类型推断。
- 更直观:直接返回一个响应式引用,逻辑更清晰。
三、Vue 2 和 Vue 3 的 v-model 对比
| 特性 | Vue 2 | Vue 3 | Vue 3 + defineModel |
|---|---|---|---|
| 默认绑定 | value 和 input | modelValue 和 update:modelValue | 自动处理 modelValue 和事件 |
| 自定义组件 | 需要 model 选项 | 需要手动定义 props 和 emits | 使用 defineModel 自动处理 |
多个 v-model | 不支持 | 支持多个 v-model 绑定 | 支持多个 v-model 绑定 |
| 代码简洁性 | 需要额外配置 model 选项 | 需要手动定义 props 和 emits | 最简洁,逻辑更清晰 |
| 类型支持 | 无 | 支持 TypeScript | 支持 TypeScript,类型推断更友好 |
四、总结
- Vue 2:
v-model基于value和input,自定义组件需要model选项。 - Vue 3:
v-model基于modelValue和update:modelValue,支持多个v-model绑定。 - Vue 3.3 +
defineModel:进一步简化v-model的实现,代码更简洁,类型支持更好。 如果你使用 Vue 3.3 或更高版本,推荐使用defineModel来简化v-model的实现。
请简述 Vue 3 相较于 Vue 2 的主要改进?
- 性能优化:
-
用 Virtual DOM 实现,提高了渲染性能。
-
引入编译器优化,减少了运行时开销。
-
打包体积减小有利于降低首屏加载时间
-
- 代码优化:
- 组合api允许开发者更灵活地组合和重用逻辑,同时提高了可维护性和可读性。
- 支持Ts 提供了更强大的类型推断和检查。
- 提供了一套自定义渲染器 API,使得开发者能够更容易地创建自定义渲染器或跨平台应用。
vue3与vue2有哪些不同
代码方面
-
proxy代替Object.definePrototety响应式系统
-
重写vdom,优化编译性能
-
增加了composition api(setup,ref,reactive),让代码更易于维护
-
v-model 用法
-
v-if优先级高于v-for- vue2: v-for比v-if优先
- Vue3: v-if比v-for优先
- 一般不建议v-if和v-for一起使用
-
生命周期变化- Vue2: beforeCreate、created、beforMount、munted、beforeUpdate、updated、beforeDestroy、destroyed;
- Vue3: setup、onBeforeMount、onMounted、onBeforeUpdate、onUpdated、onBeforeUnmount、onUnmounted
-
render函数默认参数createElement移除改为全局引入
-
组件事件现在需要在 emits 选项中声明
-
组件层面-
Vue3 templete支持多个根标签
Fragments -
Vue3 新增Teleport组件,将组件内部模板挂载到想挂的DOM上
-
Vue3 css支持v-bind 绑定变量
-
Vue3 新增异步组件 用defineAsyncComponent 声明
-
Vue3 新增宏:defineEmits、defineModel、defineProps
-
-
实例化上:- Vue2: new Vue
- Vue3: createApp
-
公共逻辑抽离- Vue2: mixin
- Vue3: hooks
-
diff算法优化: vue3 有静态标记 -
父组件监听子组件方法
- vue2: @hook:生命周期
- vue3: @vue:生命周期、同时也可以使用ref
14.路由的差异
* vue2: new VueRouter
* vue3: createRouter
打包方面
- 支持tree shaking :把无用的模块进行“剪枝”,很多没有用到的API就不会打包到最后的包
vue3在哪些方面提升了性能
通过响应式系统的重写、编译优化、源码体积的优化(按需加载)三个方面提升了性能。
1. 响应式系统提升
vue2在初始化的时候,通过Object.defineProperty对data的每个属性进行访问和修改的拦截,getter进行依赖收集、setter派发更新。在属性值是对象的时候还需要递归调用defineproperty。看下大致实现的代码:
function observe(target) {
if (target && typeof target === "Object") {
Object.keys(target).forEach((key) => {
defineReactive(target, key, target[key])
})
}
}
function defineReactive(obj, key, val) {
const dep = new Dep();
observe(val) // 如果属性值是对象就遍历它的属性
Object.defineProperty(obj, key, {
get() {
return val
},
set(v) {
val = v
dep.notify();
}
})
}
而如果属性是数组,还需要覆盖数组的七个方法(会改变原数组的七个方法)进行变更的通知:
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
ob.dep.notify()
return result
})
})
从这几段代码可以看出Object.defineProperty的几个缺点:
- 初始化时需要遍历对象所有key,层级多的情况下,性能有一定影响
- 动态新增、删除对象属性无法拦截,只能用set/delete api代替
- 不支持新的Map、Set等数据结构
- 无法监控到数组下标的变化(监听的性能代价太大)
所以在vue3中用了proxy全面代替Object.defineProperty的响应式系统。proxy是比较新的浏览器特性,拦截的是整个对象而不是对象的属性,可以拦截多种方法,包括属性的访问、赋值、删除等操作,不需要初始化的时候遍历所有属性,并且是懒执行的特性,也就是在访问到的时候才会触发,当访问到对象属性的时候才会递归代理这个对象属性,所以性能比vue2有明显的优势。
总结下proxy的优势:
-
可以监听多种操作方法,包括动态新增的属性和删除属性、has、apply等操作
-
可以监听数组的索引和 length 等属性
-
懒执行,不需要初始化的时候递归遍历
-
浏览器新标准,性能更好,并且有持续优化的可能
export function reactive(target: object) {
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers
)
}
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
const proxy = new Proxy(
target,
baseHandlers
)
proxyMap.set(target, proxy) // 用weakMap收集
return proxy
}
2. 编译优化(虚拟dom优化)
编译优化主要是通过重写虚拟dom。优化的点包括编译模板的静态标记、静态提升、事件缓存
标记(PatchFlag)
根据尤大直播所说,更新的性能提升1.32倍,ssr提升23倍。 在对更新的节点进行对比的时候,只会去对比带有静态标记的节点。并且 PatchFlag 枚举定义了十几种类型,用以更精确的定位需要对比节点的类型。
看这段代码
<div id="app">
<p>前端好好玩</p>
<div>{{message}}</div>
</div>
vue2编译后的渲染函数:
function render() {
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('p', [_v("前端好好玩")]), _c('div', [_v(
_s(message))])])
}
}
这个render函数会返回vnode,后面更新的时候vue2会调patch函数比旧vnode进行diff算法更新(在我的上篇文章有解析过),这时候对比是整个vnode,包括里面的静态节点<p>前端好好玩</p>,这样就会有一定的性能损耗。
vue3编译后的渲染函数:
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock("div", { id: "app" }, [
_createVNode("p", null, "前端好好玩"),
_createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
]))
}
只有_createVNode这个函数带有第四个参数的才是非静态节点,也就是需要后续diff的节点。第四个参数是这个节点具体包含需要被diff的类型,比如是text节点,只有{{}}这种模板变量的绑定,后续只需要对比这个text即可,看下源码中定义了哪些枚举的元素类型:
TEXT = 1,// 动态的文本节点
CLASS = 1 << 1, // 2,动态Class的节点
STYLE = 1 << 2, // 4,表示动态样式
PROPS = 1 << 3, // 8,动态属性
FULL_PROPS = 1 << 4, // 16 动态键名
HYDRATE_EVENTS = 1 << 5, // 32 带有事件监听器的节点
STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序的
KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性
UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key
NEED_PATCH = 1 << 9, // 512
DYNAMIC_SLOTS = 1 << 10, // 动态插槽
HOISTED = -1, // 静态提升的标记,不会被diff,下面的静态提升会提到
BAIL = -2 //
//位运算,有符号右移运算符,不了解的可以看这juejin.cn/post/688518…
静态提升
静态提升的意思就是把函数里的某些变量放到外面来,这样再次执行这个函数的时候就不会重新声明。vue3在编译阶段做了这个优化。还是上面那段代码,分别看下vue2和vue3编译后的不同
vue2:
function render() {
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('p', [_v("前端好好玩")]), _c('div', [_v(_s(message))])])
}
}
vue3:
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
const _hoisted_1 = { id: "app" }
const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "前端好好玩", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", _hoisted_1, [
_hoisted_2,
_createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
]))
}
可以看到vue3将不变的节点声明放到了外面去执行,后面再渲染的时候直接去_hoited变量就行,而vue2每次render都需要执行_c生成新的节点。这里还有一个点,_hoisted_2的_createVNode第四个参数-1,标记这个节点永远不需要diff。
事件缓存
默认情况下事件被认为是动态变量,所以每次更新视图的时候都会追踪它的变化。但是正常情况下,我们的 @click 事件在视图渲染前和渲染后,都是同一个事件,基本上不需要去追踪它的变化,所以 Vue 3.0 对此作出了相应的优化叫事件监听缓存
<div id="app">
<p @click="handleClick">前端好好玩</p>
</div>
vue3编译后:
import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"
const _hoisted_1 = { id: "app" }
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", _hoisted_1, [
_createVNode("p", {
onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.handleClick && _ctx.handleClick(...args)))
}, "前端好好玩")
]))
}
可以看到onClick有一个_cache判断缓存赋值的操作,从而变成静态节点
3. 源码体积的优化
vue3通过重构全局api和内部api,支持了tree shaking,任何一个函数,如ref、reavtived、computed等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小
watch 和 watchEffect 的区别?
watch:既要指明监听数据的源,也要指明监听的回调 watchEffect不用
watch 可以访问改变前后的值 watchEffect 只能获取改变后的值;
watch运行的时候 默认不会立即执行,值改变后才会执行,而watchEffect运行后可立即执行,这一点可以通过watch的配置项immeriate改变;
watchEffect 有点像 computed
computed注重的是计算出来的值(回调函数的返回值),所以必须写返回值;watchEffect注重的是过程(回调函数的函数体),所以不用写返回值;watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调;
| 属性 | watchEffect | watch | 说明 |
|---|---|---|---|
| 数据源 | 自动收集 | 显式指定 | watchEffect 自动追踪,watch 需要明确指定 |
| 回调参数 | (onCleanup) | (value, oldValue, onCleanup) | watch 提供新旧值 |
| immediate | ❌ 不支持 | ✅ 支持 | watch 可配置立即执行 |
| deep | ❌ 不支持 | ✅ 支持 | watch 可深度监听对象 |
| flush | ✅ 支持 | ✅ 支持 | 控制执行时机 |
| onTrack | ✅ 支持 | ✅ 支持 | 调试依赖追踪 |
| onTrigger | ✅ 支持 | ✅ 支持 | 调试依赖触发 |
| once | ❌ 不支持 | ✅ Vue 3.4+ | watch 可配置只执行一次 |
| 多数据源 | ❌ 不支持 | ✅ 支持 | watch 可监听多个数据 |
flush属性值
'pre':默认,组件更新前执 'post':DOM 更新后执行 'sync':同步立即执行在赋值语句后立即执行
const count = ref(0)
// flush: 'pre' - 默认,组件更新前执行
watch(count, (newVal) => {
console.log('pre - 组件更新前')
// 此时 DOM 还未更新
}, { flush: 'pre' })
// flush: 'post' - DOM 更新后执行
watch(count, (newVal) => {
console.log('post - DOM 更新后')
// 可以安全操作 DOM,访问更新后的 DOM
}, { flush: 'post' })
// flush: 'sync' - 同步立即执行
watch(count, (newVal) => {
console.log('sync - 同步立即执行')
// 在赋值语句后立即执行
}, { flush: 'sync' })
flush:'sync'和immediate:true 区别
immediate:true - 立即执行一次 + 变化时执行
flush:'sync':修改同步执行
watchPostEffect和watchSyncEffect 区别
是 Vue 3 提供的便捷 API,它们是 watchEffect 的特定 flush 时机的简写形式。
- watchPostEffect:等同于
watchEffect(..., { flush: 'post' }),在DOM更新后执行。
- 场景 1: 安全操作 DOM,因为更新已完成;获取dom的宽度
- 场景 2:确保 DOM 已渲染,然后初始化第三方库
- watchSyncEffect:等同于
watchEffect(..., { flush: 'sync' }),同步执行
场景 1: 紧急状态同步立即锁定界面,防止用户操作
场景 2: 实时表单验证同步验证,立即反馈
v-if和v-for哪个优先级高?
vue2中,v-for的优先级更高。vue3中,v-if的优先级更高。
无论2中还是3中,都不推荐一起使用 v-if 与 v-for。
vue3文档中推荐写法如下。 在外新包装一层 <template> 再在其上使用 v-for 可以解决这个问题 (这也更加明显易读):
<template v-for="todo in todos">
<li v-if="!todo.isComplete">
{{ todo.name }}
</li>
</template>
vue2和computed结合使用
介绍一下nextTick
- 为什么需要nextTick?
答:vue中数据是同步更新的,视图是异步更新。所有在我们同步代码中修改了数据,是无法访问更新后的DOM。所以官方就提供了nextTick。
-
vue中响应式数据改变,不会立即更新DOM,而是更新了vnode,等下一次事件循环才一次性去更新DOM
-
nextTick实现原理
##场景
在某些情况下,你可能需要获取更新后的 DOM 状态,比如元素的尺寸、位置等。使用 Vue.nextTick 可以确保你在 DOM 更新后获取到最新的状态。
this.someData = 'new value';
this.$nextTick(() => {
const element = document.getElementById('my-element');
const width = element.offsetWidth;
console.log('Element width:', width);
});
在组件更新后执行操作
在组件中,你可能需要在数据更新后执行一些操作,比如滚动到某个位置、聚焦某个输入框等。使用 Vue.nextTick 可以确保这些操作在组件更新后执行。
this.someData = 'new value';
this.$nextTick(() => {
this.$refs.myInput.focus(); // 聚焦输入框
});
避免重复更新
在某些情况下,你可能需要在数据更新后立即进行另一次数据更新。使用 Vue.nextTick 可以确保第一次更新完成后再进行第二次更新,避免不必要的重复更新。
this.someData = 'new value';
this.$nextTick(() => {
this.anotherData = 'another value'; // 在第一次更新完成后更新另一个数据
});
在测试中使用
在编写单元测试时,你可能需要等待 Vue 完成 DOM 更新后再进行断言。使用 Vue.nextTick 可以确保在 DOM 更新后进行断言。
it('updates the DOM', done => {
vm.someData = 'new value';
Vue.nextTick(() => {
expect(vm.$el.textContent).toBe('new value');
done();
});
});
v-if 和 v-show 的区别
相同点:值:true || false;都是用来控制元素的显示和隐藏
不同点:
- v-if: 可以和v-else、v-else-if配合使用
- v-if为true: dom树上不会有该元素,不会渲染
- v-show为true: dom树上还存在元素,只是其display: none
怎么理解Vue的单向数据流?
单向数据流是指:数据在组件树中的流动方向,是从父组件流向子组件的。这个设计使得数据流更加可预测和易于调试,确保应用状态的一致性。
简单理解:父组件的状态对于子组件是只读的,子组件想改,只能通过事件的方式,通知父组件自己改。
父组件如何监听子组件生命周期?
- vue2 使用 @hook:mounted
- vue3 使用 @vue:mounted
- 自定义事件,在子组件生命周期中去执行 下面是vue3的写法
<template>
<h1 @click="send">Home 页面</h1>
<Text @vue:mounted="fn" />
</template>
<script setup>
import { onMounted, ref } from 'vue'
import Text from '../components/Text.vue'
const fn = () => {
console.log('Text mounted')
}
</script>
说一下watch和computed的区别
-
watch: 用于声明在数据更改时调用的侦听回调
- 在某个值发生改变时触发某些操作 如异步请求
- 不支持缓存,数据变化就执行回调
-
computed: 用于声明要在组件实例上暴露的计算属性。
- 复杂的计算,依赖某个、多个数据,计算出新数据
- 支持缓存,但是依赖数据一改变就会重写计算
- 必须有返回值
- 可以写get 和set
- 不能有异步操作,有异步操作是无意义的
watch怎么停止监听?
import { ref, watch } from 'vue'
const count = ref(0)
const soptWatch = watch(() => count.value, (newVal, oldVal) => {
console.log(newVal, oldVal)
})
soptWatch()
v-for中 key的作用?
用来标识列表中每个元素的唯一标识符。key 的存在可以帮助Vue有效地更新和渲染DOM。
Vue3中怎么访问实例?
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
const appContext = instance.appContext
Vue中怎么做全局错误监听?
app.config.errorHandler
// main.js
const app = createApp(App)
app.config.errorHandler = (err, vm, info) => {
console.log(err, vm, info)
// 错误日志上报
}
window.onerror = function (event) {};
// 未处理的promise错误
window.onunhandledrejection = function (event) {}
怎么监听子组件内的错误?
// 子组件
throw new Error("Error");
//父组件
import { onErrorCaptured } from 'vue'
// 监听到子组件错误,执行回调
onErrorCaptured((err) => {
console.log('error', err)
})
页面跳转时,滚动到指定模块?
- 在具体页面,mounted构子中滚动到某个模块(可以通过参数判断啥的)
scrollBehavior声明路由时,配置dom、top等属性,页面存在指定元素
vue-router怎么动态添加、删除路由?
import { useRouter } from 'vue-router';
const router = useRouter();
const addRoute = () => {
const newRoute = {
path: '/hello', name: 'hello', component: () => import('../components/HelloWorld.vue'), // 动态加载组件
};
router.addRoute(newRoute);
};
删除路由
router.removeRoute('xxx');
23. 介绍一下vuex?
vuex是官方提供全局状态管理工具(vue2),但是在vue3中更推荐使用Pinia。 vuex主要有五大特性:
state: 存储应用的全局状态,所有组件都可以访问getters: 类似computed,用于派生属性mutations: 唯一合理修改state数据的方法(同步)actions: 异步异步操作,然后通过提交 commit 来修改statemodules: 将状态和相关的 mutations、getters、actions 进行模块化管理,一般是页面级
SPA怎么优化首页白屏时间?
最终解决方案:SSR 服务端渲染
SPA: Single Page Application(单页应用),Vue 和 React都是。
Vue项目中,我们打完包生成dist时,如果没有指定多入口,就只会生成一个html文件,和一些js、css、图片等资源文件。html中并没有渲染元素,就一个id为app的div。页面上的元素都是通过请求js动态渲染上去。所有本身就挺慢的。
- **路由懒加载,**首屏只请求首页相关js和css
- 组件异步加载,首页先渲染首屏的资源,首屏下面的组件异步加载
- 尽量减少http请求,因为浏览器有最大并发数限制,可以利用http2多路复用
- 利用打包工具做代码拆分,
splitChunks - 代码体积压缩gzip,打印日志去除
- 图片压缩、图片存入CDN
说一下Vue3声明一个响应式数据的方式?
-
ref: 通过.value访问及修改 -
reactive: 直接访问、只能声明引用数据类型 -
computed: 也是通过.value,声明需要 传 get、set -
toRef: 类似ref的用法,可以把响应式数据的属性变成ref -
toRefs: 可以把响应式数据所有属性 转成一个个ref -
shallRef: 浅层的ref,第二层就不会触发响应式 -
shallReactive: 浅层的reactive,第二层就不会触发响应式 -
customRef: 自定义ref
vue3和vue2路由差异和使用场景
路由模式
new Router()和createRouter
vue3
import { createRouter } from 'vue-router'
const router = createRouter({
// ...
})
路由模式 mode 配置改为 history ,属性值调整为:
"history"=>createWebHistory()"hash"=>createWebHashHistory()"abstract"=>createMemoryHistory()
import { createRouter, createWebHistory } from 'vue-router'
// createWebHashHistory 和 createMemoryHistory (SSR相关) 同理
createRouter({
history: createWebHistory(),
routes: []
})
基础路径 base 被作为 createWebHistory 的第一个参数进行传递(其他路由模式也是一样):
import { createRouter, createWebHistory } from 'vue-router'
createRouter({
history: createWebHistory('/base-url/'),
routes: []
})
通信
二、组件通信
1.Props/Emits(父子通信)
props(父->子)
<!-- 父组件 -->
<Child :title="data" />
<!-- 子组件 -->
<script setup>
defineProps(['title'])
</script>
- 类型校验:
defineProps({ title:{ type:String, required:true } }) - 单项数据流:子组件不能直接修改props(需要通过emit通知父组件)。
emits(子->父)
<!-- 子组件 -->
<button @click="$emit('update', value)" ></button>
<!-- 父组件 -->
<Child @update="handleUpdate" />
- Vue3特性:
defineEmits(['update'])显式申明事件。
2.v-model双向绑定(语法糖进阶)
- 单值绑定
<!-- 父组件 -->
<Child v-model="message" />
<!-- 子组件 -->
<input :value="modelValue" @input="$emit('update:modelValue',$event.target.value)" >
<script setup>
defineProps(["modalValue"])
defineEmits(["update:modelValue"])
</script>
- 多值绑定
<Child v-model:title="title" v-model:content="content" />
3.ref/expose(父访问子组件)
- 模版引用
<!-- 父组件 -->
<Child ref="childRef" />
<script setup>
const childRef = ref(null);
// 访问子组件暴露的属性/方法
childRef.value.childmethod();
</script>
<!-- 子组件 -->
const childMethod = () => {};
defineExpose({ childMethod })
</script>
4.provide/inject(跨层级通信)
- 依赖注入
// 祖先组件
import { provide } from 'vue';
provide('theme', 'dark');
// 后代组件
import { inject } from 'vue';
// 第二个参数为默认值
const theme = inject('theme', 'light')
- 响应式数据
// 提供响应式数据
const count = ref(0);
provide('count', count);
5.事件总线(Event Bus)
- Vue3官方废弃
$on,推荐第三方库(如mitt)
// eventBus.js
import mitt from 'mitt';
export const emitter = mitt();
// 组件A(发布事件)
emitter.emit('refresh', data);
// 组件B(订阅事件)
emitter.on('refresh', (data) => { /* ... */ })
// 组件卸载时取消订阅
onUnmounted(() => emitter.off('refresh'))
vue2的EventBus是
import {EventBus} from './event-bus.js' // 引入事件中心
// 组件A(发布事件)
EventBus.$emit('addition', { num:this.num++ })
// 组件B(订阅事件)
EventBus.$on('addition', param => { this.count = this.count + param.num; })
6.状态管理(pinia)
- 替代Vuex的官方状态库
// store/counter.js
export const useCounterStore = defineStore('counter',{
state:()=>({ count: 0 }),
actions:{
increment() { this.count++; }
}
})
// 组件中使用
const store = useCounterStore();
store.increment();
7.属性透传($attr)
- 透传非props属性
<!-- 父组件 -->
<Child class="child-style" data-id="123" />
<!-- 子组件 -->
<div v-bind="$attrs"></div>
<script setup>
// 禁用自动继承
defineOptions({ inheritAttrs: false });
</script>
8.模版引用(Template Refs)
- 直接操作DOM
<template>
<input ref="inputRef" />
</template>
<script setup>
const inputRef = ref(null);
onMounted(() => inputRef.value.focus())
</script>
同名属性合并策略:Vue3中后绑定的属性会覆盖前面的,而Vue2会合并。
Vue 2 结果:
- 同名属性:组件数据覆盖 mixin 数据
- 不同名属性:合并保留
- 方法:组件方法覆盖 mixin 方法
Vue 3 结果:
data()返回的对象被完全覆盖,不是合并- 只有组件中定义的属性存在
- mixin 中的其他属性被丢弃
为什么ref需要.value?
- js 原始值不是响应式的,ref通过封装对象(
{ value:... })实现响应式 - 底层实现:
function ref(value) {
return {
__v_isRef: true,
// 依赖追踪
get value() { track(this, 'value'); return value; },
// 触发更新
set value(newVal) { value = newVal; trigger(this, 'value'); }
};
}
为什么 reactive 不需要 .value?
reactive 的工作方式
因为 reactive 创建的是 Proxy 对象,可以拦截属性的访问和修改
import { reactive } from 'vue';
const state = reactive({
count: 0,
user: { name: 'John' }
});
// 直接访问属性
state.count = 1;
state.user.name = 'Alice';
// 因为 reactive 创建的是 Proxy 对象
// 可以拦截属性的访问和修改
原理深入
ref的响应式实现
- 基本类型:通过
RefImpl类实现,而RefImpl类内部使用了 JavaScript 原生的 getter/setter 语法(不是直接调用Object.defineProperty)。 - 对象类型:内部转化为reactive代理
reactive的响应式实现
- 基于Proxy代理整个对象,递归处理嵌套熟悉
- 依赖收集:在get时调用track收集依赖
- 触发更新:在set时调用trigger通知更新
为什么解构reactive对象会失去响应性?
- 解构得到的是普通值,非响应式引用;使用toRefs转化为ref
插槽
父组件用v-slot:name或#name指定插槽名以及多个插槽
<Child>
//多个插槽
<template #header>头部内容</template>
<template #default>默认内容</template>
<template #footer>底部内容</template>
</Child>
或者在父组件动态插槽
<template>
<ChildComponent>
<template v-for="slotName in slotNames" #[slotName]>
<div :key="slotName">这是 {{ slotName }} 插槽的内容</div>
</template>
</ChildComponent>
</template>
<script setup>
const slotNames = ['header', 'body', 'footer'];
</script>
- 子组件:用
<slot name="header">定义具名插槽
<template>
<slot name="header"></slot>
<slot></slot> <!-- 默认插槽 -->
<slot name="footer"></slot>
</template>
provide/inject能否替代Vuex?
- 适用场景:跨层级但关系明确的组件。
- 局限性:不适合全局状态管理,无法跟踪状态变化历史。
如何实现自定义v-model修饰符?
- 通过modelModifiers判断修饰符存在,并调整数据逻辑。
<!-- CustomInput.vue -->
<template>
<input
:value="modelValue"
@input="handleInput"
type="text"
/>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
modelValue: String,
// 接收 model 修饰符
modelModifiers: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits(['update:modelValue'])
const handleInput = (event) => {
let value = event.target.value
// 处理各种自定义修饰符
if (props.modelModifiers.uppercase) {
value = value.toUpperCase()
}
if (props.modelModifiers.trim) {
value = value.trim()
}
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
emit('update:modelValue', value)
}
</script>
路由守卫的next作用,在Vue Router 4之前用法,和Vue Router 4 后用法 区别
核心区别总结
| 特性 | Vue Router 3(Vue 2) | Vue Router 4(Vue 3) |
|---|---|---|
| API 风格 | 回调函数风格 | 返回值风格 |
| next 参数 | 必须提供且调用 | 可选,逐渐被淘汰 |
| 控制方式 | 调用 next() 函数 | 返回特定值 |
| 异步处理 | 容易产生回调地狱 | 天然支持 async/await |
| TypeScript | 类型支持一般 | 完整的类型安全 |
Vue Router 3(Vue 2)的 next 用法 三种调用方式
必须调用 next(),否则路由导航会挂起
router.beforeEach((to, from, next) => {
// 1. next() - 放行导航
if (用户已登录) {
next()
}
// 2. next(path) - 重定向导航
else if (需要登录) {
next('/login')
}
// 3. next(false) - 取消导航
else if (表单未保存) {
const confirm = window.confirm('确定离开?')
if (!confirm) {
next(false)
}
}
})
Vue Router 4(Vue 3)的 next 用法
Router 4 next仍然支持传统写法,但是推荐不使用
router.beforeEach((to, from) => {
// 1. 放行导航
if (条件1) {
return true // 显式放行
// 或者不返回值(默认放行)
}
// 2. 取消导航
else if (条件2) {
return false // 取消导航
}
// 3. 重定向到路径
else if (条件3) {
return '/login' // 字符串路径
}
// 4. 重定向到路由位置
else if (条件4) {
return {
path: '/login',
query: { redirect: to.fullPath }
}
}
// 5. 传递错误
else {
return new Error('权限验证失败')
}
})
router.beforeEach((to, from, next) 的to,from next分别是什么
to - 目标路由对象
router.beforeEach((to, from, next) => {
console.log('目标路由:', to)
// to 包含的目标路由信息:
console.log('路径:', to.path) // "/user/123"
console.log('完整路径:', to.fullPath) // "/user/123?name=john"
console.log('名称:', to.name) // "user-profile"
console.log('参数:', to.params) // { id: "123" }
console.log('查询参数:', to.query) // { name: "john" }
console.log('哈希:', to.hash) // "#section"
console.log('重定向来源:', to.redirectedFrom) // 原始路由对象
})
from - 来源路由对象
router.beforeEach((to, from, next) => {
console.log('来源路由:', from)
// from 包含的来源路由信息(与 to 结构相同):
console.log('来源路径:', from.path) // "/home"
console.log('来源完整路径:', from.fullPath) // "/home?tab=1"
console.log('来源名称:', from.name) // "home"
console.log('来源参数:', from.params) // {}
console.log('来源查询参数:', from.query) // { tab: "1" }
})
next - 导航控制函数
router.beforeEach((to, from, next) => {
// 1. next() - 放行导航
next()
// 2. next(false) - 取消导航
// next(false)
// 3. next(path) - 重定向
// next('/login')
// 4. next(object) - 重定向到路由位置
// next({ path: '/login', query: { redirect: to.fullPath } })
// 5. next(error) - 传递错误 (Vue Router 4)
// next(new Error('权限验证失败'))
})
Vue2项目如何升级Vue3
- 使用`@/vue/compat`官方工具过渡
- 逐步替换废弃API(`$children,filters`等)
- 优先迁移新组件,逐步重构旧组件
Vite 和 Webpack 对比分析
Webpack: 打包优先 (Bundle-Based)
- 原理:在启动开发服务器之前,必须先打包构建整个项目。它会从入口文件(如
src/main.js)开始,分析所有模块依赖,生成一个或多个打包后的 Bundle 文件。 - 开发服务器 服务于这些已经打包好的文件。
- 问题:当项目规模变得非常大时,这个“打包构建”的过程会非常慢,导致冷启动时间很长。
Vite: 原生 ESM 优先 (Native-ESM-Based)
-
原理:利用浏览器原生的 ES 模块 (ESM) 支持。它不需要在启动时打包整个项目。
-
工作流程:
-
启动:Vite 只启动一个轻量级的静态文件服务器,几乎瞬间完成。
-
请求:当浏览器请求某个模块时(如
import App from './App.vue'),Vite 才会在服务器端对该模块进行按需编译和转换,然后返回给浏览器。 -
依赖 vs 源码:
- 依赖:使用
esbuild预构建,速度极快。 - 源码:按需编译,只编译当前页面需要用到的文件。
- 依赖:使用
-
Vite 架构特点
浏览器 ESM 指的是 ECMAScript Modules(ES 模块)在浏览器中的原生支持。
-
基于原生 ES Modules
- 开发环境不打包,直接使用浏览器ESM
- 按需编译,只编译当前需要的模块
- 利用浏览器缓存机制
-
开发服务器
- 使用 Koa 开发服务器
- 中间件方式处理请求
- 每个文件独立编译和缓存
-
构建工具
- 生产环境使用 Rollup 打包
- 统一的配置和插件系统
开发环境启动速度
Vite
- 冷启动:秒级(< 1秒)
- 只启动开发服务器,不打包
- 按需编译,初始编译量极小
Webpack
- 冷启动:10-30秒(大型项目)
- 需要构建完整的依赖图
- 打包所有入口文件和依赖
热更新速度(HMR)
Vite
- 毫秒级响应(50-100ms)
- 只重新编译修改的文件
- 基于ESM的精确更新
Webpack
- 秒级响应(1-3秒)
- 需要重新构建依赖图
- 整个模块链可能被重新编译
Vite 需要打包吗
1. 开发环境:不打包
在开发模式下 (npm run dev),Vite 的核心优势就是 「不打包」。
-
原理:它利用浏览器原生的 ES 模块 (ESM) 支持,直接将你的源码文件提供给浏览器。
-
流程:
- 你请求
http://localhost:5173。 - Vite 服务器瞬间启动,返回你的
index.html。 index.html里通过<script type="module" src="/src/main.js">加载你的入口文件。- 浏览器解析到
import { createApp } from 'vue'和import './style.css'时,会向 Vite 服务器发起新的请求。 - Vite 服务器在接收到这些请求时,才会 「按需」 对请求的模块进行转换(例如,将
.vue文件拆成三个部分,将TypeScript转换成JavaScript,将Sass转换成CSS),然后返回给浏览器。
- 你请求
开发环境不打包的好处:
- 极速启动:服务器启动时间与项目规模无关。
- 高效热更新:只更新你修改的那个文件,速度极快。
2. 生产环境:需要打包
当你运行 npm run build 时,Vite 会进行打包。
为什么生产环境需要打包?
-
性能问题 (最重要的原因)
- 大量的 HTTP 请求:一个中型项目可能有成千上万个模块。如果浏览器为每个模块都发起一个 HTTP 请求,巨大的网络延迟会彻底拖垮加载性能。
- 打包 可以将这些零散的文件合并成少数几个文件,极大地减少 HTTP 请求数。
-
兼容性
- 并非所有浏览器都对原生 ESM 有很好的支持,尤其是旧版本浏览器。打包工具可以进行语法降级和 Polyfill。
-
代码优化
- Tree Shaking:消除那些从未被使用的 "死代码"。
- 代码压缩:去除注释、空白符,缩短变量名,减小文件体积。
- 代码分割:将代码拆分成多个 chunk,实现按需加载,优化缓存策略。