ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Vue2进阶:从MVVM到核心指令与响应式原理

Vue2进阶:从MVVM到核心指令与响应式原理 这两年我面试过不少前端发现一个现象很多人简历上写着“熟悉 Vue2”但聊到 MVVM 到底怎么落地、核心指令各自的边界和适用场景、响应式系统的依赖收集过程就开始含糊。更常见的场景是新人一上来就用 Vue3 组合式 API等真到维护老项目时看到满屏 option 写法反而懵了。这篇我就好好讲一讲 Vue2 从 MVVM 到核心指令这条完整链路。内容定位是入门进阶目标是让你看完之后既能理解 Vue2 的底层组织逻辑也能直接上手写模板和业务逻辑不再只停留在“会用”的层面。先说明一下示例里涉及 Vue2 的版本以 2.6 / 2.7 为主部分兼容性说明会顺带提一句 Vue3方便你脑子里建立对比。1. MVVM 不是算法而是 Vue 组织代码的方式很多初学者一听到 MVVM 就紧张觉得这是某种高深设计模式。其实你完全可以把它当作一个代码组织结构来看数据、视图、业务逻辑三者各管各的再由框架自动建立数据和视图之间的联动关系。Vue2 官方也把 MVVM 当作基础模型写进了文档——虽然它内部并不是教科书式的标准 MVVM 实现但不妨碍我们用这个模型去理解框架的边界。1.1 数据驱动视图而不是手动操作 DOM早年用 jQuery 写页面常见流程是监听事件 → 读表单数据 → 手动改 DOM。页面大一点代码就变成一团乱麻你根本分不清状态在哪里流转。Vue2 想解决的问题很直接把“视图状态”抽成一份 JavaScript 数据对象让视图跟着数据走。数据驱动之后你的关注点从 DOM 操作转移到了数据维护。比如页面里有一段文字需要根据用户登录状态变化你只需要维护一个userInfo对象模板里写清楚显示规则剩下的事情 Vue 来处理。MVVM 在这里的意义是让视图不再是一块需要反复修补的画布而是数据的投影。1.2 Vue2 里三个角色的划分把 MVVM 对应到 Vue2 的具体代码你会发现角色分得非常清晰View视图层模板部分通常是template选项或.vue文件里的template块负责声明式地描述最终渲染结果。Model数据层data函数里返回的对象以及computed、props等派生数据承载业务状态。ViewModel视图模型Vue 实例本身。它监听数据变化、解析模板指令、调度渲染更新把用户对视图的交互同步回数据再把数据变化反映到视图。ViewModel 这一层你去翻源码核心就是响应式系统、虚拟 DOM 渲染器和事件绑定机制。它们组合起来完成了“数据变 → 视图变”和“视图交互 → 数据变”的双向流转。2. 响应式系统MVVM 落地的核心依赖MVVM 听起来只是个概念真正让 Vue2 跑起来的是它的响应式系统。理解这一块你就明白了为什么修改一个data里的字段页面会自动更新也理解了 Vue2 数组下标修改为什么失效。2.1 Object.defineProperty 对数据做了什么事Vue2 的响应式核心是Object.defineProperty。初始化组件时Vue 会遍历data返回对象的所有属性用这个 API 把它们改造成带有 getter 和 setter 的存取器属性。这个过程可以理解为给每个字段装上了“监听器”读取字段时getter 被触发Vue 会记录“当前谁正在使用这个字段”。修改字段时setter 被触发Vue 会对比新值与旧值不一样就通知依赖该字段的视图做更新。用代码辅助理解简化后的思路是这样的let value hello const obj {} Object.defineProperty(obj, message, { enumerable: true, configurable: true, get() { // 这里是依赖收集阶段 return value }, set(newValue) { if (newValue ! value) { value newValue // 这里是派发更新阶段 notify() } } })这个 getter / setter 就是响应式的入口也是 Vue2 无法检测对象新增属性的根源——因为新增属性没有经过defineProperty处理自然不会触发更新。2.2 依赖收集到视图更新的完整链路完整的更新流程我建议你按五步去拆组件渲染时会执行一个“渲染函数”即 render 函数渲染过程中会读取模板里用到的数据字段。读取操作触发 gettergetter 把当前组件的渲染 watcher 收集为这个字段的依赖。后续如果某个事件或请求修改了该字段setter 被触发。setter 通知依赖列表里的 watcher说“你们关心的数据变了”。watcher 收到通知后重新执行渲染函数生成新虚拟 DOM再与旧虚拟 DOM 对比最后把差异更新到真实 DOM。这中间用到的机制就是典型的观察者模式。我形容它像是一个小型的消息订阅系统数据是被订阅的主题视图是订阅者Vue 实例负责管理订阅关系。2.3 数组和新增属性的响应式“盲区”一定要记住这是 Vue2 面试高频点也是实战最容易踩的坑Vue2 无法检测到“直接通过下标修改数组”和“给对象新增属性”。为什么因为Object.defineProperty只能拦截已存在的属性。数组的索引虽然本质也是属性但 Vue2 出于性能考虑没有对每个索引做拦截而是通过重写数组的 7 个变更方法push、pop、shift、unshift、splice、sort、reverse来实现响应式。所以正确写法不是this.list[0] a而是this.$set(this.list, 0, a)。对象新增属性同理用this.$set(obj, key, value)。理解了底层原理再记这条规则就不会忘记了。3. 核心指令怎么选v-if、v-show、v-for、v-text、v-html进入模板语法部分。Vue2 内置了一套指令系统指令本质是模板与数据之间的“粘合剂”。我挑实战最常用的几个逐个讲清楚每个都说明适用边界和性能考量。3.1 v-if 与 v-show原理差异决定选型很多新手把v-if和v-show当成“都能控制显示隐藏”的同类指令实际差别很大v-if是真正的条件渲染。值为假时Vue 不会渲染这个元素到 DOM连 DOM 节点都不存在。切换时元素创建/销毁代价较高。v-show是通过 CSS 的display属性控制显示隐藏元素始终存在于 DOM 中只是被藏起来了。选型原则我通常这么定切换频率低、初始条件几乎确定不变的元素用v-if避免无意义的 DOM 挂载。高频切换的 tab、弹窗、下拉框内容用v-show省去重复创建销毁的性能开销。还有一点容易忽略v-if有更高的切换消耗v-show有更高的初始渲染消耗。如果一个元素初始就是隐藏的、且大概率不会显示用v-if更划算如果初始就需要渲染只是根据操作显示隐藏用v-show更合理。3.2 v-for 列表渲染key 不是给开发者看的v-for用于列表渲染基础用法是ul li v-for(item, index) in list :keyitem.id{{ item.name }}/li /ul:key经常被忽略但它直接关系到 Vue 的 diff 性能。虚拟 DOM 比较新旧列表时如果 key 稳定且唯一Vue 能精确判断哪些节点是新增、删除、移动如果没有 key 或用了 index 当 key遇到列表中间插入数据时Vue 可能会复用错误的节点引发状态错乱。比如列表里有输入框你往列表头部插了一条数据用 index 当 key 时原第一行的输入框内容会“传染”给第二行因为 Vue 以为它们是同一个元素。用唯一 id 做 key 就没有这个问题。还要提醒一句v-for的优先级在 Vue2 里高于v-if。同一个元素上同时写这两个指令v-if也会遍历全部列表只是跳过不渲染。如果列表很大建议用 computed 先过滤数据再单独用v-for渲染而不是写在一起。3.3 v-text、v-html 和插值表达式安全边界要分清默认的插值写法{{ message }}在数据变化时会自动更新文本内容。v-text和它的区别是前者会覆盖元素内部全部内容后者只替换插值位置实战中大多数用插值就够了。v-html需要格外小心。它能直接渲染 HTML 字符串但同时也意味着 XSS 风险。用户输入、接口返回的富文本直接丢给v-html等于把脚本执行权交了出去。如果业务确实需要渲染富文本务必先做内容清洗过滤可执行脚本和恶意标签。能用插值和v-text就尽量不要碰v-html。4. 事件与数据绑定v-bind、v-on、v-model 的进阶细节指令里最高频的其实是绑定类指令。讲清楚v-bind、v-on、v-model各自干什么模板代码你基本就看得八九不离十了。4.1 v-bind 动态绑定class 和 style 的特殊语法v-bind的作用是动态绑定属性简写是冒号。绑定普通属性很简单img :srcimageUrl :altimageAlt /class 和 style 有增强语法div :class{ active: isActive, disabled: isDisabled }/div div :class[base, isActive ? active : ]/div对象语法支持根据布尔值动态增删 class数组语法可以组合多个条件 class两者还能混用。style 绑定类似对象属性名可以用驼峰或短横线Vue 会自动加前缀处理兼容性。这里有个实际经验的点class 绑定里不要把逻辑写得太复杂。如果表达式超过一行才能说清楚就应该提取到 computed 里模板保持可读性。4.2 v-model双向绑定的语法糖v-model本质是语法糖它在不同表单元素上绑定的事件不同input、textarea默认监听input事件并更新 value。checkbox、radio监听change事件并更新 checked。select监听change事件并更新 value。所以v-modelmessage等价于input :valuemessage inputmessage $event.target.value /理解了这一点你就能解释一个经典问题为什么v-model不能直接用中文输入法的组合态因为它依赖 input 事件输入法组词过程中也会触发 value 更新但 Vue 又没法区分你是在组词还是最终选中。实际项目中如果遇到搜索框的拼写体验问题通常要手动处理 compositionstart / compositionend 事件。自定义组件上用v-model时默认接收的 prop 是value发射的事件是inputthis.$emit(input, newValue)子组件里通过props: [value]接收。这个默认规则可以改组件的model选项可以定制 prop 名和事件名。4.3 v-on 事件绑定修饰符的作用v-on简写是绑定事件时可以串联修饰符比如button click.stophandleClick停止冒泡/button form submit.preventhandleSubmit阻止默认行为/form input keyup.enterhandleEnter /.stop相当于调用event.stopPropagation().prevent相当于event.preventDefault()按键修饰符.enter帮你过滤了非回车按键。修饰符不是花架子它们能让模板更声明式、减少方法里的 DOM 事件细节处理。关于修饰符我建议重点掌握.stop、.prevent、.self、.once、.native、按键修饰符。尤其.native在 Vue2 中监听组件根元素的原生事件需要它但这个能力在 Vue3 中被移除了改用了emits声明。5. Vue2 和 Vue3 的差异学旧框架时也该搭的新框架认知输入内容里搜热词也总能看到 Vue2 和 Vue3 对比。学 Vue2 的过程中穿插了解 Vue3 的差异不是为了两边混淆而是为了建立完整的框架演进认知。5.1 代码组织方式Options API 与 Composition APIVue2 主推 Options API代码按data、computed、methods、watch分块组织。同一个业务逻辑的代码会被拆散到各个选项中组件小还好组件变大时维护成本上升。Vue3 提供了 Composition API你可以按业务功能把相关逻辑聚在一起用setup函数或script setup配合组合式函数复用逻辑。两者不是替代关系而是互补简单场景用 Options API 同样清晰复杂逻辑用 Composition API 更好聚合。5.2 响应式原理Object.defineProperty vs Proxy这里对应 2.1 的内容。Vue3 把响应式底层换成了 Proxy可以从代理层面监听对象属性的新增、删除、数组下标操作不再依赖$set。这意味着很多 Vue2 的“响应式盲区”在 Vue3 中直接消失了。但因为 Proxy 无法兼容 IE11 及更早浏览器如果你需要维护老版本浏览器用户Vue2 依然是合理选择。这也是很多企业至今不愿迁移的原因之一。5.3 模板与指令层面的调整v-if的优先级在 Vue3 中高于v-for和 Vue2 相反。v-model在 Vue3 上可以绑定多个值比如v-model:title、v-model:content替代了原来.sync修饰符的功能。Vue3 组件模板可以有多个根节点Vue2 组件模板要求单一根元素。全局 API 的挂载方式有变化Vue2 的Vue.use()、Vue.component()在 Vue3 中改成了从vue包导入createApp再在 app 实例上调用use、component。这些差异对开发者最直接的影响是网上搜到 Vue2 写法时要留个心眼是不是可以直接套到 Vue3 项目里。6. 实战中不得不说的版本坑与排查方法最后要聊的是 Vue2 生态里特别容易踩的第三方库版本坑。标题里提到过vue-ueditor-wrap版本冲突、vue-konva只在单个页面使用、以及 PDF 预览、threejs 与 d3js 混用等场景这里我把通用处理思路讲出来。6.1 vue-ueditor-wrap 安转时版本冲突的处理思路vue-ueditor-wrap是 UEditor 的 Vue2 封装组件很多后台管理系统都在用。按标题热词看搜索“vue2 安装 vue-ueditor-wrap 版本冲突”的人不少说明这个问题确实普遍。冲突的典型场景是项目里已经装了某个版本的vue或相关依赖vue-ueditor-wrap的 peerDependencies 要求另一个版本范围导致 npm 安装时直接报 ERESOLVE 错误。遇到这种问题我的排查步骤是看报错信息定位是哪个依赖和哪个依赖冲突。用npm ls vue-ueditor-wrap查看当前安装的版本判断版本号是否符合项目要求。如果只是 peerDependencies 版本范围偏窄而实际使用功能不受影响可以在 package.json 里添加overrides字段强制指定某个版本。如果项目是 npm 7可以尝试用--legacy-peer-deps忽略 peer 依赖冲突但这属于临时方案要确认不影响核心功能。这里更推荐的做法是锁定依赖版本package.json 里同时固定vue-ueditor-wrap和配套的ueditor包版本避免后续安装时被自动解析到不兼容的版本。6.2 单页面引入 vue-konva 的隔离方案vue-konva是基于 Konva 的 Vue 封装通常用于 canvas 绘图。如果项目里只有一个页面用了它把它全局注册到 Vue 上会拖慢整体首屏加载增加不必要的打包体积。合理的方案是局部引入// 某个用到的组件中 import VueKonva from vue-konva export default { components: { VueKonva } }这种方式只加载当前组件用到的依赖不影响其他页面。如果多个页面都用再考虑全局注册或按需按页面异步加载路由页面组件。另一个相关问题是 Vue2 项目中用 threejs 和 d3js 开发地图。这种混场场景的关键是注意生命周期管理threejs 的 canvas 实例要在组件销毁时释放资源d3 绑定的 svg 事件也要在beforeDestroy中解绑防止内存泄漏。因为这两个库都不是 Vue 驱动的Vue 只负责容器挂载销毁逻辑要自己补全。6.3 排查第三方依赖冲突的一般套路总结一套通用方法遇到任何 Vue2 依赖问题时都可以套用第一步优先看版本npm ls 包名查看实际安装树确认版本是否和项目主版本匹配。第二步看兼容说明去包的 GitHub 仓库看 peerDependencies 和 issue很多坑在 issue 里已经有人给出解法。第三步看 Vue 版本包是否支持 Vue2 还是只支持 Vue3这两个生态的组件库不能混装。第四步看编译报错如果项目里用的构建工具和包内部语法不兼容可能需要配置 webpack 的transpileDependencies让 Babel 处理第三方代码。现在不少老项目卡在 Webpack 3/4依赖管理更容易出问题。状态我建议改动依赖前先把原 lock 文件提交一份出问题随时还原至少能保证项目不会因为排查过程而雪上加霜。6.4 Vue2 老的 PDF 预览和重型渲染优化心得在 Vue2 项目里做 PDF 预览也是一个常见需求。老项目没法直接上现代 PDF 渲染库比较稳的方案是使用pdfjs-dist——但要选兼容 Vue2 的版本初始化后通过 canvas 渲染每一页。这个方案的缺点是大文件预览滑动卡顿一个常见优化思路是只渲染当前视口范围内的页超出范围的页先占位滑到再渲染。同样的优化思路也适用于 threejs 和 d3js 这类重型渲染。不要一上来就把所有数据铺到 canvas 或 svg 上先做数据降采样或可视范围裁剪性能提升会很明显。做地图可视化的时候图层上的要素超过几千个就该考虑 canvas 而不是继续堆 svg 元素Vue2 本身不会帮你应对这种渲染压力。其实说到底Vue2 的核心思想并不复杂数据驱动视图、指令绑定操作、组件化组织单元。难就难在把这些概念真正落到项目里时会遇到各种版本、依赖、性能问题的组合拳。这篇写到的响应式原理和指令边界是我认为入门阶段最值得花时间的部分——前者让你搞清楚框架为什么这么设计后者让你日常写模板时心里有底。至于版本坑和排查方法建议碰到时再回头对照看完能少走弯路就达到目的了。
返回列表