01 引入生产环境的 Vue一句话理解开发环境development的 Vue 带着大量警告提示和未压缩代码生产环境production的 Vue 经过了压缩和 Tree-shaking体积小、速度快。为什么Vue 在开发构建里会做很多对你好但很慢的事每次响应式触发时检查数据合法性并在控制台打 warning代码没压缩体积大好几倍包含大量开发期断言assertion部署到线上时如果还用开发版等于背着沙袋跑马拉松。代码示例老式 script 标签引入Vue 2 时代的常见错误!-- 错用了未压缩的开发版 vue.js --scriptsrchttps://unpkg.com/vue3/dist/vue.global.js/script生产环境用.prod.js!-- 对生产版已压缩、无警告 --scriptsrchttps://unpkg.com/vue3/dist/vue.global.prod.js/script现代工程化项目Vite / Vue CLI自动处理但你要确认配置// vite.config.js —— Vite 默认 build 时就是 production 模式// 关键是确保构建命令设置了 NODE_ENVproduction// package.json{scripts:{build:vite build// vite build 自动以生产模式打包}}# 部署前一定要用 build 产物而不是 dev 服务器npmrun build# 产物在 dist/是压缩过的生产代码npmrun dev# 这是开发服务器带热更新和警告不能上线新人提示用脚手架Vite的项目基本不会踩这个坑坑点在于忘了 build直接把 dev 跑起来当线上用。检查方法打开线上网页 → F12 控制台如果看到一堆 Vue 警告说明你用了开发版。02 v-if vs v-show — 该选哪个一句话理解v-if是真删真建条件为假时DOM 根本不存在v-show是永远都在只换隐身衣始终渲染靠display:none切换。为什么维度v-ifv-show初始渲染成本低假时不渲染高永远渲染一次切换成本高每次都要创建/销毁极低只改 CSS适用切换不频繁切换非常频繁支持编译条件是可配合v-else否代码示例频繁切换如 Tab、折叠面板→ v-showtemplate !-- Tab 来回点用 v-show切换丝滑 -- nav button v-fortab in tabs :keytab clickcurrent tab {{ tab }} /button /nav UserProfile v-showcurrent profile / UserOrders v-showcurrent orders / UserSetting v-showcurrent setting / /template不频繁切换如权限、错误页→ v-iftemplate !-- 登录后才渲染且不会频繁切换用 v-if 省初始开销 -- AdminPanel v-ifisAdmin / !-- 报错页面几乎不出现用 v-if -- ErrorPage v-ifhasError / /template新人提示记忆口诀“常切用 show罕切用 if”。v-show不支持template也不能和v-else配合如果需要这些特性只能用v-if。权衡如果这个组件初始化很重比如内含图表即使频繁切换也可能v-if更划算——因为它推迟了首次渲染。v-show页面一加载就造好只是先藏起来。v-if条件为假时组件根本不存在。v-if比v-show性能好的依据是用户可能根本不会点击那个tab。直接不需要初始化它。03 v-for 一定要写唯一 key一句话理解v-for里的key是给 Vue 的身份证号帮它在数据变化时精准识别哪个 DOM 对应哪条数据从而高效复用、避免错乱。为什么Vue 更新列表时做的是diff差异比对它会用key判断这条数据对应的 DOM 是不是原来那个。key 稳定且唯一→ Vue 精准移动现有 DOM性能好、状态正确。key 用 index下标→ 一旦列表顺序变化插入、删除、排序index 和数据的对应关系错乱导致组件状态串台比如输入框内容跑到别处。代码示例反例用 index 当 key隐患巨大当你使用:keyindex时列表的索引0, 1, 2就成了 Vue 识别元素的唯一身份证3。删除“张三”后数据变成了索引 0李四索引 1王五Vue 的 Diff 算法是怎么想的Vue 发现索引 0 还是 0key 没变所以 Vue 认为“张三”还在只是数据变成了“李四”。它不会移动 DOM而是直接在原来的输入框里把文字改成“李四”。索引 1 还是 1key 没变Vue 同样认为 DOM 可以复用直接把文字改成“王五”。索引 2 没了Vue 把最后一个输入框销毁。最终页面的表现原本写着“张三”的第一个输入框现在变成了“李四”注意如果输入框里有用户手敲但还没提交的内容这些内容会原封不动地保留在第一个框里造成“状态串台”或“内容跑偏”的严重 Bug5。原本写着“李四”的第二个输入框变成了“王五”。王五的输入框消失了。新人提示永远不要用index当 key除非你的列表纯展示、永不增删改排序。没有 id用任何稳定且唯一的字段或拼接组合键:keyitem.type item.code。key不要用对象本身对象引用会变必须是基本类型string/number。04 模板里别写复杂表达式一句话理解模板template是展示层应该尽量简单复杂的运算搬到script里用computed模板只负责读结果。为什么模板里的表达式每次组件重新渲染都会重新求值。如果你的表达式很复杂或者用了多次就会重复计算、拖慢渲染。而且模板里塞满逻辑可读性极差。代码示例反例模板里写一堆逻辑template !-- 又长又难读且每次渲染都重新算 -- p v-ifuser.age 18 user.role admin !user.banned {{ user.firstName user.lastName 管理员 }} /p p{{ list.filter(i i.active).length }} 个活跃/p /template正例逻辑移到 computed模板清爽template !-- 模板只关心显示什么不关心怎么算 -- p v-ifcanShowAdmin{{ adminLabel }}/p p{{ activeCount }} 个活跃/p /template script setup import { computed } from vue const props defineProps({ user: Object, list: Array }) const canShowAdmin computed(() props.user.age 18 props.user.role admin !props.user.banned ) const adminLabel computed(() ${props.user.firstName} ${props.user.lastName}管理员 ) const activeCount computed(() props.list.filter(i i.active).length ) /script新人提示经验法则模板里只写取值和简单三元超过一行就该用computed。computed有缓存依赖不变时不会重算见下面响应式部分。第二部分 · 响应式数据这一部分是性能优化的核心战场。Vue 的响应式系统很强大但响应式是有成本的——每一个响应式属性都要被追踪。让真正需要响应的数据响应其余的别参与。05 data 里别放无关数据一句话理解放进响应式系统Vue 2 的data/ Vue 3 的ref、reactive的每一个字段都会被 Vue追踪注册 getter/setter、收集依赖。数据越多初始化越慢、内存占用越高。为什么响应式不是免费的午餐。比如一个有 100 个字段的大对象放进reactive()Vue 要给 100 个字段都建立追踪关系。如果其中 80 个字段根本不会变、也不需要驱动视图那就是白白浪费。代码示例反例什么都往响应式里塞script setup import { reactive } from vue // 配置项、常量、枚举这些根本不变却被做成了响应式 const state reactive({ count: 0, // 需要响应式 API_BASE: https://api.xxx.com, // 常量不该响应式 STATUS_MAP: { 0: 待付款, 1: 已付款, 2: 已发货 }, // 枚举表 pageSizes: [10, 20, 50, 100], // 固定配置 }) /script正例常量放外面只有真正驱动视图的才响应式script setup import { ref } from vue // 常量、配置、枚举 —— 普通变量不进响应式系统 const API_BASE https://api.xxx.com const STATUS_MAP { 0: 待付款, 1: 已付款, 2: 已发货 } const PAGE_SIZES [10, 20, 50, 100] // 只有需要驱动视图的数据才用 ref / reactive const count ref(0) /script新人提示判断标准“这个数据变了需不需要界面跟着变”需要 → 响应式不需要 → 普通变量。script setup顶部声明的普通常量在模板里也能直接用且不会触发响应式开销。06 不需要响应式的数据 → Object.freeze / markRaw一句话理解有些大对象如一份 1 万条的只读列表、一个第三方实例你明确不会修改那就告诉 Vue别追踪它能省掉一大笔初始化开销。为什么和上一条同理但这里针对的是必须以对象形式存在、又完全只读的场景。Vue 2 用Object.freeze()Vue 3 用markRaw()。代码示例Vue 2 风格Object.freeze()// Vue 2被冻结的对象Vue 会跳过对其属性的响应式处理exportdefault{data(){return{// 1 万条数据只读展示冻结后初始化飞快bigList:Object.freeze(fetchHugeList()),}}}Vue 3 风格markRaw()推荐script setup import { markRaw, ref } from vue // markRaw永久标记此对象永不响应式 const bigList markRaw(fetchHugeList()) // 只读展示不追踪 // 第三方非响应式实例如地图、编辑器实例也用 markRaw const mapInstance markRaw(new SomeMapLibrary.Map()) // 注意放进 ref 没问题但 markRaw 的对象内部不会被追踪 const selected ref(null) /script新人提示Object.freeze是 JS 原生方法冻结后对象不能再被修改写值会静默失败 / 报错。Vue 3 里更地道的写法是markRaw()仅禁用响应式不禁止你手动改值。典型场景只读的长列表、枚举字典、第三方类实例。07 对象层级别太深一句话理解响应式对象嵌套层级越深性能越差。能拍扁就别套娃。为什么Vue 的响应式是递归的——reactive({ a: { b: { c: 1 } } })会把a、b、c每一层都包装成响应式。层级越深初始化时遍历越多触发更新时的依赖追踪也越复杂。代码示例反例深层嵌套import{reactive}fromvue// 套了 4 层每一层都要被 Vue 递归包装conststatereactive({user:{profile:{address:{city:北京,// 改它要穿透 4 层}}}})正例扁平化结构import{reactive}fromvue// 扁平化访问和追踪都更高效conststatereactive({userCity:北京,userName:张三,})如果必须嵌套且内层不需要响应式用shallowReactive/shallowRefimport{shallowReactive}fromvue// shallowReactive只有第一层响应式深层不追踪conststateshallowReactive({user:{profile:{city:北京}}// 深层不会变响应式})新人提示设计数据结构时就优先扁平这也呼应后面第 15 条扁平化 Store。如果是表单这类必须嵌套的场景考虑shallowReactive 手动触发更新。08 watch 要精准 — 别监听整个对象一句话理解watch监听的范围越大被触发的次数就越多。只监听你真正关心的那个具体字段而不是整个对象。为什么如果你watch一个大对象那么对象里任何一个字段变化都会触发你的回调——哪怕你只关心其中一个字段。更糟的是deep: true它会深度遍历整个对象开销巨大。代码示例反例监听整个对象 深度监听又慢又会被多余触发import{watch}fromvue// 写法一监听整个 form任何字段变都触发watch(form,(){saveForm()// 哪怕只是改了 form.remark也会触发},{deep:true})// deep: true 是性能杀手开启了深度监听deep: true导致只要表单对象内部任何一个层级的属性发生变化都会触发回调正例一Vue 3用 getter 函数精准监听某个字段import{watch}fromvue// 只监听 form.price其它字段变化不触发watch(()form.value.price,(newPrice,oldPrice){recalculateTotal(newPrice)})正例二Vue 2 时代的对象路径字符串理解原理即可// Vue 2 options API用字符串路径精准监听深层字段exportdefault{watch:{form.price(newVal){// 只在 form.price 变化时触发this.recalculate(newVal)}}}新人提示能用浅监听就别用deep: true它是性能黑洞。Vue 3 推荐用getter 函数() obj.field来精准锁定监听目标。多个字段都要监听写多个watch或用watch([() a, () b], ...)。