vue 基础
1. 声明式渲染
-
模板语法 :
{{ }}文本插值、v-html输出 HTML -
指令:
- 内置指令:
v-bind、v-on、v-model、v-if/v-else-if/v-else、v-show、v-for、v-pre、v-cloak、v-once - 自定义指令:全局
Vue.directive(Vue 2)/app.directive(Vue 3),局部directives选项
- 内置指令:
2. 响应式系统
2.0 数据劫持
-
遍历 data 对象,对每个属性递归调用
defineReactive,为每个属性创建 Dep(依赖收集器)。 -
每个属性对应一个 Watcher(观察者),在渲染时收集依赖。
-
Vue 2 :基于
Object.defineProperty,递归遍历对象属性,无法检测:- 对象属性的添加/删除(需用
Vue.set/this.$set) - 无法直接通过索引修改数组(
arr[0] = xx不触发更新,需用变异方法如push,splice) - 初始化时需要递归遍历所有深层属性,性能差。
- 对象属性的添加/删除(需用
-
Vue 3 :基于
Proxy,可监听动态添加属性、数组索引修改,性能更优,支持Map、Set等原生集合的响应式
2.1 响应式核心原理
Vue 的响应式系统通过 数据劫持 + 依赖收集 + 派发更新 实现数据与视图的自动同步。
| 步骤 | 说明 |
|---|---|
| 数据劫持 | 对组件的 data 进行代理,拦截属性的读取和修改操作。 |
| 依赖收集 | 当渲染函数、计算属性或侦听器执行时,会读取响应式数据,此时将当前活跃的 Watcher(观察者) 添加到数据的依赖列表(Dep)中。 |
| 派发更新 | 当数据被修改时,Dep 通知所有依赖的 Watcher 执行更新。 |
| 异步更新队列 | Watcher 不会立即触发 DOM 更新,而是将自己推入一个队列,在下一个事件循环的 microtask 中统一执行,并通过 nextTick 提供更新后的回调。 |
2.2 Vue 2 与 Vue 3 响应式实现对比
| 特性 | Vue 2 (Object.defineProperty) |
Vue 3 (Proxy) |
|---|---|---|
| 监听对象属性新增/删除 | 不支持,需 $set / $delete |
支持,原生拦截 |
| 数组索引/长度变更 | 不支持,需用变异方法或 $set |
支持,可直接赋值修改 |
| 初始化性能 | 递归遍历所有属性,数据大时较慢 | 懒代理,访问时才递归,初始更快 |
| 代理对象类型 | 只能代理普通对象 | 可代理数组、Map、Set、WeakMap、WeakSet |
| 需要额外 API | Vue.set、Vue.delete、数组变异方法 |
无需额外 API,直接使用原生操作 |
| 依赖收集粒度 | 属性级别 | 属性级别(但基于 Proxy 实现更简洁) |
| TypeScript 支持 | 较弱,需额外装饰器或声明 | 原生 TS 支持更好,reactive 返回代理类型 |
2.3 虚拟 DOM 与 Diff 算法
当响应式数据变化触发异步更新队列后,Vue 不会直接操作真实 DOM,而是先更新 虚拟 DOM(VNode) ,再通过 Diff 算法 找出最小变化,最后批量更新到真实 DOM 上。
2.3.1 为什么需要虚拟 DOM?
- 真实 DOM 操作代价昂贵,频繁操作易造成性能瓶颈。
- 虚拟 DOM 是 JS 对象,操作成本低,可在内存中完成比对。
- Diff 算法可以复用尽可能多的真实 DOM 节点,减少重排重绘。
2.3.2 Diff 流程(以 Vue 2 / Vue 3 为例)
| 阶段 | 说明 |
|---|---|
| 新旧 VNode 树 | 组件重新渲染时,生成新的 VNode 树,与上一次渲染的旧 VNode 树进行比对。 |
| 同层比较 | 只对比同一层级的节点,不跨层比较,时间复杂度从 O(n³) 降为 O(n)。 |
| 节点类型判断 | 若新旧节点类型不同(如 div → p),则直接替换整个子树(销毁旧节点,创建新节点)。 |
| 相同节点复用 | 若类型相同且 key 相同,则尝试复用旧 DOM,仅更新变化的属性(class、style、事件等)。 |
| 子节点 Diff | 对子节点数组进行差异比对,Vue 2 使用双端比较算法(首尾交叉对比),Vue 3 增加了最长递增子序列优化。 |
2.3.3 Vue 3 Diff 优化亮点
| 优化点 | 说明 |
|---|---|
| 静态标记(PatchFlags) | 编译阶段标记节点的动态属性(如 TEXT、CLASS、STYLE),Diff 时只对比带标记的部分,跳过静态内容。 |
| 静态提升 | 将不会变化的静态节点提升到渲染函数外部,避免每次渲染都重新创建。 |
| 缓存事件处理函数 | 避免内联函数导致的子组件不必要更新。 |
| 最长递增子序列 | 在移动子节点时,寻找最长递增子序列,最小化 DOM 移动次数。 |
2.4 完整更新流程(响应式 → Diff → DOM 更新)
bash
用户修改数据
↓
触发响应式 setter / Proxy trap
↓
Dep 通知相关 Watcher
↓
Watcher 将自己推入异步更新队列
↓
nextTick 时清空队列,执行组件更新函数
↓
生成新 VNode 树,与旧 VNode 树进行 Diff
↓
根据 Diff 结果生成 DOM 操作补丁(Patch)
↓
批量更新真实 DOM
↓
触发 updated 生命周期钩子
总结
Vue 2 通过 Object.defineProperty 劫持对象属性的 getter/setter 来实现响应式,但存在局限性,比如无法监听动态添加的属性,需要通过 Vue.set 处理。Vue 3 改用 Proxy,可以代理整个对象,支持多种操作拦截,解决了上述问题,同时性能更优。响应式核心是依赖收集和派发更新,在 getter 中收集依赖,在 setter 中触发更新,并通过异步队列实现批量更新
3. vue3 API
| API | 说明 |
|---|---|
ref() |
声明任意类型的响应式数据,需通过 .value 访问。 |
reactive() |
声明对象/数组类型的响应式数据,可直接访问属性。 |
readonly() |
接收一个响应式对象或普通对象,返回其只读代理,不可修改。 |
shallowRef() |
声明浅层响应式数据,只有 .value 的访问是响应式的,内部嵌套对象不会变为响应式。 |
shallowReactive() |
声明浅层响应式对象,只有顶层属性是响应式的,嵌套对象不会被转换。 |
shallowReadonly() |
声明浅层只读对象,顶层属性只读,嵌套对象不会被转换且不具响应性。 |
computed() |
定义计算属性,基于响应式依赖缓存结果。 |
watch() |
监听特定数据源,在数据变化时执行副作用。 |
watchEffect() |
自动追踪其内部使用的响应式数据,并在数据变化时立即重新运行。 |
onMounted(), onUpdated(), onUnmounted() 等 |
组件生命周期不同阶段执行的钩子函数,用法与选项式 API 对应。 |
provide(), inject() |
用于跨层级组件通信,祖先组件提供数据,后代组件注入使用。 |
toRefs(), toRef(), isRef(), unref() 等 |
用于处理 ref / reactive 对象的辅助函数,帮助进行响应式转换和判断。 |
defineProps(), defineEmits() |
在 <script setup> 中声明组件的 props 和 emits,享受完整类型推导。 |
defineExpose() |
在 <script setup> 中声明当前组件暴露给父组件的属性或方法。 |
defineOptions() |
在 <script setup> 中声明组件名 (name) 或 inheritAttrs 等选项。 |
内置组件
内置指令
| 指令 | 说明 |
|---|---|
v-model |
在表单元素或组件上创建双向绑定。 |
v-if, v-else-if, v-else |
条件渲染,为 false 时不渲染元素。 |
v-show |
条件渲染,通过 CSS 的 display 属性切换。 |
v-for |
基于源数据多次渲染元素或模板块。 |
v-on (@) |
绑定事件监听器。 |
v-bind (:) |
动态地绑定一个或多个属性。 |
v-slot (#) |
用于声明具名插槽或作用域插槽。 |
4. 生命周期钩子
| 阶段 | Vue 2 钩子 | Vue 3 钩子(Options API) | Vue 3 钩子(Composition API) |
|---|---|---|---|
| 初始化 | beforeCreate, created | 同 | setup() 代替 beforeCreate/created |
| 挂载 | beforeMount, mounted | 同 | onBeforeMount, onMounted |
| 更新 | beforeUpdate, updated | 同 | onBeforeUpdate, onUpdated |
| 卸载 | beforeDestroy, destroyed | beforeUnmount, unmounted | onBeforeUnmount, onUnmounted |
| 错误捕获 | errorCaptured | 同 | onErrorCaptured |
| 其他 | activated, deactivated(keep-alive) | 同 | onActivated, onDeactivated |
| 调试 | renderTracked, renderTriggered(开发) | 同 | onRenderTracked, onRenderTriggered |
5. 插槽
- 默认插槽、具名插槽、作用域插槽(
slot-scopein Vue 2,v-slotin Vue 2.6+ & Vue 3) - Vue 3 中
v-slot统一为指令语法,slot和slot-scope被废弃
6. 混入(Mixin)
- 全局混入、局部混入
- 合并策略:数据递归合并,同名钩子合并为数组,方法/组件/指令等直接覆盖
- 缺点:命名冲突、隐式依赖、代码不直观 → 推荐组合式 API 替代
7. 自定义指令
-
钩子函数:
- Vue 2:
bind,inserted,update,componentUpdated,unbind - Vue 3:
beforeMount,mounted,beforeUpdate,updated,beforeUnmount,unmounted
- Vue 2:
-
参数:
el,binding,vnode,prevVnode
8. 过滤器(Filters)
- Vue 2 支持模板内过滤器(
{{ msg | filter }})及全局/局部定义 - Vue 3 中移除,推荐用计算属性或方法替代
9. 动画与过渡
<transition>单元素过渡<transition-group>多元素/列表过渡- 类名约定:
v-enter-from/v-enter-to等(Vue 3 命名变化) - JavaScript 钩子:
@before-enter,@enter,@after-enter等
10、组件通信方式(详细对比)
| 方式 | Vue 2 | Vue 3 |
|---|---|---|
| props / $emit | 支持 | 支持,emit 需在 setup 中声明 |
| v-model | 单个,value + input |
可多个,modelValue + update:modelValue,支持自定义修饰符 |
| refs/parent / $children | $children 存在 |
移除 $children,推荐 ref + $parent 或组合式 API |
| provide / inject | 默认非响应式,可传递响应式对象 | 支持响应式传递,可提供 ref/reactive |
| event bus | new Vue() 作为总线 |
推荐用 mitt 等第三方库 |
| Vuex | Vuex 3 | Vuex 4 / Pinia |
| slot 作用域 | slot-scope |
v-slot 统一语法 |
| 组合式 API | 无 | 可直接使用 ref 传递,逻辑复用更灵活 |
11、Vue Router 对比(3.x vs 4.x)
| 特性 | Vue Router 3(Vue 2) | Vue Router 4(Vue 3) |
|---|---|---|
| 创建方式 | new VueRouter(...) |
createRouter({ ... }) |
| 模式 | mode: 'history' / 'hash' |
history: createWebHistory() / createWebHashHistory() |
| 路由守卫 | beforeEach / beforeResolve / afterEach |
同,但支持组合式 API 中的 onBeforeRouteUpdate 等 |
| 路由元信息 | meta |
同 |
| 动态路由 | addRoutes |
addRoute,且支持动态删除 |
| 导航故障 | NavigationFailureType |
更完善的类型 |
| 组合式 API | 不支持 | useRouter、useRoute |
12、状态管理:Vuex vs Pinia
| 特性 | Vuex 3/4 | Pinia |
|---|---|---|
| 设计哲学 | Flux 正统继承者:严格单向数据流 View → Action → Mutation → State → View |
拥抱 Composition API:去掉 mutations,actions 可直接修改 state , ---- API: actions、state、getters |
| API 风格 | Options API 风格(state, mutations, actions, getters) | 同时支持 Options 和 Composition API 风格,更接近普通 JS 对象 |
| 状态修改方式 | 必须通过 commit(mutation) 同步修改,异步操作需在 action 中 commit |
可以直接在 action 中 this.count++ 或 store.count++,同步/异步统一处理 |
| Mutations | 必须有,且必须是同步函数 | 没有 mutations,所有修改统一写在 actions 中 |
| 样板代码量 | 较多(需要定义 mutations + actions + 辅助函数) | 减少约 40% 样板代码,更简洁直观 |
| TypeScript 支持 | 需要额外定义 State 接口,借助 createStore<State> 或装饰器,组件中还需标注类型 |
原生 TS 支持,自动推导,几乎没有模板代码;Setup Store 风格体验更佳 |
| 模块系统 | modules 嵌套,有命名空间概念,默认所有 action/mutation 全局,需手动开启 namespaced: true |
每个 defineStore(id, ...) 自然创建一个独立模块,无嵌套、无命名空间问题,更扁平 |
| 代码体积(gzipped) | ~17 kB | ~2 kB |
| 依赖数量 | 较多依赖 | 极少依赖(几乎无) |
| 运行效率 | 优秀(底层基于 Vue 响应式) | 优秀(与 Vuex 相近,底层同样基于 Vue 3 响应式) |
| Devtools | 支持时间旅行、快照 | 支持时间旅行、快照,且更直观,与 Devtools 集成更紧密 |
| 热更新 | 支持但不完美,需要额外处理 | 原生支持 store 热更新,无需刷新页面 |
| SSR 支持 | 需手动创建 store 实例 | 自动支持,每个请求独立实例,避免状态污染 |
| 迁移成本 | --- | 可从 Vuex 增量替换 ,两者可共存;社区有 vuex-to-pinia 辅助脚本转换 80% 代码;主要改动点:state 改为函数、mutations 合并入 actions、commit 改直接调用、mapState/mapActions 换为 storeToRefs + 解构 |
| 官方推荐 | Vue 2 时代官方标准,Vue 3 可用但不再推荐 | Vue 3 官方推荐 ,Vue 2 也可通过插件使用(@pinia/nuxt 或普通 Vue 2 项目) |
javascript
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// 返回初始状态
state: () => ({
count: 0
}),
// 计算属性(派生状态)
getters: {
doubleCount: (state) => state.count * 2,
},
// 可以包含同步/异步操作
actions: {
increment() {
this.count++
},
async asyncIncrement() {
await new Promise(resolve => setTimeout(resolve, 1000))
this.count++
},
},
})
13、构建工具:Vue CLI vs Vite
| 特性 | Vue CLI(基于 webpack) | Vite |
|---|---|---|
| 启动速度 | 慢(打包后启动) | 极快(按需编译,原生 ES modules) |
| 生产构建 | 基于 webpack,配置灵活但复杂 | 基于 Rollup,预配置更简单 |
| 插件生态 | 丰富的 webpack 插件 | 插件系统兼容 Rollup 插件,且提供 Vite 插件 |
| 配置方式 | vue.config.js |
vite.config.js |
| 开发环境 | HMR 较慢(大规模项目) | HMR 快速,保留状态 |
| 环境变量 | VUE_APP_* |
VITE_* |
15、虚拟 DOM 与 diff 算法
diff 策略
- 同层比较:只比较同一层节点,不跨层。
- 双端比较(Vue 2):新旧 VNode 的 children 数组通过头尾交叉比较,找到可复用的节点。
- 静态提升(Vue 3):编译时标记静态节点,更新时跳过它们。
- Patch flag(Vue 3):标记动态节点,只更新变化的部分
总结
虚拟 DOM 是一种用 JS 对象模拟真实 DOM 的结构,通过 diff 算法对比新旧 VNode,找出差异并批量更新真实 DOM,减少了直接操作 DOM 的性能开销。Vue 2 的 diff 采用双端比较,Vue 3 则引入了静态提升和 patch flags,进一步优化了更新效率。key 是 diff 过程中识别节点的重要依据,使用稳定的 key 可以保证节点复用,避免渲染错误。
vue 常见面试题
第一、问:"谈谈你对 Vue 渐进式框架的理解?"
-
核心定义:首先清晰说明"渐进式"就是"按需取用,逐步增强"。
-
层次说明:分层次阐述其含义:
-
视图层核心:可以只用它做 DOM 渲染,像 jQuery 一样。
-
组件系统:可以只用它来构建可复用的组件。
-
客户端路由:需要页面跳转时,加入 Vue Router。
-
状态管理:应用复杂、组件间通信频繁时,加入 Pinia。
-
构建工具链:对于大型项目,使用 Vite 获得完整的工程化支持。
-
-
举例说明:结合实际场景,例如"我可以在一个旧的 JSP 或 PHP 页面里,只引入 Vue 来做一个交互复杂的表单,而不用重构整个页面"。
-
总结优势:最后总结其优点------灵活、低门槛、风险小,无论是小项目还是大应用都能很好地适应。
第二、 Vue 的响应式原理是什么?
一句话总结:Vue 通过数据劫持监听数据变化,通过依赖收集建立起数据和视图的映射关系,当数据变化时,通过异步派发更新异步地通知所有相关的视图进行diff对比并重新渲染
一个具体的例子来说明整个过程
假设你有如下代码:
javascript
data() {
return { name: 'Alice' }
}
// 页面上有:<div>{{ name }}</div>
1、 初始化 :Vue 对你的 name 属性进行劫持,设置 get 和 set。
2、 首次渲染 :渲染页面时,需要读取 name 的值,触发了 get。Vue 看到是"渲染Watcher"在读这个数据,就把这个 Watcher 添加到 name 的 "依赖盒子"(Dep)里。此时,name 和页面建立了联系。
3、 你修改数据 :执行 this.name = 'Bob',触发了 name 的 set。
4、 派发更新 :set 通知 name 的 "依赖盒子":"我的值变了!"
5、 依赖盒子通知:"依赖盒子"里存储着"渲染Watcher",于是通知它:"你依赖的数据变了,该干活了!"
6、 异步更新:"渲染Watcher"把自己放到异步队列里,在下一个Tick,重新执行渲染函数,生成新的虚拟DOM,计算差异,最后更新页面上的文字从"Alice"变为"Bob"。
第三、 为什么 Vue 默认使用模板语法,而不是像 React 那样使用 JSX?模板语法有什么好处?"
-
降低门槛,提升体验:核心原因是开发体验和学习曲线。模板语法基于标准 HTML,对初学者和设计师非常友好,符合直觉。
-
声明式与直观性:模板能非常清晰、直观地表达 UI 的结构和逻辑,代码可读性强。
-
编译时优化:这是关键的技术优势。强调 Vue 的模板不是直接在运行时解释的,而是被编译器转换成了优化的渲染函数。可以具体说出几点优化,如
静态提升、动态标记等,证明你理解其底层原理。 -
关注点分离:提及单文件组件(SFC)的结构优势,模板、逻辑、样式各司其职,便于团队协作和长期维护。
-
灵活性:最后可以补充一点,Vue 并不排斥 JSX。对于需要高度动态和编程灵活性的场景,开发者完全可以选择使用 JSX,体现了 Vue 设计的灵活性。
第四、生命周期钩子映射表
| 阶段 | Vue 2 钩子 | Vue 3 钩子(Options API) | Vue 3 钩子(Composition API) |
|---|---|---|---|
| 初始化 | beforeCreate, created | 同 | setup() 代替 beforeCreate/created |
| 挂载 | beforeMount, mounted | 同 | onBeforeMount, onMounted |
| 更新 | beforeUpdate, updated | 同 | onBeforeUpdate, onUpdated |
| 卸载 | beforeDestroy, destroyed | beforeUnmount, unmounted | onBeforeUnmount, onUnmounted |
| 错误捕获 | errorCaptured | 同 | onErrorCaptured |
| 其他 | activated, deactivated(keep-alive) | 同 | onActivated, onDeactivated |
| 调试 | renderTracked, renderTriggered(开发) | 同 | onRenderTracked, onRenderTriggered |
第五、 "watch 和 watchEffect 有什么区别?应该如何选择?"
-
依赖追踪:
-
watchEffect:自动追踪。它会自动收集其回调函数中访问到的所有响应式数据作为依赖。 -
watch:手动指定。你必须明确地告诉它要监听哪个数据源。
-
-
执行时机:
-
watchEffect:立即执行一次,然后等待依赖变化后再次执行。 -
watch:默认是懒执行的,只有当被监听的数据源变化时才执行。可以通过 { immediate: true } 选项使其立即执行。
-
-
访问旧值:
-
watchEffect:无法访问变化前的值。 -
watch:可以同时访问新值和旧值,方便进行比较。
-
第六、 生命周期钩子(执行顺序、使用场景)
父子组件生命周期顺序
- 创建:父 beforeCreate → 父 created → 子 beforeCreate → 子 created → 子 beforeMount → 子 mounted → 父 mounted
- 更新:父 beforeUpdate → 子 beforeUpdate → 子 updated → 父 updated
- 销毁:父 beforeDestroy → 子 beforeDestroy → 子 destroyed → 父 destroyed
常用钩子作用
- beforeCreate:实例初始化后,数据观测和事件配置之前。无法访问 data、props。
- created:可访问数据,但 DOM 未挂载,适合异步请求、初始化数据。
- mounted:DOM 已挂载,可操作 DOM,适合第三方库初始化。
- beforeDestroy:销毁前,适合清除定时器、取消订阅。
- activated / deactivated :
keep-alive组件激活/停用。
第七、$nextTick 原理及使用场景
原理
Vue 的异步更新队列。数据变化后,Vue 将开启一个队列,把同一个事件循环内的所有数据变化缓存起来,然后在下一个事件循环(microtask)统一执行 DOM 更新。$nextTick 的回调会在 DOM 更新完成后执行。
使用场景:
- 在数据变化后,需要获取更新后的 DOM 结构。
- 需要在 mounted 钩子中确保子组件渲染完成。
- 异步操作后需要等待 DOM 同步。
面试回答:
"$nextTick 利用 Promise 或 MutationObserver 等微任务机制,将回调延迟到下次 DOM 更新循环之后执行。我们常用来解决数据变化后立即操作 DOM 的问题,比如滚动到底部、获取元素宽高等。Vue 3 中同样有 nextTick 函数,可在组合式 API 中使用。
第八、 keep-alive 实现原理及生命周期
作用:
缓存不活动的组件实例,避免反复渲染。
原理:
内部维护一个缓存对象(键是组件的 key 或自身),当组件切换时,将被移除的组件实例保留在缓存中,而不是销毁。再次激活时从缓存取出复用,触发 activated 和 deactivated 钩子。
相关属性:
include/exclude:正则或数组,指定要缓存/不缓存的组件。max:最大缓存数,超出时根据 LRU 策略删除。
生命周期:
- 首次进入:
created→mounted→activated - 缓存后再次进入:
activated(不会重新执行created/mounted) - 离开时:
deactivated
面试回答:
"keep-alive 是一个抽象组件,它通过缓存 VNode 来保留组件状态,避免重复渲染。内部使用 LRU 算法管理缓存,可以通过 include 和 max 控制缓存策略。被缓存的组件会多出 activated 和 deactivated 钩子,用于在激活/停用时执行逻辑。
第九、 组合式 API 与选项式 API 的优缺点
选项式 API(Vue 2 主流):
- 优点:结构清晰(data、methods、computed 分块),适合初学者。
- 缺点:逻辑分散,复杂组件难以维护;复用逻辑需借助 mixin,存在缺陷。
组合式 API(Vue 3 引入):
-
优点:
- 逻辑集中,按功能组织代码,可读性和可维护性高。
- 逻辑复用简单,通过组合函数(hooks)实现,无命名冲突。
- 更好的 TypeScript 类型推断。
-
缺点:学习曲线稍陡,对初学者不够直观。
面试回答:
"选项式 API 将组件选项按类型划分,代码直观但逻辑分散。组合式 API 将相关逻辑聚合在 setup 中,通过组合函数实现复用,尤其适合大型复杂组件。Vue 3 并未废弃选项式 API,两者可混用,但组合式 API 提供了更好的逻辑复用能力和类型支持,是未来的推荐写法。"
第十、SSR 原理及优缺点
原理:
- 服务端运行 Vue 应用,生成 HTML 字符串直接返回给浏览器,客户端再"激活"(hydrate)为可交互应用。
- 同构:同一份代码在服务端和客户端均可运行。
优点:
- 更好的 SEO:搜索引擎能抓取完整 HTML。
- 更快的首屏加载:用户无需等待 JS 下载即可看到内容。
缺点:
- 开发复杂度高:需考虑 Node.js 环境兼容性。
- 服务器负载大:每个请求都重新渲染,需注意缓存策略。
- 部分 API 在服务端不可用(如 window、document),需条件判断。
面试回答:
"SSR 在服务端将 Vue 组件渲染成 HTML,发送给客户端,然后客户端进行激活。它主要解决 SPA 的 SEO 问题和首屏加载速度。但实现成本较高,需要处理服务端和客户端环境的差异,并关注服务器性能。通常我们会借助 Nuxt.js(Vue 2)或 Nuxt 3(Vue 3)这样的框架来简化 SSR 开发。"
第十一、Vue 3 新特性及与 Vue 2 的区别
核心新特性:
- 组合式 API:更好的逻辑复用和代码组织。
- Proxy 响应式:解决 Vue 2 的响应式局限,性能更优。
- Teleport:将组件内容渲染到任意 DOM 位置。
- Fragment:组件支持多个根节点。
- Suspense:用于异步组件加载时的占位。
- 全局 API 改造 :
createApp替代new Vue,全局配置隔离。 - 更好的 TypeScript 支持:源码用 TS 重写,类型更完善。
- 性能提升:编译优化(静态提升、patch flag),打包体积更小。
- Vite 官方构建工具:开发体验极大提升。
破坏性变更:
- 移除过滤器、
$children、$on/$once/$off、v-on.native等。 v-model默认 prop 和事件变化,支持多个绑定。v-if与v-for优先级改变。
面试回答:
"Vue 3 相比 Vue 2 在响应式系统、组合式 API、性能、TypeScript 支持等方面有重大改进。它引入了 Teleport、Suspense 等内置组件,并用 createApp 创建应用,避免全局污染。虽然有一些破坏性变更,但官方提供了迁移构建和工具帮助升级。Vue 3 也带来了更现代的构建工具 Vite,提升了开发体验。"
第十二、常见API使用方式 defineEmits、defineExpose、defineOptions、defineProps、
1. defineProps -- 接收父组件传递的数据
作用 :声明组件的 props(属性),代替传统的 props 选项。
基本用法:
xml
<script setup>
// 运行时声明(自动推断类型)
const props = defineProps(['title', 'count'])
// 带类型的声明(TypeScript)
const props = defineProps<{
title: string
count?: number // 可选
}>()
</script>
父组件使用:
ini
<MyComponent title="Hello" :count="10" />
2. defineEmits -- 向父组件发送事件
作用:声明组件可以触发的事件。
xml
<script setup>
// 简单声明
const emit = defineEmits(['update', 'delete'])
// 带参数验证(TypeScript)
const emit = defineEmits<{
(e: 'update', id: number): void
(e: 'delete', name: string): void
}>()
// 触发事件
emit('update', 123)
</script>
父组件监听:
ini
<MyComponent @update="handleUpdate" @delete="handleDelete" />
3. defineExpose -- 暴露组件内部属性/方法给父组件(通过 ref)
作用 :默认 <script setup> 下的组件是关闭的 ,父组件无法通过 ref 访问其内部成员。使用 defineExpose 明确暴露。
xml
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
// 只暴露 increment 和 count,其他不暴露
defineExpose({
increment,
count
})
</script>
父组件访问:
xml
<template>
<MyComponent ref="childRef" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
const childRef = ref()
onMounted(() => {
childRef.value.increment() // 调用子组件方法
console.log(childRef.value.count) // 读取子组件数据
})
</script>
4. defineOptions -- 设置组件选项(Vue 3.3+)
作用 :在 <script setup> 中声明组件名、继承属性、自定义选项等,无需单独的 <script> 块。
xml
<script setup>
defineOptions({
name: 'MyCustomName', // 组件名称
inheritAttrs: false, // 是否继承非 prop 属性
// 其他选项(如 components、directives 一般不在这里,但可自定义)
})
</script>
典型场景:
- 设置组件名(方便 Vue Devtools 识别)
- 关闭属性继承(手动控制
$attrs)
第十三、wacth & watchEffect 区别
| 特性 | watch |
watchEffect |
|---|---|---|
| 依赖收集 | 显式指定要监听的数据源(ref、reactive 属性、getter 函数) | 自动收集回调函数中使用的所有响应式数据 |
| 初始执行 | 默认不执行 ,数据第一次变化时才执行(可配置 immediate: true) |
立即执行一次,同时收集依赖 |
| 访问新旧值 | 回调中提供旧值和新值 | 只能访问新值(无法直接获取旧值) |
| 监听多个源 | 支持同时监听多个数据源(数组形式) | 自动收集多个依赖,无需显式指定 |
| 精准控制 | 可以配置 deep、flush、immediate 等选项 |
只有 flush 选项(以及 onTrack/onTrigger 调试) |
| 停止监听 | 调用返回的函数 | 同样返回停止函数 |
| 适用场景 | 需要知道具体哪个数据变化、需要旧值、需要惰性执行 | 简单副作用,自动跟踪依赖,不需要旧值 |
1、watch 基础用法
xml
<script setup>
import { ref, reactive, watch } from 'vue'
const count = ref(0)
const state = reactive({ name: 'Vue', age: 3 })
// 监听单个 ref
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})
// 监听 getter 函数
watch(
() => state.age,
(newAge, oldAge) => {
console.log(`年龄从 ${oldAge} 变为 ${newAge}`)
}
)
// 监听多个源(数组)
watch([count, () => state.age], ([newCount, newAge], [oldCount, oldAge]) => {
console.log(`count: ${oldCount}->${newCount}, age: ${oldAge}->${newAge}`)
})
// 立即执行 + 深度监听
watch(
() => state,
(newVal, oldVal) => {
console.log('state 变化了', newVal)
},
{ immediate: true, deep: true }
)
</script>
2、watchEffect 基础用法
xml
<script setup>
import { ref, reactive, watchEffect } from 'vue'
const count = ref(0)
const state = reactive({ name: 'Vue', age: 3 })
// 自动收集依赖:count 和 state.age
watchEffect(() => {
console.log(`count: ${count.value}, age: ${state.age}`)
})
// 初始立即输出:count: 0, age: 3
// 之后任何依赖变化都会重新执行
// 停止监听
const stop = watchEffect(() => { /* ... */ })
stop() // 手动停止
</script>
第十四、provide() 和 inject() 跨层级组件通信例子
xml
<!-- Ancestor.vue -->
<script setup>
import { provide, ref } from 'vue'
// 提供普通值
provide('theme', 'dark')
// 提供响应式数据(推荐)
const count = ref(0)
const updateCount = () => count.value++
provide('count', count)
provide('updateCount', updateCount)
</script>
xml
<!-- Descendant.vue -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme', 'light') // 默认值 'light'
const count = inject('count')
const updateCount = inject('updateCount')
// 使用
console.log(theme) // 'dark'
count.value++ // 响应式更新
updateCount() // 调用方法
</script>
第十五、toRefs、toRef、isRef、unref 响应式引用工具
1. toRefs -- 将响应式对象转换为普通对象,每个属性都是 ref
作用 :解构 reactive 对象时保持响应性。
javascript
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, name: 'Vue' })
// ❌ 直接解构会丢失响应性
let { count, name } = state
count++ // 不会触发视图更新
// ✅ 使用 toRefs 包装
const stateRefs = toRefs(state)
const { count, name } = stateRefs
count.value++ // 响应式生效
原理 :toRefs 为每个属性创建一个 ref 链接到原对象的对应属性。
2. toRef -- 为响应式对象的单个属性 创建 ref
作用 :保持对源对象属性的响应式引用,常用于将 props 的某个属性转为 ref 以便传递。
javascript
import { reactive, toRef } from 'vue'
const state = reactive({ count: 0 })
const countRef = toRef(state, 'count')
countRef.value++ // 同时修改 state.count
console.log(state.count) // 1
典型场景 :组合函数接收 props 中的某个属性并保持响应性。
javascript
// useFeature.js
import { toRef, watchEffect } from 'vue'
export function useFeature(propRef) {
const propVal = toRef(propRef) // 确保是 ref
watchEffect(() => {
console.log(propVal.value)
})
}
3. isRef -- 判断某个值是否为 ref
javascript
import { ref, reactive, isRef } from 'vue'
const count = ref(0)
const state = reactive({})
console.log(isRef(count)) // true
console.log(isRef(state)) // false
4. unref -- 如果参数是 ref 则返回其 value,否则返回参数本身
作用 :方便地获取值,无需手动判断 .value。
javascript
import { ref, unref } from 'vue'
const count = ref(0)
const plain = 42
console.log(unref(count)) // 0
console.log(unref(plain)) // 42
// 等价于
function myUnref(val) {
return isRef(val) ? val.value : val
}
常用场景 :在组合函数中,参数可能是 ref 也可能是普通值,使用 unref 统一处理。
第十六 跨层级通信:provide / inject
当需要从祖先组件向其所有后代组件传递数据时,使用 provide 和 inject 可以避免逐层传递 props(即"prop drilling")。
-
provide: 在祖先组件中提供数据或方法。
-
inject: 在任何后代组件中注入(接收)这些数据或方法。
祖先组件 App.vue
xml
<script setup>
import { ref, provide } from 'vue';
import DeepChild from './DeepChild.vue';
const theme = ref('light');
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light';
}
// 提供数据和方法给所有后代
provide('theme', theme);
provide('toggleTheme', toggleTheme);
</script>
<template>
<div :class="theme">
<DeepChild />
</div>
</template>
后代组件 DeepChild.vue
xml
<script setup>
import { inject } from 'vue';
// 注入来自祖先的数据和方法
const theme = inject('theme', 'light'); // 'light' 是默认值
const toggleTheme = inject('toggleTheme');
</script>
<template>
<p>Current theme is: {{ theme }}</p>
<button @click="toggleTheme">Toggle Theme</button>
</template>