ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vue3声明式渲染深入:从响应式原理到工程实践

Vue3声明式渲染深入:从响应式原理到工程实践 声明式渲染这个概念刚接触Vue3的新手往往觉得玄乎什么“数据驱动视图”听起来像玄学。但真正写过一段时间业务代码、被命令式DOM操作折磨过的人回头再看这句话才会有共鸣声明式渲染把我们从“手动指挥DOM”的泥潭里拉了出来变成“描述状态、让框架去跑腿”。这篇文章不打算抄官方文档我想从工程落地的角度把Vue3声明式渲染这套东西拆开揉碎讲讲它底层的设计逻辑、日常开发里的最佳实践以及我真实踩过的坑。适合正在学Vue3、准备从Vue2迁移、或者写了一阵子但总觉得理解不透彻的同学。1. 声明式渲染理解数据驱动视图的核心范式1.1 命令式与声明式的根本区别先别急着看API我们得把底层思维掰扯清楚。传统jQuery时代写页面流程是这样的用户点了按钮 - 我用$(#xxx).text(新值)去改文本 - 再用$(#xxx).addClass(active)去改样式。每一步指令都对应一次具体的DOM操作这种模式叫命令式编程代码里全是“怎么干”的细节。声明式编程则完全不同。你只需要声明“现在数据是这种状态界面应该长这样”至于具体是新增一个节点、删除一个旧节点、还是只更新某段文本那是框架的事。用Vue3写的话逻辑变成定义响应式数据message模板里写{{ message }}当数据改变时页面自动刷新。这个转变的本质是把“对DOM的操作”抽象成“对状态的描述”。代码里不再关心document.getElementById这类命令而是声明数据与界面之间的映射关系。这种抽象带来了两个直接好处代码可读性高业务逻辑不容易被DOM操作细节淹没状态与界面永远保持一致不用手动同步。后面会讲到Vue3通过响应式系统来追踪数据变化并自动更新视图形成“数据驱动视图”的闭环。1.2 Vue3在声明式渲染上做了什么升级Vue2时代我们也有声明式渲染模板语法和响应式系统都挺好用但Vue3在底层实现上做了两个关键升级。第一个是响应式系统的重写。Vue2用Object.defineProperty对data中的属性做递归劫持这意味着新增或删除属性时响应式系统无法感知所以要靠Vue.set和Vue.delete补丁数组通过索引修改元素无法触发更新。Vue3改用Proxy代理整个对象无论新增属性、删除属性、还是通过下标改数组元素都能被拦截到响应式覆盖面更完整初始化性能也更好因为不再需要递归遍历每个属性。这对开发体验的影响很大写代码时不用再小心翼翼地避开Vue2的响应式陷阱。第二个是模板编译的优化。Vue2在运行时通过虚拟DOM的diff算法找出差异并更新。Vue3模板编译阶段就做了静态分析把动态绑定和静态节点区分开diff时可跳过静态子树也就是编译时优化。这在高频更新的场景下性能提升非常明显。数据驱动视图的基本原则没变但驱动效率更高了。2. 响应式系统声明式渲染的发动机2.1 ref和reactive到底该选谁响应式数据是声明式渲染的动力来源。Vue3里创造响应式数据有两种主要姿势ref和reactive。很多新手困惑什么时候用哪个我的实践经验是能拆就拆优先用ref。ref底层也是通过reactive实现的只是它把值包装成了{ value: ... }的结构让基本类型也能被追踪。用ref的好处非常实际第一在script setup里定义变量后模板里直接引用不需要.value代码更简洁第二解构时不会丢失响应性这在把状态传给子组件或提取公共逻辑时极其重要。reactive适合管理结构相对固定的对象比如一个表单对象、一个配置对象。但reactive有个容易踩的坑解构或展开后响应性会丢失。// reactive解构后丢失响应性 const state reactive({ count: 0 }) const { count } state count // 页面不会更新count已经是一个普通数值了如果确实想解构并保持响应性可以借助toRefs。不过日常开发中我建议你尽量统一风格小团队用ref为主代码读起来最省心。2.2 computed声明式派生的最佳实践computed是声明式渲染里最优雅的API之一。它解决的问题是当界面上的一个值需要根据多个数据源计算得出时你怎么维护它。初级写法是监听数据变化自己去更新另一个变量const firstName ref(张) const lastName ref(三) const fullName ref(张三) watch([firstName, lastName], ([f, l]) { fullName.value f l })但用computed会更符合声明式思维const firstName ref(张) const lastName ref(三) const fullName computed(() firstName.value lastName.value)区别在哪前者描述“什么时候做、做什么”后者描述“它是什么”。computed会自动追踪依赖只有依赖变化时才重新计算并且结果会被缓存。同一个computed在模板多处使用时不会重复执行计算逻辑。我建议把computed当作“视图层的数据加工厂”。比如列表筛选、状态文案映射、带单位的数值展示这些都可以用computed声明。这样模板里可以保持干净业务判断也能集中管理。2.3 watch和watchEffect的适用边界watch和watchEffect不是用来推导视图的它们是声明式渲染体系里的“副作用处理区”。所谓副作用指那些不能被渲染逻辑覆盖的操作接口请求、手动操作DOM、日志上报、本地存储同步等。watch需要明确指定监听的数据源比如watch(source, callback)。它适合场景某个数据变化后需要去请求新数据表单值变化后需要做防抖校验并调用后端接口。watchEffect则更自动它会立即执行一次回调函数函数内部用到了哪些响应式数据它就自动监听哪些数据。适合场景初始化时需要立即执行的逻辑、且依赖多个不确定的数据源。一个很容易犯的错误是把所有数据联动都写在watch里比如A变化时改BB变化时改C最后代码里出现一堆互相监听的watch排查起来非常痛苦。正确做法是能用computed推导的值永远不要用watch去同步能用事件直接修改的数据也不要绕一圈通过watch去触发。watch只处理外部副作用。3. 模板语法数据映射视图的表达力3.1 插值表达式与指令的配合Vue3的模板语法是声明式渲染最直观的体现。{{ message }}、v-bind:titletooltip、v-ifisShow、v-foritem in list这些指令本质上都是在声明“数据与界面的映射关系”。有一点值得强调插值表达式里可以写简单的JavaScript表达式但不要写复杂逻辑。很多人把几层嵌套的三元表达式直接怼进模板比如{{ status 1 ? 待付款 : status 2 ? 已付款 : status 3 ? 已发货 : 已关闭 }}这种写法模板可读性极差。正确做法是提供一个computed属性来做状态映射。模板里的表达式会被Vue编译成渲染函数所以它有完整的JavaScript能力但设计意图是只做轻量展示。任何需要逻辑判断、数据处理的地方都应该把它抽到computed或者方法里。这是保持模板清爽的重要原则。3.2 v-model双向绑定的声明式用法v-model是表单场景下声明式渲染的重要补充。它本质是一个语法糖等价于:modelValuexxx加update:modelValuexxx $event。Vue3里v-model可以绑定多个值可以自定义修饰符比Vue2强大不少。input v-modelusername / !-- 等价于 -- input :valueusername inputusername $event.target.value /自定义组件上使用v-model也很常见比如自己封装的弹窗组件、下拉选择组件MyModal v-model:visibledialogVisible v-model:titlemodalTitle /子组件里通过defineProps接收visible和title通过defineEmits声明update:visible和update:title事件。这种模式下父组件和子组件之间的数据流清晰可追踪比Vue2时代的.sync修饰符优雅得多。3.3 列表渲染的key与性能陷阱v-for配合key是声明式列表渲染中的关键知识点。很多人知道要加key但为什么要加可能说不清楚。Vue的diff算法通过key来识别哪些节点是复用的哪些是新增的。如果key不稳定列表渲染时Vue可能错误地复用节点导致界面更新异常比如输入框内容串位。key的选择有讲究。优先使用数据本身的唯一id比如接口返回的主键万不得已才用index。举个典型场景列表第一条数据被删除如果key是index那么原本第二条数据会被判定为“第一条数据移动了位置”整个列表的DOM节点可能被复用而不是重新创建。如果列表项里有表单输入框等带内部状态的元素就会出现数据错乱的诡异问题。v-for和v-if同用的问题Vue3里也要留意。Vue3中v-if优先级高于v-for这意味着v-if访问不到v-for里的变量。官方推荐做法是需要过滤时用computed先算出过滤后的列表再在模板中v-for渲染避免在模板里做条件判断。3.4 条件渲染v-if vs v-show条件渲染的两个指令适用场景差异很大。v-if是惰性的条件为假时不会渲染DOM节点条件切换时会销毁和重建节点。v-show则是通过display: none控制显隐节点始终存在于DOM中。很多人纠结哪个更优其实判断标准很简单切换频率。高频切换比如Tab切换、下拉菜单展开收起用v-show避免反复销毁重建低频切换比如审核状态切换、权限判断用v-if减少初始渲染开销。另外如果子组件初始化时有较重逻辑必要时需要缓存状态用v-show可以避免组件频繁销毁重建。4. 组件化声明式思维的延伸4.1 props向下、emit向上组件化不是声明式渲染的附加品而是它的重要组成部分。在组件化开发中数据流是单向的父组件通过props把数据传给子组件子组件通过emit通知父组件更新数据。这个约束保证了数据变化来源可追踪不会出现两个组件互相修改状态、难以定位问题的局面。定义props时建议写清楚类型和默认值// 子组件中 const props defineProps({ title: { type: String, default: }, list: { type: Array, default: () [] }, size: { type: String, validator: (v) [small, medium, large].includes(v) } })有些同学对default只写[]或{}这是一个隐患。props的默认值如果是引用类型需要用工厂函数返回新对象否则多个组件实例会共享同一个引用改一个全变。4.2 defineComponent与defineProps的选择热搜词里有一条“vue3 definecomponent创建组件”还有一条“vue3 vite5总是报definecomponent is not defined”。这暴露了不少人对defineComponent的困惑。在Vue3的script setup模式下大多数场景不需要显式调用defineComponent编译器会自动处理组件定义。defineComponent主要用在选项式API、或者在TypeScript中需要推导组件类型时。如果你是script setup风格直接这样写就行script setup import { ref } from vue const count ref(0) /script template button clickcount{{ count }}/button /template至于报“defineComponent is not defined”常见原因是某种写法下确实调用了defineComponent但忘了从vue里导入或者在非script setup的普通script中误用了只属于setup的编译宏。解决方法是先明确你用的组件写法script setup里直接使用defineProps/defineEmits这些编译宏如果确实是普通script那就从vue中显式导入defineComponent。4.3 跨层级状态共享provide/inject与pinia当组件树层级较深需要通过很多层props传递数据时代码会变得冗长。此时可以用provide/inject实现跨层级传递父组件提供数据子孙组件直接注入使用。比如主题配置、用户信息这类全局性数据就很适合provide。但需要注意provide/inject不是完整的响应式状态管理方案。它更像依赖注入适合传递“不太变化的配置”或者配合ref/reactive的实例共享。全局状态管理我还是建议上Pinia。Pinia基于Vue3的响应式系统实现store里的state本身就是响应式的组件中修改state会触发视图更新整体符合声明式渲染数据流的设计。热搜词里提到“vue3 pinia”说明这已经成了Vue3生态的标配。用Pinia管理购物车、用户信息、菜单权限这类全局状态比手动传props舒服得多。而且Pinia的API设计非常简洁没有Vuex那么多概念开箱即用TypeScript支持也好。5. 常见问题与排查技巧实录5.1 响应式丢失的几种场景响应式丢失是数据驱动视图失灵的最常见原因。我整理了一下大概有这几类场景第一种是reactive解构后直接使用。前面提到过reactive对象的属性被解构出来时会变成一个普通值不再具备响应性。需要解构时用toRefs转换。第二种是把响应式对象赋值给普通变量再修改。比如从接口拿到数据后手动拼接了一个新对象赋给reactive对象如果拼接过程破坏了原有引用响应性也可能丢失。第三种是直接给reactive对象新增属性时某些情况下列表不会更新。虽然Proxy能拦截新增属性但如果属性本身就是后续异步赋值、初始时并不存在模板里显示的时机要特别注意必要时先初始化好完整数据结构。排查这类问题的最快方法确认页面数据源是否来自ref或reactive包裹的数据是否有解构、展开、赋值新对象等操作中断了响应链。在浏览器控制台直接修改xxx.value或xxx.属性看视图是否变化基本能快速定位。5.2 defineComponent is not defined的完整排查这个问题我再展开说一次因为我见过太多次了。报错本身很直接代码里使用了defineComponent但它没有从任何地方导入。第一种情况项目用的是script setup但你在script setup的块里调用了defineComponent。script setup中不需要也不能直接使用defineComponent编译器会自动包裹。如果你确实需要普通的script块来配合使用比如设置组件名那就需要显式导入import { defineComponent } from vue export default defineComponent({ name: MyComponent })第二种情况你在script setup里写了defineComponent({ ... })其实不需要直接按组合式API写就行。如果你只是想给组件设置名称Vue3.3以上版本提供了defineOptions宏defineOptions({ name: MyComponent })所以遇到这个报错先检查你处在什么语法上下文有没有从vue导入是不是可以在当前写法下不依赖defineComponent而用更简洁的方式5.3 props与data的边界处理热搜词里有“vue3 props赋值给data”这是开发中很典型的需求父组件传来一个props子组件想在内部维护一个副本比如编辑表单的初始值。直接写const localData ref(props.initialData)在大多数场景下是可以的但要注意如果父组件更新了props.initialDatalocalData不会自动同步。因为ref(props.initialData)只在初始化时读取了一次之后它与props没有关联。如果需要随props变化而更新可以用watch监听props后重新赋值const props defineProps({ initialData: { type: Object } }) const localData ref({ ...props.initialData }) watch(() props.initialData, (val) { localData.value { ...val } })另一个坑是直接修改props。Vue3中props是只读的虽然修改不一定报错但会破坏单向数据流导致状态不易追踪。要修改时应该通过emit通知父组件变更。子组件内部要维护副本时用上面的ref加watch方案不要直接给props属性赋值。5.4 列表懒加载与虚拟滚动的选择热搜词里同时出现了“vue3 列表懒加载”和“vue-virtual-scroller vue3使用”这俩不是一回事但经常被混着提。懒加载解决的是“数据分批请求”问题滚动到页面底部时再请求下一页数据。这是和后端接口配合层面的优化。虚拟滚动解决的是“大量DOM节点渲染卡顿”问题几千上万条数据同时渲染在页面上DOM节点太多导致内存和渲染压力大。虚拟滚动只渲染可视区域内的节点滚动的过程中动态替换。它们可以配合使用商家后台的订单列表先懒加载分页数据积累到一定量后再启用虚拟滚动。但要注意虚拟滚动对每个行项目的高度有要求如果项目高度不固定需要动态测量或估算实现复杂度会上升。如果是普通后台管理表格建议先用懒加载配合分页就够了不要一上来就上虚拟滚动容易得不偿失。5.5 iframe嵌套与外层点击事件“vue3嵌套iframe没办法触发iframe外层div的点击事件”这个问题挺常见的。根本原因是iframe是一个独立窗口对象鼠标进入iframe区域后事件由iframe文档处理不再冒泡到外层DOM所以外层的click监听收不到。解决办法有几种思路一种方案是监听iframe的load事件在iframe加载完成后注入额外的监听逻辑这需要iframe页面和主页面同源且有通信权限。另一种方案是在iframe外层罩一个透明层拦截点击事件但这么做会挡住iframe的交互不适合需要用户操作iframe的场景。如果只是希望能感知“用户是否点击了iframe区域”可以在iframe外层加pointer-events或focus事件监听因为iframe被点击时会触发blur事件主窗口失去焦点配合页面visibilitychange可以大致判断。更靠谱的方案还是同源页面之间通过postMessage通信iframe内部主动通知父页面点击事件。跨域场景下来讲iframe的交互事件确实很难完美捕获需要根据业务场景选方案。6. 性能优化与数据驱动的最佳实践6.1 计算属性的缓存价值前面提过computed会缓存计算结果。这个特性在数据驱动视图的性能优化上有很大价值。比如一个列表页同时有搜索、筛选、排序功能多个界面元素依赖同一个“过滤后的列表”。如果每次渲染都重新执行过滤逻辑性能浪费很大用computed的话只有原始数据或筛选条件变化时才会重新计算。const filteredList computed(() { return allList.value.filter(item { return item.name.includes(searchKeyword.value) item.status statusFilter.value }) })模板中多处使用filteredList时计算只执行一次。这是声明式渲染性能调优的第一个抓手把重复使用的派生数据都交给computed。6.2 shallowRef与markRaw的优化场景数据驱动视图的性能瓶颈有时候不在模板渲染而在响应式转化的成本。shallowRef是ref的浅层版本只有.value本身的变化会触发响应不会递归处理value内部的深层属性。适合场景大对象、静态配置类数据、不需要深层响应式追踪的对象。比如一张地图实例根本不需要它内部的几十个属性都变成响应式用shallowRef包一下就好。markRaw则是标记一个对象永远不转为响应式。如果你的数据只展示一次、后续永不修改可以标记为markRaw避免它在被放入响应式数据时触发递归代理。合理使用shallowRef和markRaw可以在不影响功能的前提下减少响应式系统的负担。但注意过度使用也会增加心智负担只有确认数据确实不需要深层响应时再用。6.3 v-memo与模板级优化v-memo是Vue3.2引入的指令用于跳过不必要的子节点渲染。它接收一个依赖数组当依赖值没有变化时会跳过整个子树的重渲染。div v-memo[selectedId] ChildComponent :dataitem :activeselectedId item.id / /divv-memo适合用在列表项内部内容较重、但又频繁变化的复杂场景。比如表格行内有很多子组件只有选择状态会变用v-memo缓存未变化的部分会带来明显性能提升。不过它是比较底层的优化手段日常业务代码中用得不算多而且用不好也容易出问题。我的建议是先做好computed缓存、路由懒加载、组件异步加载这些常规优化如果确实有性能瓶颈再考虑v-memo。6.4 数据驱动思维下的代码组织最后聊一点软性的。声明式渲染不只是一种技术它更是一种思维范式。写得好的数据驱动代码有几个特征第一数据是唯一事实来源。界面上的任何状态都应该能在数据中找到对应。不要用DOM节点的class去记录状态更不要用全局变量存临时状态。第二派生数据用computed表达副作用用watch处理。界面上能推导出来的东西绝不手工维护。第三组件的边界清晰。每个组件只管理自己的数据通过props和emit与外部通信状态提升到需要共享的层级去管理。我在实际项目中的体会是数据模型设计得好后续写代码非常省力数据模型乱后期维护就是无底洞。所以无论项目大小都值得在动手写页面之前先想清楚数据的形态、归属、流向再让视图跟着数据走。7. 从Vue2迁移到Vue3声明式渲染的变化清单7.1 语法和API层面的对比Vue2和Vue3在声明式渲染的体验上差异挺大。直接把Vue2项目升级到Vue3不是改改版本号那么简单很多写法都需要调整。我整理一个对比表格方便对照对比项Vue2Vue3响应式原理Object.definePropertyProxy新增/删除属性不响应需Vue.set/Vue.delete自动响应数组索引修改不响应自动响应组合式API无Options APIComposition API script setupv-model默认绑定value用.sync修改绑定modelValue支持多个v-model多根节点不支持支持异步组件对象写法defineAsyncComponent过滤器filter支持已移除用methods/computed替代全局APIVue.prototype.$xxxapp.config.globalProperties7.2 迁移过程中容易忽视的坑从Vue2切换到Vue3有几个坑容易踩。第一个是this指向问题。Vue2的选项式API里this指向组件实例所有数据和方法都挂在this上。Vue3的script setup里this基本没用了定义变量和函数直接用const/function声明模板自动感知。Vue3还兼容Options API但新项目我不建议用了写起来没有script setup顺滑。第二个是异步组件的写法变化。Vue2里异步组件是components: { MyComp: () import(./MyComp.vue) }Vue3里要用defineAsyncComponent包装import { defineAsyncComponent } from vue const MyComp defineAsyncComponent(() import(./MyComp.vue))第三个是过滤器被移除。Vue2里可以定义过滤器格式化文本Vue3中不再支持。替代方案是computed或直接写方法。第四个是$children实例链失效。Vue2里可以访问this.$childrenVue3已移除。现在更推荐通过props、emit、provide/inject来建立组件间通信。7.3 uniapp从Vue2转Vue3的思路热搜词里有一条“uniapp vue2转vue3方法”这是很多移动端开发同学关心的话题。uniapp支持Vue3编译模式但迁移时要注意几点第一检查你的自定义组件是否都用上了Composition API风格。script setup在uniapp中也能正常使用但个别小程序平台的兼容性可能有差异大项目建议先在真机上跑一遍核心功能。第二uni-app内置API和生命周期有一些差异。Vue3模式下应用生命周期和页面生命周期都需要按Vue3规范调整比如onShow等页面生命周期框架提供的方式和原生的onLoad/onShow没有变但要注意组件生命周期函数的写法不同。第三兼容性问题。插件市场里一些老插件还是Vue2模式的迁移前要先确认插件是否支持Vue3编译模式。如果要自己改造重点看响应式API的调用方式和生命周期钩子。迁移不是一蹴而就的可以分模块进行。先搭好Vue3版本的项目骨架把基础组件迁移过去再逐步迁移业务页面。保持每一个可独立运行的里程碑这样风险可控。8. 几个高性价比的实战组合8.1 Vite Vue3 TypeScriptVite已经成了Vue3项目的标准构建工具。创建项目的标准姿势是npm create vuelatest选择是否启用TypeScript、路由、Pinia、ESLint等选项。Vite冷启动快、热更新快配合Vue3的编译时优化开发体验很流畅。TypeScript配Vue3的体验比配Vue2好得多。defineProps和defineEmits支持泛型推导const props defineProps{ title: string list: { id: number; name: string }[] }()这种写法下父组件传值时会有完整类型提示子组件里使用props也会自动推导类型大幅减少低级错误。8.2 Vue3 Pinia Vue Router一个完整的Vue3中后台项目基本配置是Vite做构建Vue Router做路由Pinia做状态管理。搜索词“vue3后台管理系统”和“vue3 可视化大屏”都涉及这套组合。Pinia使用上很直观// stores/user.js import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: , username: }), getters: { isLogin: (state) !!state.token }, actions: { login(token, username) { this.token token this.username username } } })组件中使用import { useUserStore } from /stores/user const userStore useUserStore() const handleLogin () { userStore.login(token-value, 张三) }Pinia设计得好的地方在于它有完整的响应式集成且模块化程度高。每个业务模块一个store文件职责清晰对比Vuex减少了像mutation等概念学习曲线也更平缓。8.3 可视化大屏与ECharts集成“vue3 echarts” “vue3 可视化大屏”这类关键词出现频率很高说明图表可视化是Vue3的重要应用场景。和ECharts集成时最常见的问题是容器尺寸变化后图表不会自动resize。推荐的做法是封装一个基础图表组件统一处理初始化、更新和销毁script setup import * as echarts from echarts import { onMounted, onBeforeUnmount, ref, watch } from vue const props defineProps({ option: { type: Object, required: true } }) const chartEl ref(null) let chart null const initChart () { chart echarts.init(chartEl.value) chart.setOption(props.option) } const resizeChart () { chart chart.resize() } onMounted(() { initChart() window.addEventListener(resize, resizeChart) }) onBeforeUnmount(() { window.removeEventListener(resize, resizeChart) chart chart.dispose() }) watch(() props.option, (val) { chart chart.setOption(val) }, { deep: true }) /script template div refchartEl classchart-container/div /template大屏场景还有一个注意点echarts包体积不小按需引入能明显减小打包体积。用echarts/core引入自己用到的图表类型和组件配合实现按需注册。8.4 Vue3接入MQTT等实时数据场景热搜词里有“vue3 mqtt”这在物联网看板、实时监控类项目里很常见。Vue3下接入MQTT流程大致是安装mqtt依赖建立连接订阅主题在回调中更新响应式数据。import mqtt from mqtt const client mqtt.connect(wss://broker.example.com/mqtt, { username: user, password: pass }) client.on(connect, () { client.subscribe(sensor/temperature) }) client.on(message, (topic, message) { const data JSON.parse(message.toString()) temperature.value data.value humidity.value data.humidity })核心思路依然是声明式消息到达后只改数据界面自动刷新。用computed可以对原始数据做二次加工比如超过告警阈值时显示红色。在组件销毁时记得client.end()断开连接防止内存泄漏。9. 把声明式渲染理念贯彻到项目里技术和API说完了再聊聊理念层面的东西。声明式渲染带给我们最大的启发是让状态成为界面的唯一来源。在写业务代码时我习惯先自问几个问题这个页面的核心状态有哪些哪些状态之间是派生关系哪些操作会修改状态当你能把这些问题回答清楚代码自然就有了清晰的骨架。具体执行上有几点建议不要为了用API而用API声明式渲染的核心价值是让代码更好维护如果某段逻辑用命令式写反而更清晰比如某些复杂的DOM动画也不必强行用声明式封装。按数据的重要程度分层全局共享的放Pinia父子传参的走props和emit组件内部临时状态用ref和computed。所有异步数据建议都在数据层做统一的状态管理用loading、error、data三个维度来描述一个异步请求的完整生命周期。我在实际维护一个中型后台系统的过程中体会特别深用数据驱动的方式重写了几个核心页面后新增功能变得轻松了很多因为界面上每个区块都对应明确的数据改数据就是改界面。这个认知一旦建立你再回头看那些全是DOM操作的老代码会感觉完全不一样。Vue3的声明式渲染配合组合式API写起来很顺手。但记住数据驱动视图并不是银弹。它擅长的是“状态与界面的映射”至于复杂的交互、动效、跨组件通信依然需要你用合理的架构去组合各种能力。理解这一点再多的API也难不倒你。
返回列表