Vue3 是 Vue.js 框架的第三个主要版本,于 2020 年正式 9 月正式发布。它在 Vue2 的基础上进行了全面重构,带来了更好的性能、更灵活的写法和更强大的功能,同时保持了 Vue 一贯的易用性。
Vue重构的背景
1. Vue2 架构的局限性
Vue2 自 2016 年发布后,虽成为主流前端框架,但随着应用复杂度提升,其内部架构逐渐暴露一些短板:
-
响应式系统缺陷
-
性能优化空间:Vue2 的虚拟 DOM diff 算法对大规模节点更新效率有限,编译时优化能力较弱,且组件实例初始化和更新的性能在复杂场景下有提升空间。
-
代码组织问题:复杂组件中,逻辑(如数据、方法、生命周期)按选项(
data/methods/mounted)拆分,而非按功能模块聚合,导致 “逻辑碎片化”,维护大型项目时可读性和复用性下降。
2. 前端技术生态的演进
前端技术发生显著变化,Vue 需要适应新趋势:
-
TypeScript 普及:TS 凭借类型安全、代码提示等优势成为大型项目标配,Vue 需强化 TS 支持以满足企业级开发需求。
-
函数式编程流行:React Hooks 证明了函数式组件在逻辑复用和代码组织上的优势,Vue 需探索更灵活的组件写法(如 Composition API)。
总结
随着 Vue 应用从中小型向大型企业级项目扩展,开发者对框架的工程化能力要求更高,Vue3 并非对 Vue2 的否定,而是在 “渐进式框架” 理念基础上的增强,解决历史遗留问题(如响应式缺陷),同时兼容 Vue2 的大部分语法(如 Options API),降低迁移成本。
Vue3 的发布是对 Vue2 局限性的针对性优化,也是对前端技术趋势(TS、函数式编程、高效构建)的主动适配,最终目标是让 Vue 能更好地支撑从中小型应用到大型企业级项目的全场景开发,同时保持其 “渐进式” 和 “易上手” 的核心优势。
Vue3解构?失去响应式怎么办?
一、 先明确:Vue3 中什么解构会失去响应式?
Vue3 中实现响应式的核心是 ref 和 reactive,二者解构后的表现不同,只有 reactive 包裹的对象直接解构,会丢失响应式;ref 本身不存在 “解构失活” 的核心问题(仅需注意 .value) 。
-
问题场景(
reactive直接解构) :reactive是通过「代理(Proxy)」实现响应式,仅对对象的属性访问 / 修改做了劫持。直接解构reactive对象,会把属性的值(原始值 / 普通引用)提取出来,脱离了 Proxy 代理的包裹,变成普通变量,自然失去响应式。import { reactive } from 'vue'; const user = reactive({ name: '张三', age: 28 }); // 直接解构:提取出普通变量,失去响应式 const { name, age } = user; name = '李四'; // 仅修改普通变量,不会触发视图更新,也不会更新 user 中的数据 -
无问题场景(
ref解构) :ref是通过「包裹对象」实现响应式,解构ref相关数据(如ref数组、ref组成的对象),提取出的仍是ref实例,只要通过.value操作,就仍保持响应式。import { ref } from 'vue'; const name = ref('张三'); const age = ref(28); const user = { name, age }; // 解构 ref 组成的对象:提取出的仍是 ref 实例,保留响应式 const { name: userName, age: userAge } = user; userName.value = '李四'; // 正常触发视图更新
二、 解构 reactive 失去响应式的 3 种解决办法
1. 推荐:使用 toRefs 转换后再解构(核心方案)
toRefs 会将 reactive 对象的所有属性,转换为对应的 ref 实例,解构后提取的是 ref 实例,保留响应式,需通过 .value 操作(模板中可省略 .value)。
import { reactive, toRefs } from 'vue';
const user = reactive({ name: '张三', age: 28 });
// 先转换为 ref 集合,再解构
const { name, age } = toRefs(user);
name.value = '李四'; // 正常触发响应式,视图更新,且 user.name 同步更新
2. 按需转换:使用 toRef 解构单个属性
若只需解构部分属性,无需转换全部,用 toRef 单独转换目标属性,更轻量化,同样保留响应式。
import { reactive, toRef } from 'vue';
const user = reactive({ name: '张三', age: 28, gender: '男' });
// 仅转换并解构 name 属性
const name = toRef(user, 'name');
name.value = '李四'; // 正常触发响应式,视图与 user.name 同步更新
3. 替代方案:直接引用 reactive 对象(不解构)
如果无需解构,可直接在组件中引用整个 reactive 对象,通过「对象。属性」的方式访问 / 修改,始终保持响应式,操作更简洁。
import { reactive } from 'vue';
const user = reactive({ name: '张三', age: 28 });
// 不解构,直接通过对象访问属性
user.name = '李四'; // 正常触发视图更新,无响应式丢失问题
总结
- 核心问题:
reactive对象直接解构会脱离 Proxy 代理,丢失响应式(ref解构无此问题); - 核心解决:优先用
toRefs(全量)/toRef(按需)转换后再解构,保留响应式; - 简便方案:无需解构时,直接引用
reactive对象访问属性,避免响应式丢失。
vue3的compensition常用API?
Vue3 的 Composition API 是为了更灵活地组织和复用组件逻辑而设计的,以下是常用的核心 API 及用途:
1. setup()
组件的入口函数,在 beforeCreate 钩子前执行,用于编写组合式逻辑。返回的对象会暴露给模板和其他选项式 API,也可返回渲染函数。
2. 响应式 API
- ref() :创建值类型的响应式数据(如数字、字符串),通过
.value访问 / 修改,模板中可直接使用(自动解包)。 - reactive() :创建引用类型的响应式对象 / 数组(深层响应式),不能直接解构(会丢失响应式)。
- computed() :创建计算属性,支持 getter/setter,依赖变化时自动更新。
- readonly() :将响应式数据转为只读,深层生效。
- watch() :监听响应式数据变化,支持监听单个 / 多个源、深度监听、立即执行。
- watchEffect() :自动追踪依赖,响应式数据变化时执行回调,无需明确指定监听源。
3. 生命周期钩子
组合式 API 中生命周期钩子以onXXX命名,需在setup()中使用:
onMounted():组件挂载后执行。onUpdated():组件更新后执行。onUnmounted():组件卸载后执行。onBeforeMount()/onBeforeUpdate()/onBeforeUnmount()等。
4. 依赖注入
- provide() :父组件提供数据,可跨层级传递。
- inject() :子组件接收父组件提供的数据。
5. 工具函数
- toRef() :从响应式对象中创建单个属性的 ref,保持响应式关联。
- toRefs() :将响应式对象的所有属性转为 ref,方便解构。
- isRef()/isReactive()/isReadonly() :判断数据类型。
- unref() :若值为 ref 则返回
.value,否则返回原值(val = isRef(val) ? val.value : val)。
6. 高级 API
- customRef() :自定义 ref,控制响应式数据的追踪和触发(如防抖处理)。
- provide/inject 配合 Symbol:避免依赖名冲突。
- watchPostEffect() :等同于
watchEffect的flush: 'post',DOM 更新后执行。
这些 API 的核心优势是将分散的逻辑按功能聚合,提升代码复用性和可维护性,尤其适合复杂组件的逻辑拆分。
迁移注意事项
- 逐步替换已废弃 API(如
Vue.extend改用defineComponent)。 - 调整生命周期钩子名称及使用方式。
- 使用
@vue/compat库进行渐进式迁移。
总结
Vue3 通过组合式 API、性能优化和现代语言特性,显著提升了开发效率和应用性能,但需注意其与 Vue2 的非兼容变更。对于新项目,推荐直接使用 Vue3;旧项目可评估成本后逐步迁移。
Vue3 相比 Vue2 有哪些核心改进?⭐️⭐️
-
- 响应式系统重构:用
Proxy替代Object.defineProperty,支持监听数组索引、对象新增 / 删除属性。
- 响应式系统重构:用
-
- Composition API:替代 Options API,按逻辑组织代码,解决复杂组件的 “逻辑碎片化” 问题。Vue3 的 Composition API 允许将相关逻辑封装到函数中,大幅提高代码复用性。
-
- 性能优化:渲染速度提升 55%+,内存占用减少 54%,得益于重写的虚拟 DOM 和编译时优化(静态节点标记)。
-
- 更小的体积:支持 Tree-shaking,未使用的功能会被打包工具剔除。生命周期、响应式 API 等均可按需导入,未使用的功能不会被打包(Tree-shaking 支持)。
-
- 更好的 TypeScript 支持:源码用 TS 重写,原生支持类型推导,开发体验更优。
-
- 新增特性:Teleport(组件瞬移)、Suspense(异步加载)、生命周期钩子变化、多根节点模板等。
模板语法增强
-
多根节点:Vue3 支持组件模板有多个根节点(Vue2 只能有一个)。
<template> <div>根节点1</div> <div>根节点2</div> </template> -
更灵活的指令:
v-model支持自定义修饰符,v-if与v-for优先级调整(v-if更高)。 -
插槽语法:用
v-slot替代slot和slot-scope,更统一。<!-- Vue3 插槽用法 --> <Child> <template v-slot:default="scope"> {{ scope.data }} </template> </Child>
生态与工具链
- Vue Router 4配合 Vue3 使用,支持 Composition API
- Pinia替代 Vue2 的 Vuex,作为官方状态管理库,简化了 API,原生支持 TypeScript,更轻量。
- ViteVue 作者开发的构建工具,替代 Webpack,启动速度极快(基于浏览器原生 ES 模块),支持热更新。
- Vue Devtools针对 Vue3 优化的调试工具,可查看组件结构、响应式数据和生命周期。
适合场景与迁移建议
- 新项目:优先使用 Vue3 +
<script setup>+ TypeScript,开发效率更高。适合大型项目、复杂逻辑应用,需高性能或跨平台渲染的场景 - 旧项目迁移:可通过
@vue/compat兼容层逐步迁移,先使用 Vue3 运行 Vue2 代码,再逐步替换为 Composition API。适合大型项目、复杂逻辑应用,需高性能或跨平台渲染的场景 - 小型项目:Vue3 的 Options API 仍可用(兼容 Vue2 写法),无需强制使用 Composition API。
Composition API 和 Options API 的区别是什么?为什么推荐使用 Composition API?
区别:
- Options API:按
data、methods、watch等选项划分代码,逻辑分散在不同选项中,复杂组件难以维护。 - Composition API:按逻辑关注点组织代码(如把 “表单验证” 相关的变量、方法、钩子放在一起),支持通过自定义 Hooks 复用逻辑。
优势:
- 更好的代码组织
- 更好的代码复用(Composition API 通过组合函数来实现逻辑复用,这些函数可以在多个组件之间共享和复用逻辑,自定义 Hooks 替代 mixins,避免命名冲突)。
- 更灵活的类型支持(适配 TypeScript)。因为
Composition API几乎是函数,会有更好的类型推断。
结论:
Composition API对tree-shaking友好,代码也更容易压缩- 函数式编程是趋势
Composition API中见不到this的使用,减少了this指向不明的情况- 如果是小型组件,可以继续使用
Options API,也是十分友好的
Vue3 中的 setup 函数是什么?它的执行时机和参数是什么?
-
作用:
setup是 Composition API 的入口函数,用于定义响应式数据、方法、生命周期等。 -
执行时机:在
beforeCreate之前执行,此时this为undefined(不能用this访问组件实例)。 -
参数:
props:组件接收的 props(响应式,需通过props.xxx访问,不能解构,否则丢失响应性)。context:包含attrs(非响应式属性)、slots(插槽)、emit(触发事件)。
简化写法:推荐用 <script setup> 语法糖(无需手动返回变量,自动暴露给模板)。
Vue3 生命周期钩子有哪些变化?如何使用?
- 变化:Vue3 生命周期钩子需从
vue中导入,命名更直观(改为onXxx),且不再依赖this。 生命周期钩子函数在setup()函数中使用。
使用示例:
import { onMounted, onUnmounted } from 'vue';
setup() {
onMounted(() => { console.log('组件挂载完成'); });
onUnmounted(() => { console.log('组件卸载'); });
}
| Vue2 生命周期 | Vue3 生命周期(Composition API) |
|---|---|
beforeCreate | 不需要(setup 函数替代) |
created | 不需要(setup 函数替代) |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeDestroy | onBeforeUnmount |
destroyed | onUnmounted |
Vue3 如何实现组件通信?
-
Props / Emits:父传子用
props,子传父用emit(Vue3 需在emits选项中声明事件)。<!-- 子组件 --> <script setup> const emit = defineEmits(['change']); // 声明事件 emit('change', '数据'); // 触发事件 </script> -
provide/inject:跨层级通信(父组件提供数据,子孙组件注入数据,嵌套组件常用)。// 父组件提供 import { provide } from 'vue'; provide('theme', 'dark'); // 子组件注入 import { inject } from 'vue'; const theme = inject('theme'); -
Pinia:替代 Vuex 的状态管理库,用于全局状态共享(更简洁,支持 TypeScript)。
-
v-model双向绑定:Vue3 支持多个v-model,且语法更灵活(如v-model:title)。
什么是 Teleport?它的使用场景是什么?
-
Teleport(瞬移) :允许将组件的 DOM 结构 “瞬移” 到页面的任意位置(如
body下),解决样式嵌套问题。 -
场景:模态框(Modal)、弹窗(Popup)等组件,避免因父元素样式(如
overflow: hidden、z-index)导致的显示异常。 -
示例:
<teleport to="body"> <div class="modal">这是一个模态框</div> </teleport>
Suspense 是什么?如何使用?
-
Suspense:用于管理异步组件的加载状态,在组件加载完成前显示 “加载中” 内容。
-
使用条件:配合异步组件(
defineAsyncComponent)或返回 Promise 的setup函数。 -
示例:
<Suspense> <template #default> <AsyncComponent /> <!-- 异步组件 --> </template> <template #fallback> <div>加载中...</div> <!-- 加载状态 --> </template> </Suspense>
Vue3 和 Vue2 的 v-model 有什么区别?如何用 defineModel简化代码?
-
Vue2:一个组件只能有一个
v-model,默认绑定value属性和input事件,如需自定义需用model选项。 -
Vue3:
- 支持多个
v-model(如v-model:title、v-model:content)。 - 默认绑定
modelValue属性和update:modelValue事件,更直观。 - 无需
model选项,直接通过v-model:propName绑定。
- 支持多个
示例:
<!-- 父组件 -->
<Child v-model:name="username" v-model:age="userAge" />
<!-- 子组件 -->
<script setup>
const props = defineProps(['name', 'age']);
const emit = defineEmits(['update:name', 'update:age']);
// 更新值
emit('update:name', '新名字');
</script>
Vue3.4 新增的 defineModel 宏是对 v-model 实现的语法糖,无需手动声明 props 和 emit,直接创建一个双向绑定的响应式变量,大幅减少代码量。
用法步骤:
-
在子组件中使用
defineModel创建响应式变量该变量自动关联父组件的v-model,修改变量会自动触发更新事件:<!-- 子组件(使用 defineModel) --> <script setup> // 无需声明 props 和 emits,直接创建双向绑定变量 const name = defineModel('name') // 对应 v-model:name const age = defineModel('age') // 对应 v-model:age </script> <template> <!-- 直接绑定变量,修改时自动同步到父组件 --> <input v-model="name" /> <input v-model="age" /> </template> <template> <input :value="name" @input="emit('update:name', $event.target.value)" /> <input :value="age" @input="emit('update:age', $event.target.value)" /> </template> -
父组件使用方式不变仍通过
v-model:属性名绑定数据:<!-- 父组件 --> <script setup> import { ref } from 'vue' const username = ref('张三') const userAge = ref(18) </script> <template> <ChildComponent v-model:name="username" v-model:age="userAge" /> </template>
核心优势
- 减少模板代码:无需手动声明
props和emit,省去update:xxx事件的触发逻辑。 - 更直观的响应式:
defineModel返回的变量直接是响应式的,修改后自动同步父组件,与普通ref用法一致。 - 兼容原有逻辑:可通过
defineModel(属性名, { default: 默认值 })设置默认值,保持灵活性。
通过 defineModel,Vue3 的双向绑定实现变得更加简洁,尤其适合需要多个 v-model 的场景。
Pinia 和 Vuex 有什么区别?为什么推荐用 Pinia?⭐️
-
Pinia 是 Vue3 官方推荐的状态管理库,替代 Vuex,主要区别:
- API 更简洁:无需
mutations(直接通过actions修改状态),减少模板代码。 - 更轻量:体积比 Vuex 小。
- 原生支持 TypeScript:类型推导更友好,无需手动定义类型。
- 无需嵌套模块:通过组合式存储(
stores)替代模块,结构更扁平。
- API 更简洁:无需
怎么理解Vue3提供的markRaw?
markRaw 是 Vue3 提供的一个工具函数,作用很直接:标记一个对象,让它永远不会被 Vue 的响应式系统处理(即不会变成响应式对象) 。
核心理解
Vue3 的响应式系统会通过 Proxy 对对象进行代理,从而实现属性监听和依赖更新。但有些场景下,我们不希望某个对象被 “响应式化”,比如:
- 第三方库的实例(如 Chart.js、地图库的实例),被响应式处理可能导致库内部逻辑异常;
- 大型数据对象(如海量列表数据),代理会带来额外性能开销,且无需响应式;
- 需要避免响应式循环引用导致的问题。
这时就可以用 markRaw 标记该对象,它会 “告诉” Vue 响应式系统:“这个对象不用管,跳过它”。被标记后的对象:
- 不会被
reactive、ref等 API 转为响应式; - 其属性变化也不会触发组件更新(因为没有被代理,无法收集依赖)。
简单说,markRaw 就是给对象加了个 “免死金牌”,让它彻底脱离 Vue 响应式的管控,适用于明确不需要响应式的场景。
Vue3的响应式库是独立出来的,如果单独使 用是什么样的效果?
Vue3 的响应式系统(基于 Proxy 的 reactivity 模块)被设计为独立模块,可脱离 Vue 组件单独使用。单独使用时,能实现对普通 JavaScript 对象 / 值的响应式追踪,即当数据变化时,自动触发依赖该数据的回调函数(类似一个轻量级的 “数据监听工具”)。
单独使用的核心效果
-
数据响应式化通过
reactive(对象)或ref(基础类型)将普通数据转为响应式数据,修改数据时会自动触发依赖更新。示例:import { reactive, ref, effect } from '@vue/reactivity'; // 1. 响应式对象(reactive) const user = reactive({ name: '张三', age: 18 }); // 2. 响应式基础类型(ref) const count = ref(0); -
依赖追踪与自动触发通过
effect函数注册 “依赖回调”,当响应式数据变化时,回调会自动执行。示例:// 注册依赖:当 user.name 或 count 变化时,自动执行回调 effect(() => { console.log(`姓名:${user.name},计数:${count.value}`); }); // 修改数据,触发回调 user.name = '李四'; // 控制台输出:姓名:李四,计数:0 count.value = 1; // 控制台输出:姓名:李四,计数:1 -
脱离 Vue 组件独立运行无需 Vue 组件、模板或虚拟 DOM,仅通过上述 API 即可实现 “数据驱动逻辑”,适合非 UI 场景(如数据处理、状态管理)。
适用场景
- 非 Vue 项目的状态管理:在 React、原生 JS 项目中实现简单的响应式状态。
- 数据监听工具:监听对象属性变化,自动执行日志、验证等逻辑。
- 独立的状态逻辑库:封装业务逻辑时,用响应式管理状态依赖。
总结
单独使用 Vue3 的响应式库,能获得一套轻量、高效的 “数据 - 依赖” 管理机制,核心价值是自动追踪数据变化并触发关联逻辑,且用法简洁,可灵活嵌入任何 JavaScript 环境。
Vue 3 中的其他相关特性
-
readonly()和shallowReactive():readonly()是如何限制数据修改的?它是如何影响响应式系统的?shallowReactive()是什么?如何使用它避免对嵌套对象的深度代理?
-
toRefs()和toRaw():toRefs()如何将响应式对象转换为普通的引用,方便解构?toRaw()是如何返回原始对象的?何时使用toRaw()?
一、readonly ():只读响应式
1. 如何限制数据修改?
readonly() 基于 Proxy 实现,核心是拦截所有修改操作并抛出警告,同时保留依赖收集能力:
- 拦截
set/deleteProperty等修改操作,执行时直接抛出 “数据只读” 的警告,不执行实际修改; - 对
get操作正常拦截,保留依赖收集(保证读取时能触发响应式更新); - 嵌套对象会被递归转为只读代理(可通过
shallowReadonly()关闭递归)。
2. 对响应式系统的影响
- 仅禁止修改,不影响依赖收集 / 触发:读取
readonly数据仍会收集依赖,当原始响应式数据变化时,readonly数据也会同步更新并触发视图刷新; - 场景:保护全局常量、组件传入的 props(避免子组件修改父组件数据)。
二、shallowReactive ():浅层响应式
1. 核心定义
shallowReactive() 是 reactive() 的浅层版本,仅代理根对象的属性,嵌套对象不递归创建 Proxy,修改嵌套对象属性不会触发响应式更新。
2. 如何避免深度代理?
- 初始化时仅对根对象创建 Proxy,
get拦截嵌套对象属性时,直接返回原始对象(不调用reactive()递归代理); - 仅根属性的
set/get会触发依赖收集 / 更新,嵌套对象的操作无响应式效果; - 使用场景:仅需修改根属性的浅层数据(如表单临时状态),减少深层代理的性能开销。
三、toRefs ():响应式对象转引用
1. 核心逻辑
toRefs() 遍历响应式对象的所有属性,为每个属性创建对应的 ref 对象,这些 ref 与原对象属性双向绑定:
- 解构
toRefs()返回的对象时,得到的是ref而非普通值,保留响应式; - 修改
ref.value会同步更新原响应式对象,反之亦然。
2. 使用示例
const state = reactive({ a: 1, b: 2 });
const { a, b } = toRefs(state); // a、b 是 ref 对象
a.value = 3; // state.a 同步变为 3
四、toRaw ():获取原始对象
1. 核心实现
Vue 3 会为响应式对象(reactive/readonly)缓存原始对象,toRaw() 直接返回该原始对象:
- 内部通过访问响应式对象的私有属性(如
__v_raw)获取原始对象; - 返回的原始对象非代理对象,修改不会触发响应式更新。
2. 使用场景
- 避免响应式代理的性能开销(如大数据遍历);
- 临时修改数据但不想触发视图更新;
- 注意:仅能还原由 Vue 创建的响应式对象,普通对象调用无效果。
核心总结
| API | 核心逻辑 | 关键特性 | 适用场景 |
|---|---|---|---|
| readonly() | Proxy 拦截修改操作抛警告,保留依赖收集 | 只读、深度代理、同步原始数据更新 | 保护 props / 全局常量 |
| shallowReactive() | 仅代理根对象,嵌套对象返回原始值 | 浅层响应式、性能更优 | 仅操作根属性的浅层数据 |
| toRefs() | 遍历响应式对象,为属性创建绑定的 ref | 解构保留响应式、双向同步 | 解构 reactive 对象 |
| toRaw() | 返回响应式对象的原始缓存对象 | 非响应式、修改不触发更新 | 大数据操作、临时修改不更新视图 |
关键点回顾
readonly()只禁改、不禁读,依赖收集正常;shallowReactive()只代理根层,嵌套无响应式;toRefs()解决 reactive 解构丢失响应式问题,toRaw()用于获取原始对象规避响应式开销;- 四个 API 均基于 Proxy 实现,核心是通过拦截行为的差异化,适配不同的响应式需求。
TypeScript 在 Vue 3 中的使用
问题:
- Vue 3 如何支持 TypeScript?
- Vue 3 中如何使用
defineComponent来提升类型推导? - Vue 3 中如何在组件中使用强类型的
props、data和emits?
考察点:
- Vue 3 提供了更好的 TypeScript 支持,
defineComponent可以帮助组件类型推导,ref()和reactive()等 API 都支持类型推导。 props、data和emits都可以在组件中指定类型,从而帮助静态检查和自动补全。
Vue Router 和 Vuex 适配
问题:
- Vue Router 4 和 Vuex 4 相比 Vue 2 版本有什么变化?
- 如何在 Vue 3 中配置 Vue Router,支持嵌套路由和动态路由?
- Vuex 4 是否和 Vue 2 版本兼容?如何在 Vue 3 中使用 Vuex 进行状态管理?
考察点:
- Vue Router 4:支持 Vue 3,提供更强大的路由功能,支持嵌套路由、路由守卫等功能。
- Vuex 4:与 Vue 3 兼容,采用 Composition API 的方式组织状态管理,支持模块化和插件。
性能优化与懒加载
问题:
- Vue 3 中有哪些性能优化特性?
- 如何在 Vue 3 中使用懒加载优化组件?
- Vue 3 如何通过
Suspense和defineAsyncComponent优化异步加载组件的性能?
考察点:
- 性能优化:Vue 3 在响应式系统、虚拟 DOM 等方面进行了优化,提升了性能。
- 懒加载:使用
defineAsyncComponent和Suspense使得异步组件的加载更加高效,避免了在应用启动时加载所有组件。
测试与调试
问题:
- Vue 3 中如何使用 Vue DevTools 调试组件?
- 如何为使用 Composition API 的 Vue 3 组件编写单元测试?
- Vue 3 是否有更好的错误提示和调试支持?
考察点:
- Vue DevTools:支持 Vue 3 的调试,可以查看组件的状态、事件和路由信息。
- 单元测试:对于 Vue 3 组件,可以使用
@vue/test-utils进行单元测试,特别是结合 Composition API 时,测试和调试的工具更为丰富。
为什么计算属性有缓存?它是如何工作的?
一、为什么计算属性有缓存?
计算属性设计缓存的核心目的是避免重复计算,提升渲染性能:如果没有缓存,模板中每次访问计算属性(比如多次渲染同一计算属性),都会重新执行函数逻辑;而有缓存后,仅当计算属性依赖的响应式数据变化时,才会重新计算,否则直接返回缓存结果。比如一个依赖 list 的 total 计算属性,页面渲染 10 次,只要 list 不变,total 只会计算 1 次。
二、缓存的工作原理(简单版)
-
初始化阶段:定义计算属性时,Vue 会收集它的依赖项(比如计算属性函数中用到的
data/props里的响应式数据),并为计算属性创建一个 「缓存容器」和「依赖标记」。 -
访问阶段:
- 第一次访问:执行计算函数,把结果存入缓存容器,返回结果;
- 后续访问:先检查依赖项是否变化,若未变化,直接返回缓存结果;若变化,重新执行函数、更新缓存并返回新结果。
-
依赖更新阶段:当计算属性依赖的响应式数据发生变化时,Vue 会标记该计算属性的缓存「失效」,下次访问时就会重新计算。
是否可以禁用计算属性的缓存?
- 无直接禁用缓存的开关,但可通过方法替代计算属性 实现无缓存效果;
- 核心逻辑:计算属性 = 依赖缓存 + 惰性计算,方法 = 无缓存 + 即时计算,按需选择即可。
响应式依赖
问题:
- 计算属性的依赖是如何追踪的?
- 如果计算属性依赖的数据是嵌套的对象,如何确保其变化能被捕捉到?
考察点:
-
依赖追踪:
- Vue 在计算属性首次访问时,记录依赖的响应式数据。Vue 的依赖追踪系统会检测计算属性中用到的响应式数据,当数据未发生变化时,返回缓存的结果。
-
嵌套对象:
- 对于嵌套的对象属性,确保其也是响应式的(如通过
Vue.set或ref定义)。
- 对于嵌套的对象属性,确保其也是响应式的(如通过
问题:
- 计算属性和方法(
methods)的区别是什么? - 如果一个功能可以使用计算属性和方法实现,应该如何选择?
考察点:
-
计算属性:结果会基于依赖的变化进行缓存,多次访问时不会重复计算。 方法:每次调用都会重新执行逻辑,不会缓存结果。 如果逻辑较复杂且结果需要频繁访问,推荐使用计算属性。 如果逻辑不需要缓存,或者结果需要动态变化(如每次随机生成一个值),则使用方法。
-
常见应用:
- 数据格式化:如时间、货币格式化。
- 动态显示逻辑:如按钮状态、表单校验结果。
- 复杂数据派生:如过滤或排序列表。
-
性能问题解决:
- 优化依赖。
- 使用分解逻辑的多个计算属性代替复杂的大型计算属性。
computed怎么实现的缓存 ⭐
一、computed 缓存的核心实现逻辑(极简版)
Vue 中 computed 的缓存本质是依赖收集 + 状态标记,核心分 3 步,用通俗的逻辑就能理解:
1. 初始化:收集依赖 + 标记 “未计算”
定义 computed 时,Vue 会创建一个 「计算属性对象」,包含:
value:缓存结果的容器;dirty:标记是否需要重新计算(初始为true,表示 “未计算,需要执行函数”);- 依赖收集器:记录 computed 函数中用到的响应式数据(比如
data里的count)。
2. 首次访问:执行计算 + 缓存结果
第一次访问 computed 时:
- 因为
dirty: true,执行 computed 函数,把结果存入value; - 把
dirty改为false(标记 “已缓存,无需重新计算”); - 返回
value。
3. 后续访问 / 依赖更新:判断是否重算
- 普通访问:若
dirty: false,直接返回缓存的value,不执行函数; - 依赖数据变化:当 computed 依赖的响应式数据(如
count)更新时,Vue 会触发依赖通知,把dirty改回true; - 再次访问:因
dirty: true,重新执行函数、更新value、重置dirty: false,返回新结果。
核心总结
- 缓存的核心是
dirty标记:依赖不变则dirty: false,复用缓存值;依赖变化则dirty: true,重新计算; - 依赖收集是基础:Vue 能精准知道 “哪些数据变了需要让 computed 重算”,避免无意义的更新;
- 本质:用「一次计算 + 多次复用」替代「每次访问都计算」,核心提升性能。
计算属性的优点和局限性是什么
一、计算属性的核心优点
- 性能优化:自带缓存,仅依赖项变化时重新计算,避免模板中重复执行复杂逻辑(比如多次渲染同一计算属性,只需计算一次);
- 代码整洁:把模板中的复杂计算逻辑抽离,让模板更简洁(比如
{{ totalPrice }}比{{ list.reduce(...) }}更易读); - 响应式联动:自动关联依赖的响应式数据,数据变化时计算结果实时更新,无需手动监听。
二、计算属性的主要局限性
- 缓存限制:缓存可能导致依赖「非响应式数据」(如
new Date()、随机数)时,结果无法实时更新; - 只读性(默认) :Vue2 中计算属性默认只有 getter,需手动配置 setter 才能修改;Vue3 中虽可直接写,但本质还是依赖响应式数据,无法脱离依赖独立赋值;
- 异步不友好:计算属性函数必须同步执行,无法直接返回异步结果(比如不能在计算属性里写
await请求接口);
watch 和 computed 的区别
问题:
watch和计算属性(computed)的区别是什么?各自适用于什么场景?- 是否可以用
watch替代computed,或者反之?
考察点:
-
区别:
computed:声明式地计算新值,并缓存结果;适合派生出新的数据。watch:命令式地监听数据变化并执行副作用逻辑。
-
适用场景:
- 如果只是处理数据并生成新值,优先使用
computed。 - 如果需要执行异步任务或副作用逻辑(如 API 请求、计时器),适合使用
watch。
- 如果只是处理数据并生成新值,优先使用
深度监听(Deep Watch)
问题:
- 如何监听嵌套对象或数组的变化?
- 深度监听可能带来哪些性能问题?如何避免?
考察点:
-
深度监听的语法:
-
设置
deep: true:watch: { nestedObject: { handler(newVal) { console.log('Nested object changed:', newVal); }, deep: true } }
-
-
性能问题:
- 深度监听会递归遍历对象的每个属性,监听所有层级的变化,可能导致性能开销。
-
优化建议:
- 根据具体需求设计监听逻辑,避免不必要的深度监听。
- 在特定场景下,使用手动监听某些属性的方式替代深度监听。
问题:
immediate属性的作用是什么?- 在什么场景下需要使用
immediate?
考察点:
-
定义:
-
immediate: true会让 watch 回调在监听初始化时立即执行一次,而非等待数据源首次变化后才执行。 -
注意:首次执行时,
oldValue为undefined(Vue 2/3 一致)。 -
immediate解决的核心问题:避免重复编写 “初始化执行 + 监听变化” 的冗余代码 -
示例:
import { ref, watch } from 'vue'; const count = ref(0); // 配置 immediate: true,初始化立即执行回调 watch( () => count.value, (newVal, oldVal) => { console.log('count 变化:', newVal, '旧值:', oldVal); // 初始化执行时:newVal=0,oldVal=undefined // count 变化时:newVal=新值,oldVal=旧值 }, { immediate: true } );
-
-
使用场景:
- 初始化阶段需要根据数据执行某些逻辑(如根据初始值发起请求或设置页面状态)。
性能问题与优化
问题:
watch是否有性能隐患?如何优化?- 如果需要监听大量数据或频繁变化的数据,如何避免性能问题?
考察点:
-
潜在性能隐患:
- 深度监听可能导致过多的资源消耗。
- 频繁触发的回调函数可能影响应用性能。
-
优化方法:
- 避免不必要的深度监听。
- 使用节流或防抖优化回调函数:
复制 watch: { inputValue: _.debounce(function (newVal) { console.log('Value changed:', newVal); }, 300) }
侦听器的生命周期
问题:
- 侦听器何时会初始化?
- 如何在组件销毁时取消监听?
考察点:
-
初始化:
- 侦听器在组件实例化阶段初始化,除非设置了延迟绑定。
-
销毁监听:
-
Vue 自动处理组件销毁时的监听器清理。
-
对于动态绑定的侦听器(如在
created中使用$watch),需要手动清理:返回停止监听的函数:watch返回一个停止监听的函数,允许在需要时手动停止观察export default { data() { return { count: 0 }; }, created() { // 手动创建 watch(非选项式配置) this.stopWatch = this.$watch( 'count', (newVal) => { console.log('count 变为:', newVal); } ); }, beforeDestroy() { // 组件销毁前停止监听 this.stopWatch(); } };
-
问题:
- 在项目中,你曾经用
watch解决过哪些实际问题? - 是否遇到过与
watch相关的性能问题?你是如何处理的?
考察点:
-
场景应用:
- 动态请求:根据某个状态值变化实时发起 API 请求。input更改,select更改
- 动态 UI:根据某些数据变化调整页面元素显示。
-
性能问题及解决:
- 示例:在监听复杂对象时优化监听逻辑,或通过节流/防抖减少回调触发次数。
开放性问题
- 如果同时使用
watch和computed,你会如何合理分配它们的职责? - 你认为
watch的优点和局限性分别是什么?
总结
watch 是 Vue 中用于监听数据变化的重要工具,通过考察候选人在实际场景中对 watch 的理解和应用,可以评估其是否具备解决复杂逻辑问题的能力,以及是否能在项目中合理地平衡性能和功能需求。
watch 的优点和局限性分别是什么?
一、watch 的核心优点
- 异步友好:支持在监听回调中执行异步操作(如接口请求、定时器),这是 computed 做不到的;
- 精准监听:可指定监听单个 / 多个响应式数据,甚至深度监听对象内部属性、监听路由变化,触发逻辑更可控;
- 副作用处理:适合处理数据变化后的 “副作用”(如修改 DOM、发起请求、操作本地存储),逻辑边界清晰;
- 灵活配置:支持
immediate(立即执行)、deep(深度监听)、flush(回调执行时机)等配置,适配不同场景。
二、watch 的主要局限性
- 无缓存:每次监听的数据变化都会执行回调,无法复用结果,若回调逻辑复杂(如大量计算),性能不如 computed;
- 深度监听风险:监听复杂大对象时,
deep: true会遍历对象所有属性,可能导致性能损耗; - 依赖追踪被动:需手动指定监听目标,若漏写依赖或依赖变更,易出现逻辑失效(如监听
obj.a却改了obj.b,回调不会触发)。
核心总结
- 优点:异步适配好、监听精准、支持副作用、配置灵活;
- 局限:无缓存、深度监听耗性能、依赖需手动维护。
Vue 3 computed & watch 核心原理 + 与 Vue 2 差异
一、核心原理
- computed:基于
ReactiveEffect实现的带缓存懒执行副作用,首次读取 / 依赖变化后读取时执行计算并缓存,依赖未变时直接返回缓存值,核心是「缓存 + 懒执行」。 - watch:基于
ReactiveEffect监听数据源,解析数据源为取值函数,依赖变化时对比新旧值执行回调,支持deep(递归监听)、immediate(立即执行)、flush(执行时机)。
二、与 Vue 2 核心差异
| 维度 | Vue 2 | Vue 3 |
|---|---|---|
| 底层实现 | 基于 Watcher 类 | 基于 ReactiveEffect(解耦) |
| 响应式底层 | Object.defineProperty | Proxy(支持动态属性 / 数组) |
| watch 增强 | 仅支持 immediate | 新增 flush,deep 惰性遍历更优 |
| 工程化 | 无 Tree-Shaking | 支持 Tree-Shaking,体积更优 |
| 组合式 API | 仅选项式支持 | 原生适配 setup,使用更灵活 |
总结
- Vue 3 中两者均基于
ReactiveEffect替代 Vue 2 的Watcher,逻辑更解耦; - computed 核心是缓存懒执行,watch 核心是监听数据源执行回调;
- Vue 3 依托 Proxy 补齐了 Vue 2 响应式短板,且增强了 watch 的灵活性与性能。
watch与watchEffect 有什么区别,分别在什么场景下使用?⭐️⭐️
watch 和 watchEffect 是 Vue 3 中用于响应式数据变化的两个 API,它们都可以用于监听和响应数据的变化,但有一些关键的区别。理解这两个 API 的不同用途和行为对于有效地使用 Vue 3 的响应式系统非常重要。
1. watch
watch 是 Vue 3 中用于监听特定的响应式数据变化的 API。你可以选择一个或多个响应式源,并在其变化时执行相应的回调函数。
基本用法
import { ref, watch } from 'vue';
const count = ref(0);
watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`);
});
// 修改 count 时触发 watch
count.value = 1;
特点
- 监听指定的响应式数据:你需要明确指定要监听的目标,可以是一个响应式数据、多个响应式数据或 getter 函数。
- 手动触发回调:
watch只会在你指定的数据变化时触发回调函数,而不是自动运行。需要显式指定要监听的数据。 - 获取旧值:你可以通过
watch获取到变化前的旧值。
用例
- 监听单个或多个响应式数据的变化。
- 当数据变化时执行副作用,且可能需要使用旧值。
- 监听复杂的 getter 函数。
示例
import { ref, watch } from 'vue';
const count = ref(0);
const message = ref('Hello');
watch([count, message], ([newCount, newMessage], [oldCount, oldMessage]) => {
console.log(`count: ${oldCount} -> ${newCount}, message: ${oldMessage} -> ${newMessage}`);
});
count.value = 1;
message.value = 'World';
2. watchEffect
watchEffect 是 Vue 3 中的一种更简洁的 API,它会自动追踪其内部使用的所有响应式数据并在这些数据变化时执行副作用。它通常用于执行副作用,而不需要显式声明你要监听的响应式数据。
基本用法
import { ref, watchEffect } from 'vue';
const count = ref(0);
watchEffect(() => {
console.log(`count is: ${count.value}`);
});
// 修改 count 时自动触发 watchEffect
count.value = 1;
特点
- 自动追踪响应式数据:
watchEffect会自动追踪其作用域内的所有响应式数据(如ref、reactive)的变化,不需要显式指定要监听的数据。 - 立即执行:
watchEffect在注册时会立即执行一次,且执行副作用时会跟踪其内部使用的响应式数据。 - 没有旧值:
watchEffect不会提供旧值,它只关心当前的变化和执行副作用。
用例
- 执行副作用,如更新 DOM、进行计算或触发其他操作。
- 自动追踪所需的响应式数据,简化监听逻辑。
示例
import { ref, watchEffect } from 'vue';
const count = ref(0);
const message = ref('Hello');
watchEffect(() => {
console.log(`count is: ${count.value}, message is: ${message.value}`);
});
count.value = 1;
message.value = 'World';
主要区别
| 特性 | watchEffect | watch |
|---|---|---|
| 依赖追踪方式 | 自动追踪:执行函数时收集所有访问的响应式数据 | 手动指定:需显式声明监听的数据源(如 ref / 函数) |
| 数据源声明 | 无需声明,隐式依赖 | 必须显式声明(如 () => count.value) |
| 回调参数 | 无参数,无法直接获取新旧值 | 可获取新旧值((newVal, oldVal) => {}) |
| 执行时机 | 初始化立即执行(默认) | 默认初始化不执行(需 immediate: true) |
| 依赖更新 | 自动清理旧依赖,重新收集新依赖 | 依赖固定,需手动配置 deep 监听嵌套对象 |
| 使用场景 | 响应式数据变化后执行副作用(如更新 DOM、请求) | 需对比新旧值、精准监听指定数据、异步操作控制 |
| 性能 | 由于自动依赖追踪,它会依赖于回调函数中所有访问的响应式数据,可能导致更多的不必要的副作用执行 | 能够对特定的数据变化进行精确控制,性能通常较优。 |
watchEffect 如何自动追踪依赖
一、watchEffect 自动依赖追踪的核心原理
watchEffect 是 Vue 3 基于 ReactiveEffect 实现的自动依赖收集型副作用,其自动追踪依赖的核心逻辑如下:
- 初始化执行:调用
watchEffect时,立即执行传入的副作用函数; - 依赖收集阶段:执行副作用函数时,内部访问的所有响应式数据(ref/reactive)会触发
track收集依赖 —— 将当前ReactiveEffect关联到这些数据的依赖列表中; - 响应式触发:当收集到的任意响应式数据变化时,自动触发副作用函数重新执行;
- 依赖自动更新:每次副作用函数执行时,会先清理旧依赖,再重新收集新依赖(比如条件分支中使用的响应式数据变化,依赖列表会动态更新)。
简化版核心代码(理解原理)
function watchEffect(effectFn) {
// 创建 ReactiveEffect 实例
const effect = new ReactiveEffect(effectFn);
// 初始化执行副作用函数,触发依赖收集
effect.run();
// 返回停止监听函数
return () => effect.stop();
}
// ReactiveEffect 核心逻辑(简化)
class ReactiveEffect {
constructor(fn) {
this.fn = fn; // 副作用函数
this.deps = []; // 存储收集到的依赖
}
run() {
// 1. 清理旧依赖(避免无效依赖)
this.cleanup();
// 2. 将当前 Effect 设为活跃状态,准备收集依赖
activeEffect = this;
// 3. 执行副作用函数,访问响应式数据时触发 track
this.fn();
// 4. 重置活跃状态
activeEffect = null;
}
cleanup() {
// 从所有依赖列表中移除当前 Effect
this.deps.forEach(dep => dep.delete(this));
this.deps.length = 0;
}
stop() {
this.cleanup(); // 停止监听,清理所有依赖
}
}
关键细节:
- 无需显式指定监听目标:副作用函数执行过程中,所有被访问的响应式数据会自动成为依赖;
- 动态更新依赖:如果副作用函数内有条件分支(如
if (flag) { console.log(count.value) }),当flag变化时,依赖列表会自动清理旧依赖(如count)或新增新依赖; - 初始化立即执行:和
watch的immediate: true效果一致,但无需手动配置。
代码示例对比
import { ref, watch, watchEffect } from 'vue';
const count = ref(0);
const flag = ref(true);
// 1. watchEffect:自动追踪依赖
const stopWatchEffect = watchEffect(() => {
// 自动收集 count 和 flag 为依赖
if (flag.value) {
console.log('count:', count.value);
}
});
// 2. watch:需显式指定监听目标
const stopWatch = watch(
// 显式声明监听 count 和 flag
() => ({ count: count.value, flag: flag.value }),
(newVal, oldVal) => {
// 可获取新旧值
console.log('新值:', newVal, '旧值:', oldVal);
if (newVal.flag) {
console.log('count:', newVal.count);
}
},
{ immediate: true } // 需手动配置才初始化执行
);
// 数据变化触发更新
count.value = 1; // watchEffect 和 watch 均触发
flag.value = false; // watchEffect 清理 count 依赖,watch 仍监听(需手动处理)
三、关键补充
-
watchEffect 的停止与清理:
-
返回停止函数,调用后停止监听;
-
副作用函数可接收
onInvalidate参数,用于清理异步副作用(如取消请求、清除定时器):watchEffect((onInvalidate) => { const timer = setTimeout(() => console.log('执行'), 1000); // 副作用重新执行/停止时,先执行清理逻辑 onInvalidate(() => clearTimeout(timer)); });
-
-
执行时机控制:通过
flush选项(pre/post/sync)控制副作用执行时机,默认pre(组件更新前执行); -
适用场景边界:
- 适合 “数据变化后执行副作用,无需关注新旧值” 的场景(如更新 DOM、触发请求);
- 需对比新旧值、精准监听指定数据时,优先用
watch。
总结
-
watchEffect 自动追踪依赖原理:初始化立即执行副作用函数,执行过程中自动收集所有访问的响应式数据为依赖;数据变化时触发函数重新执行,且每次执行会清理旧依赖、重新收集新依赖;
-
与 watch 的核心区别:
- 依赖追踪:watchEffect 隐式自动追踪,watch 需显式指定监听目标;
- 参数获取:watchEffect 无新旧值参数,watch 可获取;
- 执行时机:watchEffect 初始化必执行,watch 默认不执行(需
immediate);
-
使用场景:watchEffect 适合简单的副作用执行,watch 适合需精准监听、对比新旧值的场景。