vue常见问题

vue常见问题 1、Vue响应式原理 核心Vue2和Vue3响应式原理的实现以及核心差异核心区别在于数据劫持的方式不同Vue2使用 ES5 的Object.defineProperty()来劫持对象的属性访问器。这就导致它有两个先天不足一是无法检测对象属性的添加和删除所以提供了Vue.set二是无法直接监听数组下标的变化因此 Vue2 不得不重写数组的 7 个变异方法push/pop等来实现响应式。Vue3则使用 ES6 的Proxy代理整个对象。Proxy 可以直接监听对象和数组的变化包括属性的动态添加、删除以及数组索引和长度的变更无需额外的 hack。配合Reflect使用性能和可维护性都更好。对比项Vue2Vue3优势响应式系统Object.defineProperty像给每个房间装监控新增房间就监控不到Proxy像给整栋楼装监控任何变化都能捕捉更高效、更灵活API设计选项式API (Options API)像做填空题代码被拆分到不同格子里缺点一个功能的代码分散在各处维护时要上下滚动查找组合式API (Composition API) 选项式API相关代码可以组织在一起优点同一功能的代码可以放在一起逻辑更清晰且兼容vue2代码组织更清晰复用性更好TypeScript支持一般原生支持开发体验更好Tree Shaking不支持支持打包体积更小多根组件不支持支持代码更简洁性能一般提升30%-50%运行更快内存占用更少代码组织方式Vue2选项式 APIOptions API// 像做填空题代码被拆分到不同格子里 export default { data() { return { count: 0, name: } }, methods: { increment() { this.count } }, mounted() { console.log(组件已挂载) } }⚠️ 缺点一个功能的代码分散在各处维护时要上下滚动查找Vue3组合式 APIComposition API// 相关代码可以组织在一起 import { ref, onMounted } from vue export default { setup() { const count ref(0) const name ref() const increment () count.value onMounted(() { console.log(组件已挂载) }) return { count, name, increment } } }✅ 优点同一功能的代码可以放在一起逻辑更清晰响应式原理底层核心Vue2Vue3Object.definePropertyProxy无法检测对象属性的添加/删除可以检测所有变化数组变化需要特殊处理数组变化自动监听嵌套对象需要深度遍历懒代理性能更好通俗理解Vue2 像给每个房间装监控新增房间就监控不到Vue3 像给整栋楼装监控任何变化都能捕捉根节点数量Vue2只能有一个根节点!-- ❌ 这样写会报错 -- template div内容1/div div内容2/div /template !-- ✅ 必须包一层 -- template div div内容1/div div内容2/div /div /templateVue3支持多个根节点片段!-- ✅ 可以直接写多个根节点 -- template div内容1/div div内容2/div /template生命周期钩子变化Vue2Vue3组合式API说明beforeCreatesetup()由setup替代createdsetup()由setup替代beforeMountonBeforeMount组件挂载前mountedonMounted组件挂载后beforeUpdateonBeforeUpdate更新前updatedonUpdated更新后beforeDestroyonBeforeUnmount合并了destroyedonUnmounted合并了v-model 升级Vue2v-model 只能用一个input v-modelvalue /Vue3支持多个 v-modelinput v-modelfirstName / input v-modellastName / !-- 或者自定义 -- Comp v-model:titletitle v-model:contentcontent /2、Vue 中组件的data为什么必须是函数这是由组件复用的机制决定的。如果data是一个对象那么当创建多个组件实例时它们会共享同一个内存地址。修改一个实例的数据会直接污染其他实例这在组件化开发中是致命的。将data定义为函数每次创建组件实例时即在组件的click或生命周期中Vue 都会执行这个函数并返回一个全新的对象副本。这样每个实例就能维护自己独立的状态互不干扰。而根实例new Vue({})只会有一个不存在复用所以允许是对象。3、v-if和v-show的区别及适用场景控制手段v-if是真正的条件渲染通过销毁和重建 DOM 树来控制显隐v-show无论初始条件如何元素总是会被渲染仅仅是通过 CSS 的display属性进行切换。性能开销v-if有更高的切换开销因为涉及 DOM 的增删但初始渲染条件为假时什么也不做初始渲染开销低v-show切换开销极低但初始渲染开销高无论显示与否都会渲染。场景建议如果需要非常频繁地切换如 Tab 切换、下拉菜单使用v-show如果在运行时条件很少改变如权限模块、用户登录状态使用v-if。4、v-for为什么要加key为什么不推荐使用index作为keykey 的作用key是 Vue 虚拟 DOM Diff 算法的唯一标识。它让 Vue 能精准地判断两个节点是否是同一个从而复用和重新排序现有元素减少 DOM 操作次数提升性能。为什么不推荐 index当列表进行逆序添加、删除或排序时元素的index会随之改变。如果使用index作为keyVue 会误以为所有的项都变了导致原地复用策略失效。这不仅会引发性能下降更严重的是如果列表项包含输入框或组件状态会导致状态错乱例如原本第一行的输入框内容跑到了第二行。必须使用唯一且稳定的 id如数据库中的id作为key。5、通常在哪个生命周期发起异步请求为什么通常在created或mounted中发起。created调用较早此时虽然 DOM 还未挂载但组件的实例已经初始化完成data和methods已经可用。在这里发请求可以更早地获取数据减少页面白屏时间但由于无法操作 DOM不适合需要操作 DOM 的场景。mounted此时DOM 已经挂载完成。如果请求回来的数据需要直接操作 DOM例如初始化图表、获取元素宽高则必须在这里进行。注意不要在beforeCreate中发因为数据还没初始化也不要在beforeUpdate或updated中发容易导致死循环。6、nextTick的作用和其原理是什么作用$nextTick是 Vue 提供的异步更新队列机制。它允许你在DOM 更新完成后执行回调函数从而获取到最新的 DOM 状态。使用场景最典型的是在修改数据后需要立即操作更新后的 DOM比如获取滚动高度、聚焦输入框。由于 Vue 是异步更新 DOM直接写在该数据修改语句后面的代码无法获取最新 DOM此时需将操作放入nextTick中。原理Vue 的数据更新不会立即同步渲染而是开启一个队列。nextTick本质上是将回调函数放入这个队列的后面。Vue 内部会优先使用Promise.then微任务来实现如果浏览器不支持则降级为setTimeout宏任务。7、 Vue 性能优化路由懒加载配合 Webpack 的动态导入 (() import(...))将不同路由对应的组件分割成独立的 chunk只在访问对应路由时才加载减少首屏加载资源大小。v-for 配合 v-if 的优化避免两者在同一元素上同时使用。如果确实需要过滤列表采用计算属性提前过滤数据再遍历计算属性。函数式组件对于纯展示、无状态的组件使用函数式组件Vue2或直接使用模板Vue3 自动优化减少实例化开销。防抖与节流防抖和节流都是用于限制函数执行频率的优化手段但机制不同。防抖Debounce高频触发后延迟执行期间再触发则重置计时确保只响应最后一次如搜索框输入节流Throttle固定时间间隔内强制只执行一次稀释触发频率如滚动加载、窗口resize。✨记忆点防抖 电梯关门等无人再按才走节流 地铁到站固定时间开门。虚拟滚动针对长列表使用vue-virtual-scroller之类的库只渲染可视区域内的 DOM 节点避免 DOM 节点过多导致页面卡顿。8、JavaScript 的事件循环事件循环是 JavaScript 在单线程环境下实现异步编程的核心机制。简单来说它负责协调调用栈、任务队列宏任务队列和微任务队列之间的关系确保异步操作的回调能够在合适的时机被执行而不会阻塞主线程。它的执行流程可以概括为先执行当前调用栈中的所有同步代码。当调用栈清空后立即执行所有微任务比如 Promise.then 的回调直到微任务队列为空。如果微任务执行过程中产生了新的微任务也会继续执行。然后从任务队列中取出一个宏任务比如 setTimeout 的回调执行。重复第 2、3 步形成一个持续的循环。生活例子奶茶店点单想象你主线程在奶茶店点单同步任务你直接告诉店员要一杯珍珠奶茶代码执行店员马上记下来立即执行。异步任务奶茶需要制作时间耗时操作店员给你一张小票注册回调函数让你去旁边等不阻塞你后续点单。任务队列旁边等待取餐的顾客回调函数排成一队。微任务队列VIP 会员的加急订单Promise.then他们可以插队到普通顾客前面。流程你点完单同步代码执行完。店员先看有没有 VIP 加急订单微任务队列有的话先给他们做。然后按顺序叫号任务队列叫到号的顾客取餐执行回调。每叫完一个号再检查是否有新的 VIP 加急订单新一轮微任务如果有先处理它们再叫下一个号。如此循环直到所有队列清空。银行柜台调用栈柜员当前正在办理的业务。任务队列普通排队等候的顾客宏任务。微任务队列VIP 客户微任务可以插队但必须等当前业务办完才能插队而且插队时如果有多个 VIP他们也得按顺序来。代码示例console.log(1); // 同步任务 setTimeout(() { console.log(2); // 宏任务 }, 0); Promise.resolve().then(() { console.log(3); // 微任务 }).then(() { console.log(4); // 微任务 }); console.log(5); // 同步任务执行顺序分析执行同步代码先打印1再打印5。同步代码执行完检查微任务队列执行第一个微任务打印3该微任务又产生一个新微任务接着执行打印4。微任务队列清空取出宏任务执行setTimeout回调打印2。最终输出1, 5, 3, 4, 2。9、WebSocket 与 HTTP(S) 的区别及通信原理HTTP(S)超文本传输协议是无状态、单向的应用层协议。客户端主动发起请求服务器被动响应完成后连接通常关闭HTTP/1.1 支持 keep-alive 但仍是半双工。WebSocket是 HTML5 开始提供的全双工、双向通信协议建立在 TCP 之上。客户端和服务器都可以主动向对方发送数据连接一旦建立会保持打开状态直到任意一方主动关闭。通信机制对比特性HTTP(S)WebSocket通信模式请求-响应客户端必须主动请求全双工双方均可主动推送连接方式短连接每次请求新建连接或 keep-alive 复用长连接握手后保持连接协议标识http:// / https://ws:// / wss://数据格式文本通常为 JSON/XML/HTML文本或二进制帧头部开销每次请求携带大量头部如 Cookie、User-Agent连接建立后头部开销极小仅需少量控制帧实时性差需轮询或长轮询模拟高真正实时HTTP(S) 如何通信客户端浏览器发起 TCP 连接HTTPS 额外 TLS 握手。发送 HTTP 请求报文包含请求行、头部、空行、可选主体。服务器处理请求返回 HTTP 响应报文。连接关闭或 keep-alive 等待下一个请求。若需实时数据只能通过轮询每隔一段时间发请求或长轮询服务器挂起请求直到有数据才响应但效率低、延迟高。WebSocket 如何通信握手阶段客户端发起 HTTP 请求包含Upgrade: websocket和Connection: Upgrade头部以及Sec-WebSocket-Key随机值。服务器响应 101 Switching Protocols返回Sec-WebSocket-Accept确认升级连接协议从 HTTP 切换为 WebSocket。数据传输阶段连接保持双方可随时发送数据帧文本或二进制。任意一方可主动关闭连接发送关闭帧。场景在线聊天室如微信网页版。使用 HTTP(S)假设 A 发送消息给 BA 的浏览器必须通过 HTTP POST 将消息发给服务器服务器存储后B 的浏览器需要不断轮询服务器“有新消息吗”。如果轮询间隔 1 秒消息延迟最多 1 秒但服务器压力大大量无效请求且若轮询间隔长则延迟高。这就好比你每隔一分钟去邮局问“有我的信吗”即使没信也要跑一趟。使用 WebSocketA 发送消息后服务器立即通过已建立的 WebSocket 连接主动推送给 B 的浏览器。B 无需询问消息瞬间到达。这就像你和朋友之间拉了一根专用电话线随时可以说话对方也能随时回应实时性强且资源消耗低。10、Vue Router的两种模式hash和history有什么区别如何选择Vue Router有两种路由模式hash模式和history模式hash模式URL包含#号例如http://example.com/#/home优点不需要后端配置任何服务器都能支持缺点URL不美观有#号适用场景内嵌页面、演示项目history模式URL无#号例如http://example.com/home优点URL更规范美观缺点需要后端配置将所有非静态资源请求重定向到index.html在开发电商后台管理系统时可以选择了history模式因为URL需要简洁专业。如果遇到刷新出现404问题。通过Nginx配置解决location / { try_files $uri $uri/ /index.html; }这样所有非静态资源请求都会重定向到index.htmlVue Router就能正确处理路由了。适用场景官网、后台管理系统选择标准内嵌页面、演示项目用Hash模式官网、后台管理用History模式混合APP根据客户端能力选择11、Vue Router 的路由守卫有哪些路由守卫说白了就是路由跳转过程中的“关卡”主要分为三大类全局守卫如router.beforeEach、路由独享守卫如beforeEnter和组件内守卫如onBeforeRouteLeave。在我负责的后台管理系统中最常用的是全局前置守卫来做权限控制。例如每次跳转前在beforeEach里检查用户是否登录以及他的角色权限如管理员、普通用户是否允许访问目标页面。如果没有权限就拦截并跳转到404或首页 。// 通俗例子就像进小区大门要刷卡不同楼层还要再刷不同的卡router.beforeEach((to, from, next) {const token localStorage.getItem(token);if (to.meta.requiresAuth !token) {next(/login); // 没带卡先去办卡登录} else {next(); // 有卡放行}});12、动态路由参数如何获取组件复用时如何处理数据更新动态路由定义使用冒号语法如{ path: /user/:id, component: UserDetail }。获取参数方式选项式APIthis.$route.params.id组合式APIconst route useRoute(); const userId route.params.id关键点当路由参数变化时组件会被复用不会重新创建所以需要监听路由变化。例子在商品详情页开发中遇到问题从商品A跳到商品B时页面内容没更新。原因是组件实例没有重新创建。解决方案1.监听路由参数变化watch: { $route.params.id: { handler(newId) { this.fetchProduct(newId); }, immediate: true // 初始化时也执行 } }2.或使用路由守卫Vue3推荐import { onBeforeRouteUpdate } from vue-router onBeforeRouteUpdate((to, from) { fetchProduct(to.params.id); })这样当URL变化时就能正确获取新商品的数据。13、如何响应路由参数的变化当使用路由参数时比如从/user/1跳转到/user/2组件实例默认会被复用这虽然性能好但意味着created或mounted钩子不会再次触发。为了解决这个问题有两种简单办法在目标组件中使用watch监听$route对象的变化一旦参数变了就重新获取数据。使用 Vue 3 组合式 API 中的onBeforeRouteUpdate路由守卫。// 通俗例子同样是“用户详情”页面只是把id从1换成了2页面内容得刷新watch(() route.params.id, (newId) {fetchUserData(newId); // id变了重新请求新用户的数据});14、Vue 里常用的修饰符有哪些事件修饰符最常用的是.stop阻止冒泡和.prevent阻止默认行为。比如click.stop防止点击父元素触发submit.prevent阻止页面刷新。表单修饰符v-model.trim自动过滤输入框首尾空格v-model.number自动将字符串转成数字v-model.lazy将默认的 input 事件改为 change 事件也就是在失焦或回车时才更新数据能减少频繁更新。键盘修饰符keyup.enter监听回车键常用于搜索框。-- 通俗例子表单输入时的各种小便利 -- input v-model.trimusername placeholder用户名前后不能有空格 / input v-model.numberage typenumber / button click.preventsubmitForm提交/button !-- 防止点击后刷新页面 --15、Vue的Webpack和Vite核心区别开发体验的速度差想象一下在餐厅点餐Webpack像传统中餐厅厨师必须把所有菜都提前做好你点完菜才能上桌启动慢热更新慢Vite像日式快餐店你点什么就立刻做什么不用等启动快热更新快Webpack传统大厨模式特点需要先打包整个应用类似厨师把所有菜都做好项目越大打包时间越长配置复杂需要写大量配置文件热更新体验修改组件后需要等待Webpack重新打包整个应用可能要5-10秒Vite现代快餐模式特点利用浏览器原生ES模块按需编译点什么做什么冷启动几乎瞬间完成配置简单开箱即用热更新体验修改组件后只重新编译这个组件几乎瞬间更新为什么Vite这么快Webpack打包所有代码 → 启动 → 热更新需要重新打包Vite启动时只预构建第三方依赖一次浏览器请求哪个文件Vite才编译哪个文件修改文件时只更新修改的部分适用场景场景WebpackVite项目规模大型项目中小型项目开发体验一般极佳配置复杂度高低适合技术栈传统架构、复杂生态Vue/React现代框架项目类型大型SPA、MPA中小型SPA、SSR总结Webpack功能强大适合大型项目但开发体验慢Vite开发体验极佳适合现代Vue/React项目启动和热更新快10倍以上用个比喻Webpack像老式汽车功能齐全但启动慢Vite像电动车启动快驾驶体验好特别适合日常通勤开发16、ES6新特性let和const替代var最基础、最重要的特性解决了var的三大问题变量提升、函数作用域模糊、可重复声明。var像一个公共储物柜谁都可以打开修改let/const像个人保险箱只能在特定区域块级作用域打开。// ES5 (var的问题) var a 1; var a 2; // 没有报错直接覆盖了 if (true) { var a 3; } console.log(a); // 输出3变量作用域混乱 // ES6 (let/const) let b 1; // let b 2; // 报错Identifier b has already been declared if (true) { let b 3; // 块级作用域不影响外面的b console.log(b); // 输出3 } console.log(b); // 输出1 const PI 3.14; // PI 3.1415; // 报错Assignment to constant variable const obj { name: ES6 }; obj.name JavaScript; // 正常对象内部属性可以修改箭头函数简化函数写法解决this指向问题。传统函数像随身听谁用谁决定内容箭头函数像固定歌词的歌歌词内容固定不会因为谁唱而改变。// 传统函数 function add(a, b) { return a b; } // 箭头函数简化版 const add (a, b) a b; // 解决this指向问题经典场景定时器/回调函数 const person { name: Tom, sayHi: function() { setTimeout(function() { console.log(Hi, Im ${this.name}); // 输出Hi, Im undefinedthis指向错误 }, 1000); } }; // 用箭头函数解决 const person { name: Tom, sayHi: function() { setTimeout(() { console.log(Hi, Im ${this.name}); // 输出Hi, Im Tomthis指向正确 }, 1000); } };模板字符串告别繁琐的字符串拼接支持多行和嵌入表达式。ES5字符串拼接像拼图需要一块一块拼ES6模板字符串像现成的画直接把内容嵌进去。// ES5 (字符串拼接) var name Alice; var age 20; var message Hello, my name is name and Im age years old.; // ES6 (模板字符串) const name Alice; const age 20; const message Hello, my name is ${name} and Im ${age} years old.;解构赋值快速从数组/对象中提取值减少样板代码。传统方式像打开冰箱找东西需要一个一个翻解构赋值像直接拿指定盒子一目了然。// 传统方式 const user { name: Alice, age: 20, city: Beijing }; const name user.name; const age user.age; const city user.city; // 解构赋值ES6 const { name, age, city } user; console.log(name, age, city); // Alice 20 Beijing // 数组解构 const arr [1, 2, 3]; const [first, second, third] arr; console.log(first, second, third); // 1 2 3 // 跳过中间项 const [a, , b] [10, 20, 30]; console.log(a, b); // 10 30 // 默认值 const { name, age, gender female } { name: Alice, age: 20 }; console.log(gender); // female默认参数告别繁琐的参数默认值判断。ES5像点菜时要主动说不要辣ES6像菜单上直接标出默认口味不用多问。// ES5 (需要手动判断) function greet(name, age) { name name || Guest; age age undefined ? 20 : age; console.log(Hello, ${name}. You are ${age} years old.); } // ES6 (直接在参数中设置默认值) function greet(name Guest, age 20) { console.log(Hello, ${name}. You are ${age} years old.); } greet(); // Hello, Guest. You are 20 years old. greet(Alice); // Hello, Alice. You are 20 years old. greet(Bob, 30); // Hello, Bob. You are 30 years old.扩展运算符...灵活处理数组和对象扩展运算符像万能连接器可以把不同的东西轻松连接在一起// 数组合并 const arr1 [1, 2, 3]; const arr2 [4, 5, 6]; const arr3 [...arr1, ...arr2]; console.log(arr3); // [1, 2, 3, 4, 5, 6] // 对象合并 const obj1 { a: 1, b: 2 }; const obj2 { c: 3, d: 4 }; const obj3 { ...obj1, ...obj2 }; console.log(obj3); // {a: 1, b: 2, c: 3, d: 4} // 函数参数 function sum(...numbers) { return numbers.reduce((acc, curr) acc curr, 0); } console.log(sum(1, 2, 3, 4)); // 10运用ES6特性优化现有代码在用户列表页面可以使用解构赋值和模板字符串优化数据处理和显示代码使代码更简洁易读。// 优化前 function renderUser(user) { const name user.name; const age user.age; const city user.city || Unknown; return div name ( age ) from city /div; } // 优化后 function renderUser({ name, age, city Unknown }) { return div${name} (${age}) from ${city}/div; }为什么在Vue中推荐使用箭头函数因为箭头函数的this继承自外层作用域避免了在Vue方法中this指向问题使代码更简洁可靠。// 在Vue组件中 methods: { // 传统函数需要绑定this handleClick: function() { console.log(this.message); // 有效 }, // 箭头函数自动继承Vue实例的this handleClick: () { console.log(this.message); // 无效this不是Vue实例 } }注意在Vue组件中方法应该用普通函数而不是箭头函数因为箭头函数的this不会绑定到Vue实例。