3.2 指令系统全景 本节摘要:指令是模板里带 v- 前缀的特殊属性,按作用可分为条件渲染、列表渲染、双向绑定、样式与显隐四类。本节逐类拆解它们在编译产物与运行成本上的差异——尤其是 v-if 与 v-show 这对高频面试题的量化对比,以及自定义指令的钩子模型。 你应当能获得的能力 阅读完本节,你应当能够: 按成本模型给四类指令分类,而不是死记清单; 从"编译产物 + 切换频率"两个维度决策 v-if / v-show; 写出带全部钩子的自定义指令,并说明各钩子的触发时机; 解释 v-for 与 v-if 不建议同级的编译原因。 指令的成本模型 指令不是平等的。按"贵不贵"分三档: 贵(动真实 DOM 结构):v-if 系——创建与销毁节点,连带组件的话还要走完整生命周期;
本节摘要:指令是模板里带 v- 前缀的特殊属性,按作用可分为条件渲染、列表渲染、双向绑定、样式与显隐四类。本节逐类拆解它们在编译产物与运行成本上的差异——尤其是 v-if 与 v-show 这对高频面试题的量化对比,以及自定义指令的钩子模型。
阅读完本节,你应当能够:
指令不是平等的。按"贵不贵"分三档:
带着这个分级看具体指令。
<!-- v-if:不满足时产物里根本没有这个节点 --> <div v-if="isVip">尊享内容</div> <div v-else-if="isMember">会员内容</div> <div v-else>普通内容</div> <!-- v-show:节点始终存在,只是被 display:none 藏住 --> <div v-show="visible">悬浮提示</div>
编译产物差异直观:
// v-if 的渲染函数(简化) isVip ? createElement('div', ..., '尊享内容') : createEmptyVNode(); // v-show 的渲染函数(简化) createElement('div', { directives: [{ name: 'show', value: visible }] }, '悬浮提示');
v-if 分支不命中时返回空节点,切换时销毁/重建(组件场景连带卸载挂载);v-show 产物恒在,运行时改样式。
| 维度 | v-if | v-show |
|---|---|---|
| 初始不命中时 | 零渲染成本 | 照常渲染,仅隐藏 |
| 切换成本 | 高(销毁重建) | 低(改样式) |
| 支持template与v-else | 是 | 否 |
| 适用 | 条件很少变、分支重 | 高频切换、轻量内容 |
决策一句话:切换频率低用 v-if,频率高用 v-show;分支是重组件(表单、图表)几乎无脑 v-if——白渲染一个藏起来的重组件最亏。
顺带一条编译级忠告:v-for 与 v-if 别写在同一元素上。Vue 2 里 v-for 优先级更高,v-if 在每次循环里逐个判断;Vue 3 反过来 v-if 优先,拿不到循环变量直接报错。要过滤列表,用计算属性先滤:
computed: { activeItems() { return this.items.filter(i => i.active); } }
<li v-for="(item, index) in items" :key="item.id"> {{ index }} - {{ item.name }} </li>
编译产物是一个循环生成的 vnode 数组,每项带着 key。key 不是给人看的注释,是 diff 阶段识别"同一节点"的身份证——第 4 章专门剖析,这里先立规矩:用稳定业务 id,别用数组索引。索引在插入删除时会错位,导致复用错节点、输入类内容串位的经典 bug。
表单上 v-model 展开是两件事的组合:
<input v-model="text"> <!-- 等价于 --> <input :value="text" @input="text = $event.target.value">
语法糖在受控组件上还有微调:复选框绑定 checked 与 change、单选与下拉同理;组件上 v-model 展开为 prop 与 event 的约定(Vue 2 是 value/input,Vue 3 改为 modelValue/update:modelValue,并支持 v-model:title 这类多参数形式)。第 8 章表单专题会展开,此处记住"双向绑定 = 单向绑定 + 反向事件"这一本质,它并不是什么双向数据流。
对象与数组两种语法覆盖绝大多数场景:
<div :class="{ active: isActive, 'text-danger': hasError }"></div> <div :class="[baseClass, isActive ? 'active' : '']"></div> <div :style="{ fontSize: size + 'px' }"></div>
产物是普通的属性 vnode,成本低,可放心高频更新。
当需求是"对普通底层元素进行底层 DOM 访问"(聚焦、拖拽、埋点曝光),自定义指令比组件更合适。Vue 2 五个钩子,Vue 3 换名并语义对齐组件生命周期:
// Vue 3:一个点击外部区域关闭弹层的指令 const clickOutside = { mounted(el, binding) { el.__handler = (e) => { if (!(el === e.target || el.contains(e.target))) { binding.value(e); } }; document.addEventListener('click', el.__handler); }, unmounted(el) { document.removeEventListener('click', el.__handler); } }; app.directive('click-outside', clickOutside);
<div v-click-outside="close">弹层内容</div>
钩子对照表:
| Vue 2 | Vue 3 | 时机 |
|---|---|---|
| bind | mounted | 元素插入前 / 插入后 |
| inserted | mounted | 已插入父节点 |
| update | updated | 所在组件更新(搬迁前后各一次) |
| componentUpdated | updated | 所在组件及子级更新完 |
| unbind | unmounted | 解绑 |
一个易踩的坑:指令回调里依赖响应式数据时,更新阶段(updated)需要自己比对新旧值决定是否重执行——指令钩子没有自动依赖追踪,它是编译系统留给你的逃生门,不是响应式系统的一部分。
顺带一个可以直接动手的验证:在浏览器开发者工具的性能面板里录一段切换操作,分别对 v-if 与 v-show 版本各录一次,对比脚本耗时与强制重排的条数。你会直观看到 v-if 版本出现节点销毁与创建的调用栈,而 v-show 版本只有样式读写。亲手量过一次,比背十遍对比表印象深刻得多,也顺便熟悉了第 9 章要用的测量工具。
指令最终产出的 vnode,正是下一章的主角——虚拟 DOM 与 diff。