前端面试-Vue篇
- 1、Vue对比其他框架的优势
- 2、Vue框架和React框架的区别
- 3、Vue虚拟DOM实现和React虚拟DOM实现对比
- 4、Vue的虚拟DOM实现原理
- 5、讲述一下Vue中虚拟DOM的Diff算法
- 6、Vue2的响应式系统
- 7、Vue3的响应式系统
- 8、什么是ReactiveEffect 实例
- 9、Vue2和Vue3的响应式系统对比
- 10、Vuex和Pinia的区别
- 11、Webpack和Vite的区别
- 12、computed和watch的区别和应用场景
- Vue computed 和 watch 对比表
- 13、watch和watchEffect的区别
- 14、v-if和v-for不建议一起使用的原因
- 15、v-for遍历元素的key属性为什么不建议使用index
- 16、v-if 和 v-show的区别
- 17、介绍一下Vue的生命周期(最新版)
- 18、MVC和MVVM的区别
- 19、讲述一下Vue中的数据双向绑定
- 20、Vue2和Vue3的区别
- 21、 Vue2中data为什么是函数而不是对象
- 22、Vue-自定义指令
- 23、Vue-Route路由
- 24、路由传值参数
- 25、路由守卫
- 26、hash路由和history路由的区别
- 27、Vue-Slot插槽
- 28、nextTick的作用和使用场景
1、Vue对比其他框架的优势
- 上手简单,入门难度较小
- 有详细的中文文档和活跃的社区
- Vue提供双向数据绑定,使数据和视图的同步变得简单
- Vue具有完整强大的工具链,能够快速搭建和开发项目
- 提供轻量级的核心库,不依赖其他库,加载速度快,性能好
2、Vue框架和React框架的区别
| 维度 | Vue 3 | React 18 |
|---|---|---|
| 核心定位 | 渐进式 MVVM 框架 | 只负责 View 的 UI 库 |
| 语法范式 | 单文件组件:template + script + style;支持选项式 + 组合式(setup) | 函数组件 + Hooks; 原生JS + JSX编程; |
| 数据绑定 | 响应式(Proxy 拦截,自动追踪依赖) | 手动 setState/ useState,不可变数据流 |
| 组件更新机制 | 依赖追踪 + 异步队列批量更新(nextTick) | Fiber + 调度器(lane 模型),时间切片 |
| 内置特性 | v-model、指令、keep-alive、transition、teleport 等官方内建 | 无内置高级特性,全由社区提供 |
| 状态管理 | Pinia、Vuex | Redux、MobX、Zustand、Recoil、Jotai |
| 路由 | Vue Router(官方) | React Router(社区) |
| 构建工具 | Vite(官方默认) | 自由选型:Create-React-App、Vite、Next.js、Remix |
| 生态周边 | 官方维护:Router、Pinia、DevTools、VSCode 插件 | 社区主导:选择多但碎片化 |
| 典型使用场景 | 后台管理系统、中后台、渐进式升级老项目 | 大型 SPA、跨平台(React Native)、Node 同构(Next.js) |
- React中的不可变数据是指数据创建后不能直接修改,需要创建并返回一个全新的数据副本,而非在原数据上修改
3、Vue虚拟DOM实现和React虚拟DOM实现对比
| 维度 | Vue 3 | React 18 |
|---|---|---|
| VDOM 结构 | 组件级 VNode + 块(Block)树 | 纯 Fiber 链表(vNode = FiberNode) |
| 更新触发 | 响应式系统(Proxy)精准收集依赖,仅受影响的组件重新渲染 | setState/useState手动触发,父组件更新默认带动所有子组件(除非 React.memo) |
| Diff 算法 | 编译期静态标记+ 动态补丁(patch flag)→ 只 Diff 动态节点 | 全树 Diff(双指针遍历 Fiber 链表),跳过 keyed 节点用 Map 加速 |
| 静态提升 / Block | 编译期把静态节点提升为常量,生成 Block 树,Diff 时跳过静态部分 | 无静态提升,每次 Diff 都遍历整棵子树(开发者需手动 memo) |
| 时间切片 | 无(更新是同步微任务,nextTick 批量) | 有(Fiber lane 模型 + Scheduler)可中断/恢复 |
| 编译期优化 | 模板编译成render 函数 + patchFlag,运行期只做最小 patch | JSX 编译成createElement 调用,运行期无法区分静动态 |
4、Vue的虚拟DOM实现原理
虚拟DOM 主要是用JavaScript 对象(VNode)描述真实 DOM 结构,通过 Diff 算法比较新旧 VNode (虚拟节点)树,找出最小差异并批量更新真实 DOM
- VNode 虚拟节点用 JavaScript 对象描述真实 DOM 结构的轻量级节点单元
- 虚拟DOM实现原理的步骤如下:
- 创建VNode:JS 对象 = 虚拟节点 一个虚拟节点相当于一个轻量级的真实 DOM 结构
interfaceVNode{type:string|Component// div 或组件定义props:Object|null// 属性/事件children:VNode[]|string|null// 子节点key:string|number|null// Diff 标识el:Element|null// 对应真实 DOM(挂载后)} - h 函数:创建 VNode 的工厂
import{h}from'vue'// 写法 ① 模板<div id="app">{{msg}}</div>// 写法 ② 渲染函数 = 产生 VNoderender(){returnh('div',{id:'app'},this.msg)} - Diff 算法:只比较同级节点(双指针)
functionpatch(oldVNode,newVNode,container){// ① 类型不同 → 直接替换if(oldVNode.type!==newVNode.type){replaceNode(oldVNode,newVNode,container)return}// ② 类型相同 → 更新 props & 递归 childrenpatchProps(oldVNode.props,newVNode.props,el)patchChildren(oldVNode.children,newVNode.children,el)}
- diff算法核心策略:
- 同层比较(不跨层)
- 双指针头尾遍历子节点,O(n) 复杂度
- key 匹配 → 最大程度复用真实 DOM 节点
- Patch 过程:执行最小 DOM 操作,所有变更批量执行
- 所有变更批量执行,避免频繁 reflow(重绘)
- 组件级 Patch:函数式 & 异步
- 函数组件直接 patch() 返回结果
- 有状态组件创建/更新实例 → 调用 render() → 得到 VNode → 继续 patch
- 异步组件patch() 内部识别 Suspense 边界,等待加载完成后再继续 patch
5、讲述一下Vue中虚拟DOM的Diff算法
- 虚拟 DOM 的 Diff 算法可以概括为“同层比较 + 双端指针 + key 复用 + 最长递增子序列(LIS)”,把时间复杂度从 O(n³) 降到 O(n),只移动/创建/删除必要的 DOM 节点
- 虚拟DOM的 Diff 算法实现步骤如下:
- 总体入口:patch(oldVNode, newVNode)只同层比较,不跨层;类型不同立刻整节点替换,不再往下比
functionpatch(oldVNode,newVNode,container){if(oldVNode.type!==newVNode.type){replaceNode(oldVNode,newVNode,container)// 类型不同直接替换return}patchProps(oldVNode.props,newVNode.props)// 属性差异更新patchChildren(oldVNode.children,newVNode.children,container)// 子节点 Diff} - 子节点 Diff:patchKeyedChildren(核心)双端指针先吃掉两端相同节点,减少剩余待处理长度
letoldStart=0,oldEnd=oldChildren.length-1letnewStart=0,newEnd=newChildren.length-1// ① 双端预处理:头-头、尾-尾、头-尾、尾-头while(oldStart<=oldEnd&&newStart<=newEnd){if(isSameVNode(oldChildren[oldStart],newChildren[newStart])){patch(oldChildren[oldStart],newChildren[newStart],container)oldStart++;newStart++}elseif(isSameVNode(oldChildren[oldEnd],newChildren[newEnd])){patch(oldChildren[oldEnd],newChildren[newEnd],container)oldEnd--;newEnd--}else{break}// 预处理结束,跳出} - 剩余乱序:key 是身份证;通过 key 找旧节点, 避免销毁重建, 实现LIS复用
// ② 建立 key → oldIndex 映射constkeyToOldIndex=newMap()for(leti=oldStart;i<=oldEnd;i++){keyToOldIndex.set(oldChildren[i].key,i)}// ③ 遍历剩余新子序列,记录可复用旧节点constnewIndexToOldIndex=[]// 下标 = 新序列位置,值 = 旧序列位置letmoved=false,lastIndex=0for(leti=newStart;i<=newEnd;i++){constnewChild=newChildren[i]constoldIndex=keyToOldIndex.get