Vue开发中的JS核心功底:从响应式原理到事件循环
搞 Vue 开发这几年我最大的体会是真正卡住你的往往不是 Vue 本身的 API而是 JS 的基础功底。Vue 只是一个视图层的框架路由、状态管理、组件通信、响应式原理底层全是 JS 在撑着。今天我就把在 Vue 项目里真正高频用到的 JS 核心知识点完整盘一遍从环境配置到响应式原理从事件循环到业务实战每一块都是我在实际项目中踩过坑、验证过的东西。这篇文章适合刚学完 Vue 基础但写项目总卡壳的人也适合准备 Vue 面试想系统梳理 JS 考点的人。1. 先把地基打牢Vue 项目里的 JS 基础盘1.1 从环境配置到依赖安装初始化项目时就在用 JS 思维很多人觉得 Vue 环境配置就是一个npm install的事其实从你敲下npm create vuelatest开始JS 的核心概念就一直在参与。先说 Node.js 版本管理。Vue 3 和 Vite 对 Node 版本有硬性要求Vite 5 通常需要 Node 18版本不对的时候报错信息五花八门最常见的就一句ERR_OSSL_EVP_UNSUPPORTED。这个坑我踩过好几次后来统一用 nvm 管理 Node 版本项目根目录放一个.nvmrc文件写明18.19.0团队成员拉下代码后执行nvm use就能切到对应版本再也没为环境问题吵过架。再说依赖安装。npm install慢到怀疑人生的时候很多人会换 cnpm但 cnpm 的依赖嵌套结构和 npm 不一样偶尔会装出一些诡异的node_modules问题。我个人的习惯是npm 能装就 npm装不动就换 pnpm。pnpm 最大的特点是硬链接复用多个项目共享同一个依赖存储装包速度快磁盘占用小。但你需要注意pnpm 对依赖的隔离更严格你项目里用到但 package.json 没声明的幽灵依赖在 npm 下可能能用切到 pnpm 直接报module not found这个我后面在问题排查部分会细讲。还有环境变量。项目里有.env.development、.env.production这些文件Vite 底层用loadEnv去解析你在代码里写import.meta.env.VITE_API_BASE_URL本质上就是在操作 JS 模块系统。很多初学者不理解为什么变量名必须以VITE_开头因为 Vite 会把env文件里的变量挂到import.meta.env上非VITE_前缀的不会暴露给客户端代码这是出于安全的考虑——防止你误把密钥等敏感信息打进前端包。1.2 模板语法、渲染逻辑与 JS 基本功的对应关系Vue 模板里的{{ }}、v-if、v-for、:class、click看起来是模板语法实际上每一行背后都是 JS。举个最常见的例子v-for的key。很多新手喜欢用index作为 key结果列表删除某个元素后出现了勾选项串行的问题。原因很简单key是 Vue 做 diff 时的唯一标识用index意味着删除第一项后后面的每一项索引都变了Vue 以为整个列表都变了DOM 就跟着乱。用 JS 的思路理解就是——你用一个不可靠的标识去做对象匹配结果自然不可靠。所以我在项目里的约定是数据源里有id就用id没有id就生成一个唯一标识坚决不用index。再比如v-if和v-show的选择。v-if是真正的条件渲染false时直接不渲染这个节点对应 JS 里的短路求值v-show是始终渲染只是切换display: none对应 JS 里的切换属性。这里有个性能经验如果节点的显隐切换非常频繁比如 Tab 切换、弹窗开关用v-show更省性能如果某个组件只会在特定条件下出现比如权限按钮、错误提示用v-if更合适因为false时连创建组件的开销都省了。还有:class的动态绑定其实和 JS 的数组、对象操作是一回事。项目里经常写:class[isActive ? active-class : , base-class]用 JS 的思路理解这就是在拼接一个类名数组isActive为true时加active-classfalse时不加。更优雅的写法是用对象语法:class{ active-class: isActive, base-class: true }底层逻辑就是Object.keysfilter本质上是个Boolean判断题。2. 高频 JS 核心 APIVue 组件里的日常三件套2.1 字符串与数组判断includes、indexOf、some/find 的选型Vue 项目里写最多的逻辑是什么根据我的经验排第一的是判断有没有第二是找出某个对象。判断字符串是否包含我遇到过很多写法str.indexOf(xxx) ! -1、str.includes(xxx)、str.search(/xxx/)、str.match(/xxx/)。选哪个我的偏好是判断是否包含子串用includes(xxx)语义最清晰返回布尔值需要知道子串位置用indexOf(xxx)返回索引找不到是 -1需要正则匹配并捕获内容用match(/xxx/)返回匹配数组或 null需要全局替换用replaceAll(/xxx/g, yyy)注意正则必须加g标志这里有个includes和indexOf的细节includes在判断NaN时比indexOf更可靠因为indexOf用的是严格相等而NaN ! NaN所以[NaN].indexOf(NaN)返回的是 -1但[NaN].includes(NaN)返回true。Vue 项目里处理数组去重、判断存在性时这个细节能避免不少隐蔽 bug。在对象数组里查找元素很多人第一反应是find。它确实是找元素的首选但要注意它返回的是第一个匹配项如果没有找到则返回undefined。而some返回的是布尔值只关心是否存在不关心是哪一个。filter返回的是所有匹配项组成的数组经常用来做列表筛选。我在项目里的选型原则是方法返回结果适用场景示例includes布尔值判断基本类型数组是否存在某个值[a,b].includes(a)indexOf索引数字需要知道位置索引arr.indexOf(a)some布尔值对象数组判断是否存在满足条件项list.some(item gt; item.id 3)find匹配项或 undefined获取单个匹配对象list.find(item gt; item.id 3)filter匹配项数组筛选出所有满足条件的项list.filter(item gt; item.age gt; 18)2.2 map、解构与展开符数据处理三板斧Vue 项目里最常做的就是把后端返回的数据改造成前端要的形状。这个环节map、解构、展开符这三个 JS 特性几乎是必备武器。先说map。它的核心价值是把数组的每一项都映射成新的结构比如把后端返回的{ id, name, deptId }改造成下拉框需要的{ value, label }const options resData.map(item gt; ({ value: item.id, label: item.name, }));这里我想多提一句map的回调函数参数为(item, index, arr)凡事都可以用Array.map处理但并不是所有循环都适合。如果只是要列表的每个元素都操作一遍但不需要返回值用forEach更贴合语义。再说解构。Vue 的defineProps和defineEmits本身就在解构的语法里打转。实际业务里后端返回的数据往往嵌套很厚比如res.data.list你直接const { data: { list } } res就能一步到位。解构数组的场景也很常见比如上传成功后返回的[url]直接const [url] res就取到了。展开运算符...的应用场景更加广泛。复制对象时{ ...obj }合并对象时{ ...obj1, ...obj2 }在 Vue 里最常见的就是修改表单数据时保持响应性// 错误示范直接给 obj 加属性Vue 3 里虽然后期加了 proxy 可以监测新增属性 // 但为了代码可维护性最好还是用新对象赋值 form.value { ...form.value, status: 1 };这里背后有一个响应式原理的问题我放到下一章详细展开。现在你只需要记住一个结论在 Vue 3 里操作对象尽量用展开符生成新对象再赋值而不是直接改原对象属性这样既不会丢失响应性也方便做撤销回退。2.3 函数、闭包与 thisVue 3 组合式 API 里的 JS 考点Vue 2 时代的经典坑是this指向问题。methods 里写setTimeout(function(){ this.xxx 1 })拿到的this是window而不是组件实例因此很多人开始用var _this this的写法。Vue 3 组合式 API 彻底绕开了这个问题——因为组合式 API 的核心就是普通函数它不依赖this。但this的知识还是值得搞明白因为 Vue 3 内部、第三方库、JavaScript 原生 API 里到处都存在this绑定问题。我在面试别人时经常问普通函数和箭头函数的 this 有什么区别标准答案是普通函数的this取决于调用方式箭头函数的this取决于定义位置词法作用域。说得更直白一点箭头函数没有自己的this它沿用的是外层作用域的this。Vue 3 项目里还有一个重要概念和闭包强相关——watch 清理。比如你在watch里启动了定时器watch(source, (newVal, oldVal) gt; { const timer setInterval(() gt; { ... }, 1000); // 问题下次 watch 触发时上一个 timer 没被清理 });正确做法是使用onWatcherCleanup或者在回调里返回清理函数。Vue 的 watch 回调支持返回一个函数用于清理watch(source, (newVal, oldVal) gt; { const timer setInterval(() gt; { ... }, 1000); return () gt; clearInterval(timer); // 下次触发前会调用清理函数 });这就是闭包的典型应用——timer被清理函数捕获Vue 内部会在合适时机调用它。理解闭包你才能真正理解为什么这个变量在这个函数执行完之后还能被访问。3. 深入响应式从 Proxy 到手写 mini Vue3.1 Vue 3 响应式背后的 JS 原理Vue 3 之所以能把let data { count: 1 }变成修改 data.count 页面自动更新靠的是 ES6 的Proxy和Reflect。Proxy可以理解为一个拦截层。你访问对象属性、给对象属性赋值、删除属性时都会先经过get、set、deleteProperty等拦截函数。Vue 3 的reactive就是利用Proxy拦截了读取和写入操作读取属性时在get里记录谁在用这个属性依赖收集修改属性时在set里通知所有用这个属性的人数据变了触发更新为什么还需要Reflect因为Proxy的拦截函数在返回默认行为时用Reflect最稳妥。比如delete obj.a直接用delete操作符在某些严格模式下会报错但Reflect.deleteProperty(obj, a)返回布尔值行为更可控。另外get拦截中还需要处理this指向Reflect.get(target, key, receiver)的第三个参数receiver就能修正这一点。这里有一段极简的响应式实现思路理解了它你就理解了 Vue 3 的reactiveconst targetMap new WeakMap(); // 存储对象和其属性对应的依赖集合 function track(target, key) { // 记录 currentEffect 依赖了这个对象属性的 key } function trigger(target, key) { // 找到 target 下面 key 对应的所有 effect依次执行 } function reactive(target) { return new Proxy(target, { get(target, key, receiver) { track(target, key); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { const result Reflect.set(target, key, value, receiver); trigger(target, key); return result; }, }); }实际源码比我写的复杂得多但核心结构就是这样。你需要理解的是响应式的本质不是魔法而是 JS 语言能力Proxy和一种设计模式依赖收集/触发器的组合。3.2 手写 reactive、ref、effect、computed 的简化版标题里正好有一个词条是脱离 Vue 源码使用原生 Proxy 手写 reactive、ref、effect、computed我强烈建议每一个 Vue 开发者都动手做一遍这件事。做完之后你对为什么 ref 访问要写.value、为什么 computed 有缓存这些问题会有完全不同的理解。先说effect。它就是副作用函数的集合你可以理解为当数据变化时需要重新执行的函数。手写版本非常简练let currentEffect null; function effect(fn) { currentEffect fn; fn(); // 执行时触发 get完成依赖收集 currentEffect null; }然后是ref。为什么ref要包装成{ value: xxx }的结构因为Proxy只能代理对象没法直接代理原始值数字、字符串、布尔。Vue 的做法是把原始值打包成一个对象再用 reactive 去代理function ref(initValue) { return reactive({ value: initValue }); }这样ref(0)返回的是{ value: 0 }的响应式代理所以你访问时必须写count.value模板里自动解包是编译器帮你做的事情。接着是computed。它的本质是惰性 effect 缓存。在 getter 里缓存上一次的计算结果只有当依赖变化时才重新计算function computed(fn) { let cache; let dirty true; const obj { get value() { if (dirty) { cache fn(); dirty false; } return cache; }, }; // 每次依赖更新标记 dirty true return obj; }把这一套写完你会明白 Vue 框架的设计并不是玄学它就是在 JS 基础之上做了一层聪明的封装。之前我面试遇到一个人他对 Vue 3 源码如数家珍但让他脱离 Vue 源码去实现一个极简的响应式系统一下子就懵了。所以我一直觉得会背源码不如会写核心思想。3.3 为什么理解响应式能帮你排查项目 bug响应式原理不是纯粹为了应付面试它在项目排查里作用极大。举个我真实遇到的例子。有个同事在项目中用了addEventListener监听某个数据变化const state reactive({ count: 0 }); window.addEventListener(customEvent, () gt; { console.log(count changed); });问题来了customEvent事件和数据变化没有直接关系数据变化时不会触发任何自定义事件所以这行代码死都执行不到。他当时的困惑是Vue 是响应式的我改了 count应该自动触发这个监听啊。这就是不理解 Vue 响应式触发机制导致的因为响应式的通知范围只限在依赖收集阶段收集到的副作用函数一个没有进入任何 effect 的普通函数不会因为数据变化而自动执行。另一个高频坑是Vue 3 的 reactive 对象被解构后失去响应性。看这段代码const state reactive({ count: 0 }); const { count } state; count; // 页面不会更新原因也不复杂解构之后count是原始值已经脱离了Proxy代理。Vue 官方提供了toRefsAPI就是为了解决这个问题。理解 Proxy 的原理后你根本不需要背解构之后再赋值不响应这个结论你自然就知道原始值脱离了代理层怎么可能还有拦截呢。4. 路由、组件通信与透传JS 在 Vue 中的串联作用4.1 vue 路由与路由参数从 URL 到页面的 JS 逻辑Vue Router 是 Vue 生态里最核心的插件之一。它有两种路由模式hash和history。hash模式底层监听的是hashchange事件history模式底层依赖的是History API的pushState和replaceState加上popstate事件的监听。换句话说路由本身就是一个 JS 事件系统。项目里最常踩的坑是路由参数获取。很多人直接从组件里用route.query.id或者route.params.id但如果整个项目犯了一个错误——在模板中直接{{ $route.query.id }}在 Vue 3 中它会正常显示吗答案是可以。但$route对象在组件内被响应式代理了吗可以但不够直观。更好的写法是我在项目里定下的规则用useRoute获取参数并且把query和params区分清楚query对应 URL 中的?keyvalue刷新页面后依然存在params对应动态路径段/user/:id刷新后如果路由配置没匹配上容易丢失这里还有一个经典 bugparams只能配合name使用不能配合path。很多人写router.push({ path: /user, params: { id: 1 } })结果参数没传过去页面没报错但拿不到值。因为path拼接的是静态路径params不会自动拼进 URL。换成router.push({ name: User, params: { id: 1 } })才正确。4.2 组件通信中的 JS 思想props、emit、透传Vue 组件通信的底层逻辑其实就是 JS 里最基础的两件事传参和回调。父组件给子组件传props本质是函数传参子组件emit一个事件本质是调用父组件传入的回调函数。所以我在项目里经常说Vue 组件通信就是带参数的函数调用。透传这个词条值得展开讲一下。Vue 3 中没有在子组件props和emits中声明的属性或事件会自动挂到组件的根节点上这叫透传fallthrough。默认情况下class、style、id这些会被自动继承如果不想自动继承可以设置inheritAttrs: false。在封装第三方 UI 组件库的二次封装组件时透传非常常用——你需要把多余的属性和事件全部透传给底层的 el-input 或 a-input 组件。用v-bind$attrs实现透传的原理是 ES6 的展开运算符 动态绑定 事件透传的组合。本质上你是在做一次对象属性批量传参。这也是为什么理解 JS 的展开运算符能直接提升你写 Vue 组件的水平。4.3 Vue 与 React/Vue 与 Angular 的差异本质上是 JS 设计模式的差异热搜词里有vue和react的区别和angular和vue区别这两个话题我聊聊最核心的差异。Vue 和 React 最大的差别在响应式设计。Vue 的响应式是对数据对象做代理数据变了自动触发视图更新React 需要手动调用setState或者依赖 useState 的 setter 函数。这个差异的根源在于 JavaScript 语言的两种不同用法Vue 选择了 Proxy 拦截React 选择了不可变数据 函数式更新。没有谁更高级只有适不适合你的团队。Angular 和 Vue 的差别主要体现在架构的重量级上。Angular 是一个完整的框架自带依赖注入、RxJS、模块系统Vue 则更像一个视图库 渐进式框架你可以只引入核心库也可以一步步加入路由、状态管理工具。Angular 上手成本高是因为 Django 式的大而全需要你接受它的整套思维模式Vue 上手快则是因为它贴近原生 JS 的思维方式你只要会写 JS 加简单的模板语法基本就能开始写了。这个差异和 JS 本身的生态一样——没有银弹只有适合的场景。5. 事件循环Vue 项目里最难啃但最值得啃的 JS 概念5.1 Event Loop、宏任务与微任务Vue 异步更新的基石标题里的热搜词有js event loop过程和js宏我结合 Vue 来讲透这个知识点。JavaScript 是单线程语言但浏览器环境提供了一套任务调度机制叫做事件循环Event Loop。所有的 JS 代码被分为两种任务宏任务MacroTasksetTimeout、setInterval、I/O 操作、UI 渲染、MessageChannel等微任务MicroTaskPromise.then、queueMicrotask、MutationObserver等事件循环的处理顺序是先执行一个宏任务然后清空当前微任务队列再进行 UI 渲染然后再取下一个宏任务。微任务队列在每轮宏任务事件之后都会被清空这就是nextTick的原理基础。Vue 3 的nextTick为什么能拿到更新后的 DOM因为 Vue 的响应式触发更新时它不会同步地去更新 DOM而是把这次更新任务放进一个微任务队列等当前宏任务结束后再批量执行。所以你在同一轮的同步代码里修改了多个响应式数据Vue 会合并成一次 DOM 更新这就是 Vue 异步更新的机制。nextTick(callback)就是等到 DOM 更新完成后执行 callback的便捷写法。具体流程我用一个代码说明const count ref(0); async function update() { count.value 1; console.log(document.querySelector(#app).textContent); // 旧值 await nextTick(); console.log(document.querySelector(#app).textContent); // 新值1 }第一次console出来的是旧值因为 DOM 更新还没有执行await nextTick()之后微任务队列执行完了DOM 已经更新才能拿到新值。5.2 事件循环在项目中的应用场景理解了事件循环很多项目 bug 就能一眼定位。场景一表单校验后获取焦点。我在项目里写过一个校验失败后自动聚焦到第一个错误字段的功能同步代码写inputRef.focus()完全不生效因为此时 DOM 还没更新元素根本不存在。改成await nextTick(); inputRef.focus();就好了。场景二大数据列表的渲染卡顿。有一次我处理一个 5000 行的表格一次性v-for渲染页面直接卡死。事件循环的视角下这是单个宏任务执行时间过长导致的 UI 阻塞。我当时做了一个分批渲染优化把 5000 条数据分成 50 批每批 100 条用setTimeout(..., 0)或requestAnimationFrame分批插入每轮宏任务只处理 100 条页面每秒能保持 60 帧的流畅度。这就是对事件循环理解的实际应用。场景三多个接口请求的依赖顺序。项目里经常遇到先请求用户信息再根据用户信息请求菜单列表。如果你用的是Promise链或async/await走的其实都是微任务队列。理解这一点后你会明白为什么很多项目要用Promise.allSettled去并发处理多个请求而不是一个接一个地 await——因为微任务队列的执行顺序可以让多个并发请求在同一个事件循环周期内发起。5.3 从事件循环看 setTimeout 和 Promise 的执行顺序面试中必考的setTimeout和Promise顺序题放到 Vue 项目里其实也有现实映射。看这个例子setTimeout(() gt; { console.log(setTimeout); }, 0); Promise.resolve().then(() gt; { console.log(promise); });输出顺序是promise然后setTimeout原因是Promise.then是微任务当前宏任务也就是这段脚本本身执行完毕后浏览器会立刻清空微任务队列然后才轮到下一次宏任务也就是setTimeout的回调。Vue 项目里有一个真实碰撞如果nextTick内部实现的也是微任务而某个第三方库在nextTick之后又注册了一个setTimeout那么先 nextTick 再 setTimeout的先后执行顺序就是确定性的。我在封装组件时经常故意利用这个顺序await nextTick(); // 先等 DOM 更新 setTimeout(() gt; { // 再执行需要等待 DOM 完全渲染后的逻辑比如滚动定位、动画触发 }, 0);这样做的好处是nextTick只能保证Vue 的 DOM 更新已完成但浏览器渲染到页面还需要一个 UI 渲染步骤用setTimeout把逻辑推到下一个宏任务确保浏览器已经走了渲染流程。6. 业务场景实战JS 在真实 Vue 项目中的落地6.1 视频播放Vue 里播放 m3u8 流的两种方式热搜词里有vue播放m3u8我在直播管理后台项目里做过视频播放分享一下经验。m3u8是 HLS 协议HTTP Live Streaming的索引文件里面放了一串.ts分片文件的地址。播放它有两个前置条件一是浏览器原生 video 标签不支持 HLS 格式Safari 除外二是必须把.ts分片拼接起来才能连续播放。Vue 项目里最稳妥的方案是引入hls.js。这个库的核心逻辑是请求m3u8索引文件解析出.ts分片地址然后通过 Media Source ExtensionsMSE把分片喂给浏览器。使用方式import Hls from hls.js; let hls null; function playVideo(videoEl, url) { if (hls) { hls.destroy(); } if (Hls.isSupported()) { hls new Hls(); hls.loadSource(url); hls.attachMedia(videoEl); } else if (videoEl.canPlayType(application/vnd.apple.mpegurl)) { // 兼容 Safari 原生支持 videoEl.src url; } }方案二是用video.js配合videojs-contrib-hls插件。一些项目用video.js好看的主题就沿用这套方案。不过我现在更推荐hls.js因为它的 API 更贴近 JS 原生出错信息也更好排查。注意播放 m3u8 流时经常遇到跨域问题需要服务端在响应头里加Access-Control-Allow-Origin。如果播放卡顿可以先检查是不是网络问题再看分片的并发请求数设置hls.js的config里可以调maxBufferLength和maxMaxBufferLength等参数。6.2 复制粘贴从 execCommand 到 Clipboard API项目里点击复制功能很常见比如复制订单号、复制邀请链接。我的实现演进史是这样的第一版用document.execCommand(copy)先创建一个隐藏的textarea把要复制的内容塞进去选中内容后执行复制命令再删除这个临时元素。这个方案兼容性老到几乎所有浏览器都支持但体验不够好而且execCommand已经被标记为废弃 API。第二版换成navigator.clipboard.writeText(text)这个 API 更简洁async function copyText(text) { try { await navigator.clipboard.writeText(text); console.log(复制成功); } catch (err) { console.error(复制失败, err); } }但它有几个注意事项必须 HTTPS 环境下才能用而且会触发浏览器的权限弹窗、需要用户授权。本地开发时用localhost没问题线上必须确保证书是有效的 HTTPS。第三版做兼容降级优先用 Clipboard API不支持或不满足条件时降级到execCommand。判断条件写得干净一点function isClipboardSupported() { return navigator.clipboard amp;amp; window.isSecureContext; }复制功能看着简单但点击复制后提示用户已复制、复制成功后不清空选中的内容、iOS 上的兼容处理这些都是细节。我在移动端遇到过navigator.clipboard在 iframe 内不可用的问题最后是在顶层页面封装一个复制函数通过postMessage调用这个我在 6.5 节一起讲。6.3 验证 URL 有效性从正则到 URL 构造函数js验证url有效性也是热搜词。以前我做表单校验时喜欢写一长串正则什么^(http|https):\/\/[^\s]$但这种正则既看不懂也容易漏掉边缘情况。后来我换了一个更可靠的思路——直接用new URL()构造函数function isValidUrl(str) { try { const url new URL(str); return [http:, https:].includes(url.protocol); } catch (err) { return false; } }new URL(str)在传入的字符串不符合 URL 规范时会抛出TypeError所以用try...catch包起来就能完成判断。这比正则简单得多性能和正确率也更高。如果你还需要校验域名是否真的可访问那就得发一个请求去探测了但那是另一个话题。Vue 项目里使用它的场景很多用户输入活动链接、运营配置跳转地址、富文本编辑器里粘贴外链等统一用这个函数校验比维护一堆正则快多了。6.4 省市区三级联动数组与对象结构设计的 JS 思路js三级联动和省市区编码和名称js数据这两个热搜词我放在一起讲。三级联动本质是一个根据上一级选择结果筛选下一级数据的过程。最常见的数据结构是平铺数组 parentId关联或者嵌套对象。我更推荐用平铺数组 parentId。原因很简单数据更新时你只需要改一条数据做筛选时用filter或者find就能拿到所有子级。而嵌套对象结构在编辑回显时非常痛苦——你得递归遍历或者维护一个反向索引。实现思路大概是const regions [ { code: 110000, name: 北京市, parentCode: null }, { code: 110101, name: 东城区, parentCode: 110000 }, { code: 110102, name: 西城区, parentCode: 110000 }, // ... ]; function getChildren(parentCode) { return regions.filter(item gt; item.parentCode parentCode); }Vue 组件里维护三个数组provinceList、cityList、districtList选择省时根据provinceCode过滤出城市列表选择市时过滤出区县列表。核心就是一次filter没别的花头。这个功能真正要注意的是回显。编辑页面里需要根据已有的省市区编码反向找出完整的省市区名称。数据量小的时候可以直接遍历数据量大、层级深的时候最好在建表时把code和name的对应关系做成一个字典用Map结构来做 O(1) 的查询const regionMap new Map(regions.map(item gt; [item.code, item]));6.5 iframe 与父页面刷新通信的 JS 方案iframe关闭jquery并刷新父页面js这个热搜词我太熟了。之前做一个老系统的嵌入项目主系统是 jQuery 的老代码新模块用 Vue 开发以 iframe 形式嵌入。这里有一个经典需求Vue 子页面点击按钮后关闭这个 iframe同时刷新父页面。方案有两种方案一同域如果父子页面同源直接在子页面写window.parent.location.reload();方案二跨域跨域情况下无法直接操作父页面需要用postMessage。子页面发送消息parent.postMessage({ type: REFRESH, payload: { id: 1 } }, *);父页面添加监听window.addEventListener(message, (event) gt; { if (event.data.type REFRESH) { window.location.reload(); } });这里我必须强调一个安全点postMessage的第二个参数不要用*应该指定父页面的确切源origin父页面在监听时也要校验event.origin只处理来自可信来源的消息。否则你的页面很容易被其他页面通过 iframe 方式注入恶意消息。还有一个相关热搜词是webrtc vue使用。WebRTC 在 Vue 里的使用也是非常常见的比如音视频通话、屏幕共享。它的 JS 核心在于navigator.mediaDevices.getUserMedia获取本地媒体流RTCPeerConnection建立点对点连接。这里我不展开讲但在 iframe 通信时如果要把本地流传给父页面或兄弟 iframepostMessage同样是最常用的桥接方式。7. 常见问题与排查技巧实录7.1 Vue 打包后布局异常三个高频原因热搜词里有vue 打包后 布局异常这个问题我在项目里遇到过两次每次都让人想摔键盘。第一个原因是publicPath设置错误。默认情况下 Vite 构建的资源路径是绝对路径/assets/xxx.js如果你的项目部署在服务器子目录下而不是根目录资源路径会全部 404导致样式和 JS 加载失败页面布局直接垮掉。解决办法是在vite.config.ts里设置export default defineConfig({ base: ./, // 使用相对路径 });第二个原因是 CSS 中使用了非相对路径的背景图或字体文件。比如background: url(/images/bg.png)写的是绝对路径部署到子目录后找不到对应的图片。统一改用相对路径或者让打包工具处理资源引用。第三个原因是路由模式。使用history模式打包后部署到 nginx 如果没有配置try_files重写规则刷新某个子路由页面就会 404看起来像布局异常其实整个页面都没了。解决方案要么切回hash模式要么配置 nginxlocation / { try_files $uri $uri/ /index.html; }7.2 异步上传图片FormData 与接口联调的坑asp js 异步上传图片是一个偏后端的老技术话题但异步上传图片这五个字本质就是 JS 编写一个异步文件上传逻辑的过程。Vue 项目里最常见的异步上传方案是使用FormDataaxiosconst formData new FormData(); formData.append(file, fileObj); const res await axios.post(/api/upload, formData, { headers: { Content-Type: multipart/form-data }, });这里有几个很容易踩的坑其一Content-Type不要手动设置。你用 axios 时如果手动指定了multipart/form-data可能会丢失boundary参数服务端在解析时就会找不到文件边界导致上传失败。axios 会根据FormData自动生成正确的请求头你只需要在确实需要覆盖时再做设置。其二上传进度和取消。axios 支持onUploadProgress回调适合做进度条取消上传用AbortController这是一个比较新的 Web API底层就是AbortSignal。这两个功能在文件上传场景里基本是标配。其三文件大小限制和类型校验。前端校验${file.size / 1024 / 1024}MB是否超过限制用file.type.startsWith(image/)判断图片类型。这些判断全是 JS 的基础知识。7.3 配置了依赖却报 module not found幽灵依赖排查前面说到过 pnpm 的依赖隔离项目里有个同事用 pnpm 安装依赖后代码里直接import _ from lodash;但package.json里根本没有声明lodash。在 npm 的扁平化 node_modules 下这个代码可能能跑因为lodash被其他依赖装了并且提升到了顶层。切换到 pnpm 后这个幽灵依赖直接暴露问题报module not found。排查思路是先确认第三方依赖是否是间接依赖用pnpm why lodash查看依赖树来源如果项目确实需要直接用这个库就在 package.json 里显式声明依赖然后重新安装。如果只是某段代码误用了第三方库内部提供的类型最好还是用项目依赖的自身 API不要依赖第三方库的传递依赖。类似的坑还有版本不一致导致的 API 差异。比如两个子组件版本不同某个在 v3 里才有的方法在 v2 里不存在打包时不报错运行时报xxx is not a function。我的排查习惯是遇到not a function、undefined is not a function第一反应不是看代码逻辑而是先检查这个 API 是否存在、版本是否正确。7.4 其他高频问题速查表我在实际项目里还遇到一些高频问题整理成一张速查表供你们参考问题现象常见原因排查思路页面数据不更新给普通变量赋 import 的对象未用 ref/reactive检查变量是否为响应式代理组件 props 修改无效子组件直接修改 props用 emit 或者使用响应式内部 state路由切换后页面不刷新同一个组件实例被复用watch 路由参数或使用:key强制重建打包后文件体积过大未做代码分割动态 import 与路由懒加载定时器无限叠加watch/组件销毁时未清理使用onUnmounted或清理函数事件绑定后无法销毁addEventListener 未移除记录 handler 引用在onUnmounted中移除最后再分享一点个人经验做 Vue 开发五年多越来越觉得一项技能的决定性因素不是你会多少个框架 API而是你愿不愿意花时间去理解底层那层 JS。响应式看起来神奇背地里就是 Proxy 和几个普通函数异步更新看起来玄学本质就是事件循环的微任务队列组件通信看似繁琐说白了就是函数传参与回调。一次性把框架 API 全背下来只会越背越焦虑真正常用的就那几十个 JS 核心方法把每个方法的原理和边界条件摸透比多记一百个组件属性有用得多。后续如果你想继续扩展可以沿着这个方向走试着把nextTick的源码读一遍然后自己封装一个 small Vue 的运行时再试试用原生 JS 写一个小型响应式表格组件不依赖任何框架。做完这两个小项目你的 Vue 功力会有一个质的飞跃。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →