Vue2 事件系统深度解析:修饰符、绑定进阶与渲染优化
导读:本文深入剖析 Vue2 事件系统的完整语义------从 v-on 的三种绑定形式到六大事件修饰符的底层原理,从键盘修饰符的 keyCode 机制到 v-bind 的 class/style 对象写法,最后以 v-cloak 解决 FOUC 闪烁问题收尾。所有概念均配有可运行的完整 HTML 示例,同时引用 MDN、Vue 2 官方文档等权威资料。适合有基础 Vue2 使用经验、希望吃透底层机制的前端开发者。
目录
- 零、导读与学习价值
- [0.1 示例覆盖清单](#0.1 示例覆盖清单 "#01-%E7%A4%BA%E4%BE%8B%E8%A6%86%E7%9B%96%E6%B8%85%E5%8D%95")
- [0.2 核心名词速查](#0.2 核心名词速查 "#02-%E6%A0%B8%E5%BF%83%E5%90%8D%E8%AF%8D%E9%80%9F%E6%9F%A5")
- [0.3 为什么要学本篇](#0.3 为什么要学本篇 "#03-%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E5%AD%A6%E6%9C%AC%E7%AF%87")
- [一、v-on 事件绑定的完整语义](#一、v-on 事件绑定的完整语义 "#%E4%B8%80v-on-%E4%BA%8B%E4%BB%B6%E7%BB%91%E5%AE%9A%E7%9A%84%E5%AE%8C%E6%95%B4%E8%AF%AD%E4%B9%89")
- [1.1 三种绑定形式](#1.1 三种绑定形式 "#11-%E4%B8%89%E7%A7%8D%E7%BB%91%E5%AE%9A%E5%BD%A2%E5%BC%8F")
- [1.2 v-bind:onclick 与 v-on:click 的本质区别](#1.2 v-bind:onclick 与 v-on:click 的本质区别 "#12-v-bindonclick-%E4%B8%8E-v-onclick-%E7%9A%84%E6%9C%AC%E8%B4%A8%E5%8C%BA%E5%88%AB")
- [1.3 event 对象的传递规则](#1.3 event 对象的传递规则 "#13-event-%E5%AF%B9%E8%B1%A1%E7%9A%84%E4%BC%A0%E9%80%92%E8%A7%84%E5%88%99")
- [二、v-on 事件修饰符体系](#二、v-on 事件修饰符体系 "#%E4%BA%8Cv-on-%E4%BA%8B%E4%BB%B6%E4%BF%AE%E9%A5%B0%E7%AC%A6%E4%BD%93%E7%B3%BB")
- [2.1 .prevent --- 阻止默认行为](#2.1 .prevent — 阻止默认行为 "#21-prevent--%E9%98%BB%E6%AD%A2%E9%BB%98%E8%AE%A4%E8%A1%8C%E4%B8%BA")
- [2.2 .stop --- 阻止冒泡](#2.2 .stop — 阻止冒泡 "#22-stop--%E9%98%BB%E6%AD%A2%E5%86%92%E6%B3%A1")
- [2.3 .once --- 只触发一次](#2.3 .once — 只触发一次 "#23-once--%E5%8F%AA%E8%A7%A6%E5%8F%91%E4%B8%80%E6%AC%A1")
- [2.4 .capture --- 捕获模式](#2.4 .capture — 捕获模式 "#24-capture--%E6%8D%95%E8%8E%B7%E6%A8%A1%E5%BC%8F")
- [2.5 .self --- 仅自身触发](#2.5 .self — 仅自身触发 "#25-self--%E4%BB%85%E8%87%AA%E8%BA%AB%E8%A7%A6%E5%8F%91")
- [2.6 .passive --- 性能提升](#2.6 .passive — 性能提升 "#26-passive--%E6%80%A7%E8%83%BD%E6%8F%90%E5%8D%87")
- [2.7 修饰符串联](#2.7 修饰符串联 "#27-%E4%BF%AE%E9%A5%B0%E7%AC%A6%E4%B8%B2%E8%81%94")
- 三、键盘与系统修饰符
- [3.1 键盘修饰符机制](#3.1 键盘修饰符机制 "#31-%E9%94%AE%E7%9B%98%E4%BF%AE%E9%A5%B0%E7%AC%A6%E6%9C%BA%E5%88%B6")
- [3.2 系统修饰键](#3.2 系统修饰键 "#32-%E7%B3%BB%E7%BB%9F%E4%BF%AE%E9%A5%B0%E9%94%AE")
- [四、v-bind 的 class 与 style 绑定](#四、v-bind 的 class 与 style 绑定 "#%E5%9B%9Bv-bind-%E7%9A%84-class-%E4%B8%8E-style-%E7%BB%91%E5%AE%9A")
- [4.1 class 绑定三种写法](#4.1 class 绑定三种写法 "#41-class-%E7%BB%91%E5%AE%9A%E4%B8%89%E7%A7%8D%E5%86%99%E6%B3%95")
- [4.2 style 绑定三种写法](#4.2 style 绑定三种写法 "#42-style-%E7%BB%91%E5%AE%9A%E4%B8%89%E7%A7%8D%E5%86%99%E6%B3%95")
- [五、v-cloak 与 FOUC 问题](#五、v-cloak 与 FOUC 问题 "#%E4%BA%94v-cloak-%E4%B8%8E-fouc-%E9%97%AE%E9%A2%98")
- [5.1 FOUC 的根因](#5.1 FOUC 的根因 "#51-fouc-%E7%9A%84%E6%A0%B9%E5%9B%A0")
- [5.2 v-cloak 的解决原理](#5.2 v-cloak 的解决原理 "#52-v-cloak-%E7%9A%84%E8%A7%A3%E5%86%B3%E5%8E%9F%E7%90%86")
- 六、综合案例:动态新闻列表
- 总结
零、导读与学习价值
0.1 示例覆盖清单
| 序号 | 知识点 | 示例形式 |
|---|---|---|
| 1 | v-on 三种绑定形式(语句/函数/调用) | 入门示例 + 实战示例 |
| 2 | v-bind:onclick vs v-on:click | 对比说明 |
| 3 | $event 对象传递 | 入门示例 |
| 4 | .prevent 修饰符 | 入门示例 + 实战示例 |
| 5 | .stop 修饰符 | 入门示例 + 实战示例 |
| 6 | .once 修饰符 | 入门示例 |
| 7 | .capture / .self / .passive 修饰符 | 实战示例 |
| 8 | 修饰符串联 | 综合示例 |
| 9 | 键盘修饰符(keyCode / 别名) | 入门示例 + 实战示例 |
| 10 | 系统修饰键(ctrl/alt/shift/meta) | 实战示例 |
| 11 | v-bind:class 对象写法 | 入门示例 + 实战示例 |
| 12 | v-bind:class 数组写法 | 实战示例 |
| 13 | v-bind:style 对象/数组写法 | 入门示例 |
| 14 | v-cloak 解决 FOUC | 完整示例 |
| 15 | 综合案例:动态新闻列表 | 完整实战 |
0.2 核心名词速查
| 术语 | 一句话解释 |
|---|---|
| v-on | Vue 的事件绑定指令,简写为 @,将 DOM 事件与 Vue 实例方法关联 |
| 事件修饰符 | 附加在 v-on 后的点语法(如 .stop),编译时转换为对应的 DOM API 调用 |
| 事件冒泡 | 事件从最内层元素向外传播的默认行为,可用 .stop 阻止 |
| 默认行为 | 浏览器对特定事件的内置响应(链接跳转、表单提交等),可用 .prevent 阻止 |
| keyCode | 键盘按键的数字标识符(已被 KeyboardEvent.key 取代,但 Vue2 仍支持) |
| v-bind:class | 动态绑定 class,支持字符串/对象/数组三种形式 |
| v-bind:style | 动态绑定 style,支持字符串/对象/数组三种形式 |
| FOUC | Flash of Unstyled Content,页面加载时短暂出现未处理模板({{ }})的闪烁问题 |
| v-cloak | 配合 CSS [v-cloak]{display:none} 在 Vue 接管前隐藏模板,解决 FOUC |
| $event | v-on 中显式传递原生事件对象的特殊变量名 |
| passive | addEventListener 的第三个参数选项,告知浏览器不会调用 preventDefault,允许滚动优化 |
0.3 为什么要学本篇
- 工程必备:事件修饰符是 Vue 项目中防止"冒泡污染"、禁止表单误提交的标准手段,几乎每个业务组件都会用到。
- 面试高频 :
.stopvs.self、.preventvsreturn false、passive的性能意义、v-cloak解决闪烁,均是前端面试中 Vue 章节的典型考点。 - 性能意识 :理解
.capture和.passive的底层机制,有助于在长列表、滚动监听等场景做出正确的性能决策。 - 框架基础:v-bind:class/style 的对象写法是 Vue 组件化开发中动态样式控制的核心模式,Element UI、Vant 等组件库大量使用这一范式。
一、v-on 事件绑定的完整语义
名词解释
- v-on :Vue 的事件绑定指令,格式为
v-on:事件名="处理器",简写为@事件名="处理器"。 - 语句(Statement) :直接在引号中写 JS 表达式,如
num++,适合简单操作。 - 函数引用(Function Reference):传递函数名(不带括号),Vue 会在事件触发时自动将原生事件对象作为第一个参数传入。
- 函数调用(Function Call) :传递带括号的调用表达式,可以自定义参数,通过
$event显式传递原生事件对象。
1.1 三种绑定形式
Vue 的 v-on 指令支持三种处理器写法,背后的编译结果截然不同:

【代码注释】该图自上而下展示 v-on 三种写法在模板编译后的等价形式,关注中间那一列「编译为」节点------这是理解三者差异的关键。黄色「语句」被包裹成 function(e){ num++ },每次触发即时求值;紫色「函数引用」直接把方法本身注册为监听器,Vue 模板编译器会自动把原生事件对象注入为第一个参数;绿色「函数调用」被包裹成 function(e){ changeNum(3, e) },所以必须用 $event 才能把事件对象接力传进去。底部灰色节点给出选型判据。市面应用 :Element UI 的 @click="handleSubmit" 广泛使用函数引用;@click="goto('/home', $event)" 则是函数调用的典型场景。
需要补充一点编译层的细节:Vue 模板编译器对处理器是「语句还是函数引用」的判断,依据是一个简单路径正则 (源码 src/compiler/codegen/events.js 中的 simplePathRE)。如果处理器字符串匹配形如 a.b.c、a['b']、fn 这样的「简单路径」,就当作函数引用直接注册(on:{click: fn});否则当作内联语句,包裹成 function($event){ ... } 再注册。这解释了为什么 @click="fn" 触发时 e 自动注入,而 @click="fn()"(带括号,不是简单路径)里的 fn 拿不到事件对象------它走的是「内联语句」分支,包裹函数里并未把 $event 传给 fn。详见 Vue2 官方文档 --- 事件处理。
下面是一个完整的对比示例:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>v-on 三种绑定形式</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<style>
button { margin: 8px; padding: 6px 14px; cursor: pointer; }
.log { background: #f5f5f5; padding: 10px; border-radius: 4px; min-height: 60px; }
</style>
</head>
<body>
<div id="app">
<h3>计数器:{{ count }}</h3>
<!-- ① 语句:适合简单操作 -->
<button @click="count++">① 语句:count++</button>
<!-- ② 函数引用:自动获得原生事件对象 -->
<button @click="increment">② 函数引用:+2</button>
<!-- ③ 函数调用:可以传自定义参数,$event 传递事件对象 -->
<button @click="addN(5, $event)">③ 函数调用:+5(显示按钮文字)</button>
<div class="log">{{ log }}</div>
</div>
<script>
new Vue({
el: '#app',
data: {
count: 0,
log: '等待点击...'
},
methods: {
// 函数引用形式:第一个参数自动是 MouseEvent
increment(e) {
this.count += 2;
this.log = '函数引用 ------ 按钮文字:' + e.target.innerText;
},
// 函数调用形式:自定义参数 n,$event 映射到 e
addN(n, e) {
this.count += n;
this.log = `函数调用 ------ 参数 n=${n},按钮:${e.target.innerText}`;
}
}
});
</script>
</body>
</html>
【代码注释】increment(e) 使用函数引用形式:Vue 模板编译器在内部生成 on: { click: increment },事件触发时原生 MouseEvent 自动作为第一个参数传入。addN(5, $event) 使用函数调用形式:$event 是 Vue 模板中的特殊变量,代表原生事件对象;若不写 $event 则 e 无法获取。市面应用 :两者混用在 Ant Design Vue 的表格行点击场景中很常见------@row-click="onRowClick($event, row)" 就是函数调用形式,将行数据和事件对象同时传入。
1.2 v-bind:onclick 与 v-on:click 的本质区别
这是一个极容易混淆的陷阱。v-bind:onclick 和 v-on:click 看起来相似,本质完全不同:
| 对比维度 | onclick="fn()" / v-bind:onclick="'fn()'" |
v-on:click="fn" / @click="fn" |
|---|---|---|
| 绑定机制 | HTML 属性,最终变成内联 onclick 字符串 |
Vue 事件系统,使用 addEventListener |
| 方法来源 | 全局函数或字符串求值 | Vue 实例的 methods |
| this 指向 | 全局 window(严格模式报错) |
Vue 实例 |
| 修饰符支持 | 不支持 | 完整支持 |
| 推荐使用 | 不推荐 | 推荐 |
1.3 $event 对象的传递规则
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>$event 传递规则</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
<div id="app">
<!-- 函数引用:e 自动就是事件对象 -->
<input type="text" @input="onInput1" placeholder="函数引用">
<!-- 函数调用:必须显式写 $event -->
<input type="text" @input="onInput2('前缀-', $event)" placeholder="函数调用带参数">
<p>输入内容:{{ result }}</p>
</div>
<script>
new Vue({
el: '#app',
data: { result: '' },
methods: {
onInput1(e) {
// e 是原生 InputEvent
this.result = '[引用] ' + e.target.value;
},
onInput2(prefix, e) {
// prefix 是自定义参数,e 是 $event(原生 InputEvent)
this.result = prefix + e.target.value;
}
}
});
</script>
</body>
</html>
【代码注释】onInput1(e) 中的 e 是浏览器原生 InputEvent,由 Vue 在事件触发时自动注入。onInput2('前缀-', $event) 中若省略 $event,方法签名里的 e 将是 undefined,这是初学者最常踩的坑。市面应用 :文件上传场景 @change="upload($event, index)" 需要同时知道文件列表($event.target.files)和当前上传位置(index),必须用函数调用形式。
【实战要点】
- 经典应用场景 :
$event最常见于需要同时传递业务参数和原生事件对象的场景,如@click="handleDelete(item.id, $event)"可以在阻止冒泡的同时拿到 item id。 - 常见坑 :在函数调用形式中忘记写
$event,导致 handler 里的事件对象参数为undefined,e.target报错。排查方法:检查模板里$event是否遗漏。 - 最佳实践 :能用函数引用就不用函数调用(代码更简洁);当需要传递额外参数时才切换为函数调用并补充
$event。
【本章小结】
| 形式 | 写法 | 事件对象获取 | 适用场景 |
|---|---|---|---|
| 语句 | @click="count++" |
无需 | 一行逻辑 |
| 函数引用 | @click="fn" |
自动注入第一个参数 | 无额外参数 |
| 函数调用 | @click="fn(x, $event)" |
手动写 $event |
需要额外参数 |
记忆口诀:「带括号用 $event,不带括号自动传」
【面试考点】
Q1:v-on:click="fn" 和 v-on:click="fn()" 有什么区别?
A:前者是函数引用,Vue 将 fn 直接注册为事件监听器,触发时原生事件对象自动作为第一个参数传入;后者是函数调用(语句),Vue 在内部生成 () => fn() 这样的包裹函数,fn 执行时没有参数,若想获取事件对象需要显式写 fn($event)。追问「何时用哪种」:不需要额外参数时用引用,需要传业务参数时用调用。
Q2:为什么不推荐用 v-bind:onclick 替代 v-on:click?
A:v-bind:onclick 本质是设置 HTML 属性,最终生成内联事件字符串,其中的函数需要是全局作用域下的函数,this 指向 window 而非 Vue 实例,也不支持任何事件修饰符。v-on:click 走 Vue 的事件系统,底层调用 addEventListener,函数通过 Vue 的方法系统绑定,this 正确指向实例,且支持 .stop、.prevent 等全套修饰符。
二、v-on 事件修饰符体系
名词解释
- 事件修饰符 :附加在
v-on指令后的点语法(.stop、.prevent等),Vue 模板编译器会将其转换为对应的 DOM API 调用,开发者无需在方法内手写。 - 事件冒泡(Bubbling):事件从触发元素向上传播至祖先元素的默认行为,由 DOM 规范定义。
- 事件捕获(Capturing):事件从根节点向下传播至目标元素的阶段,优先于冒泡。
- 默认行为(Default Action) :浏览器对特定事件的内置响应,如
<a>的跳转、<form>的提交。 - addEventListener options :
addEventListener(type, handler, options)的第三个参数,可设置{ capture, once, passive }三个选项。
概念与底层原理
Vue 的事件修饰符本质上是模板编译阶段的语法糖。以 @click.stop.prevent="fn" 为例,Vue 的模板编译器(vue-template-compiler)会将其转换为类似这样的渲染函数代码:
js
// 模板:@click.stop.prevent="fn"
// 编译结果(伪代码):
on: {
click: function($event) {
$event.stopPropagation(); // .stop
$event.preventDefault(); // .prevent
return fn.call(this, $event);
}
}
【代码注释】vue-template-compiler 将 .stop 和 .prevent 编译为在包裹函数内依次调用 e.stopPropagation() 和 e.preventDefault(),顺序与修饰符书写顺序一致。这意味着修饰符是编译时注入 的,不是运行时通过 hook 处理,因此不会有额外的运行时开销。市面应用:查看 Vue 单文件组件编译后的 render 函数(浏览器 DevTools Sources 中),可以直接观察到这些注入的调用。
对于 .once、.capture、.passive,Vue 将它们映射到 addEventListener 的 options 参数:
js
// @click.once.capture.passive="fn" 对应:
element.addEventListener('click', fn, {
once: true, // .once
capture: true, // .capture
passive: true // .passive
});
【代码注释】.once、.capture、.passive 这三个修饰符不是在 handler 内注入代码,而是改变 addEventListener 第三个参数(options 对象),属于注册阶段 的配置。options 对象语法是 IE 11 之后才广泛支持的现代 API,Vue 2 内部对旧浏览器做了兼容降级处理(降级为布尔型 useCapture 参数)。市面应用 :移动端 H5 页面中,@touchmove.passive 是提升滑动流畅性的标准写法,Vue CLI 生成的项目脚手架模板中已内置此优化。

【代码注释】该图把六个事件修饰符按生效阶段 分成两类,这是比「逐个背 API」更本质的记忆方式。黄色分支是「运行时注入」------.prevent、.stop、.self 由编译器把对应判断/调用插入到 handler 包裹函数内部,每次触发都执行;绿色分支是「注册时配置」------.once、.capture、.passive 不改 handler 体,而是改变 addEventListener 第三个参数 options,属于绑定监听器时的一次性配置。重点看紫色节点「codegen 前缀标记」:Vue 编译器并不直接生成 options 对象,而是给事件名加单字符前缀(!=capture、~=once、&=passive),运行时模块 src/platforms/web/runtime/modules/events.js 的 updateDOMListeners 再解析前缀还原出 options 配置(参考 event 揭秘)。市面应用 :这套编译转换机制让开发者完全专注于业务逻辑,不需要在每个 handler 里写 e.stopPropagation()。
2.1 .prevent --- 阻止默认行为
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>.prevent 修饰符示例</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<style>
.ctx-menu { position: fixed; background: #fff; border: 1px solid #ccc;
padding: 8px; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,.15); }
.box { width: 200px; height: 120px; background: #dbeafe;
border: 2px solid #3b82f6; display: flex; align-items: center;
justify-content: center; user-select: none; cursor: pointer; border-radius: 6px; }
</style>
</head>
<body>
<div id="app">
<!-- 阻止链接跳转 -->
<a href="https://www.baidu.com" @click.prevent="onLinkClick">
点我(已阻止跳转)
</a>
<hr>
<!-- 阻止表单提交跳转 -->
<form action="https://www.baidu.com/s" @submit.prevent="onSubmit">
<input v-model="keyword" type="text" placeholder="搜索关键词">
<button type="submit">搜索(阻止跳转)</button>
</form>
<hr>
<!-- 自定义右键菜单 -->
<div class="box" @contextmenu.prevent="onContextMenu">
右键点击此区域
</div>
<div v-if="menu.show" class="ctx-menu"
:style="{top: menu.y + 'px', left: menu.x + 'px'}">
<div @click="menu.show=false">菜单项 A</div>
<div @click="menu.show=false">菜单项 B</div>
</div>
<p>{{ message }}</p>
</div>
<script>
new Vue({
el: '#app',
data: {
keyword: '',
message: '',
menu: { show: false, x: 0, y: 0 }
},
methods: {
onLinkClick() {
this.message = '链接点击了,但没有跳转!';
},
onSubmit() {
this.message = `搜索关键词:${this.keyword}(表单提交已阻止)`;
},
onContextMenu(e) {
this.menu = { show: true, x: e.clientX, y: e.clientY };
}
}
});
</script>
</body>
</html>
【代码注释】三个典型的 .prevent 场景:① <a> 的 @click.prevent 阻止页面跳转,实现 SPA 内的自定义导航逻辑;② <form> 的 @submit.prevent 阻止表单跳转,改用 Ajax 提交;③ @contextmenu.prevent 阻止浏览器的默认右键菜单,替换为自定义浮层。.prevent 等价于在 handler 内调用 e.preventDefault(),但将行为声明化,方法体保持纯净。市面应用 :所有 SPA 项目的登录表单均使用 @submit.prevent 配合 axios 提交,避免页面跳转。
2.2 .stop --- 阻止冒泡
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>.stop 修饰符示例</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<style>
.outer { width: 320px; height: 200px; background: #fef3c7;
border: 2px solid #eab308; display: flex; align-items: center;
justify-content: center; border-radius: 8px; }
.inner { width: 180px; height: 100px; background: #fee2e2;
border: 2px solid #ef4444; display: flex; align-items: center;
justify-content: center; border-radius: 4px; }
button { padding: 6px 16px; cursor: pointer; }
.log { margin-top: 12px; background: #f1f5f9; padding: 8px; border-radius: 4px; }
</style>
</head>
<body>
<div id="app">
<div class="outer" @click="log('外层 div 被点击')">
外层(点击空白区域触发)
<div class="inner" @click.stop="log('内层 div 被点击,不冒泡')">
内层(.stop 阻止冒泡)
<button @click.stop="log('按钮被点击,不冒泡')">按钮</button>
</div>
</div>
<div class="log">
<div v-for="(msg, i) in logs" :key="i">{{ msg }}</div>
</div>
<button @click="logs=[]" style="margin-top:8px">清空日志</button>
</div>
<script>
new Vue({
el: '#app',
data: { logs: [] },
methods: {
log(msg) {
this.logs.unshift(new Date().toLocaleTimeString() + ' --- ' + msg);
}
}
});
</script>
</body>
</html>
【代码注释】嵌套 div 结构演示冒泡:不加 .stop 时点击按钮会依次触发「按钮 → 内层 div → 外层 div」三个 handler;加上 .stop 后冒泡在当前元素止步。市面应用 :商品卡片整体可点击进入详情,但卡片上的「加入购物车」按钮不应触发详情跳转,常用 @click.stop 阻止冒泡。
【实战要点】
- 经典应用场景 :弹窗组件(Modal)中点击遮罩层关闭弹窗,但点击弹窗内容区不应关闭------在内容区加
@click.stop是最简洁的实现。 - 常见坑 :
.stop会阻断事件继续向上传播,若父组件也监听了同一个事件(如全局点击关闭下拉菜单),可能导致该逻辑失效。此时应改用.self或在document级别做精确判断。 - 最佳实践 :只在确实需要隔断冒泡的最近元素上使用
.stop,避免在多层嵌套中都加.stop造成事件流污染,难以调试。
2.3 .once --- 只触发一次
.once 对应 addEventListener 的 { once: true } 选项,事件触发一次后监听器自动移除。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>.once 修饰符示例</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
<div id="app">
<!-- 防重复提交:只允许提交一次 -->
<button @click.once="onSubmit" :style="{background: submitted ? '#ccc' : '#3b82f6', color:'#fff', padding:'8px 20px'}">
{{ submitted ? '已提交(不可重复点击)' : '提交订单' }}
</button>
<p>{{ message }}</p>
</div>
<script>
new Vue({
el: '#app',
data: { submitted: false, message: '' },
methods: {
onSubmit() {
this.submitted = true;
this.message = '订单已提交,监听器已移除,无论点多少次都不会再触发。';
}
}
});
</script>
</body>
</html>
【代码注释】.once 让事件监听器在首次触发后自动销毁,等价于在 handler 里手动 element.removeEventListener,但更简洁。市面应用 :防止用户重复点击「支付」或「提交」按钮是 .once 的经典场景;Vue Router 的 <router-link> 内部也有类似的"只点一次导航"逻辑。
2.4 .capture --- 捕获模式
默认情况下,Vue 事件监听在冒泡阶段 处理。.capture 让监听器在捕获阶段就触发,比冒泡的同类 handler 优先执行:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>.capture 修饰符</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<style>
.box { padding: 20px; margin: 10px; border: 2px solid #a855f7;
background: #f3e8ff; border-radius: 4px; }
</style>
</head>
<body>
<div id="app">
<!-- 父元素用捕获,子元素用冒泡 -->
<div class="box" @click.capture="log('父层 capture 触发')">
父层(capture 捕获阶段)
<div class="box" @click="log('子层 bubble 触发')">
子层(默认冒泡阶段)------点击此处
</div>
</div>
<div v-for="msg in logs" :key="msg" style="color:#333">{{ msg }}</div>
<button @click="logs=[]">清空</button>
</div>
<script>
new Vue({
el: '#app',
data: { logs: [] },
methods: {
log(msg) { this.logs.push(msg); }
}
});
</script>
</body>
</html>
【代码注释】点击子层时,日志顺序为「父层 capture 触发 → 子层 bubble 触发」,捕获阶段先于冒泡阶段。这是 DOM 事件流的规范行为:MDN --- 事件流。市面应用 :全局快捷键拦截、埋点统计需要在最外层捕获阶段收集所有点击事件时会用到 .capture。
2.5 .self --- 仅自身触发
.self 让 handler 只在 event.target === event.currentTarget(即事件来源就是当前元素本身,而非子元素冒泡上来)时才执行:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>.self 修饰符</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<style>
.modal-mask { position: fixed; inset: 0; background: rgba(0,0,0,.5);
display: flex; align-items: center; justify-content: center; }
.modal-body { background: #fff; padding: 30px; border-radius: 8px;
min-width: 300px; }
</style>
</head>
<body>
<div id="app">
<button @click="show=true" style="padding:8px 20px">打开弹窗</button>
<!-- 点击遮罩(.self 确认是遮罩本身)时关闭;点击内容区不关闭 -->
<div v-if="show" class="modal-mask" @click.self="show=false">
<div class="modal-body">
<h3>弹窗内容</h3>
<p>点击弹窗外部遮罩关闭,点击这里不会关闭。</p>
<button @click="show=false">手动关闭</button>
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data: { show: false }
});
</script>
</body>
</html>
【代码注释】.self 的判断条件是 event.target === event.currentTarget:遮罩层自身点击时两者相等,handler 执行;点击内部 .modal-body 时 target 是子元素,不等于遮罩,handler 不执行。这比 .stop 更优雅------不需要在子元素上都加 .stop。市面应用 :Element UI <el-dialog> 的 click-modal-to-close 功能内部使用了相同逻辑。
2.6 .passive --- 性能提升
.passive 对应 addEventListener 的 { passive: true } 选项。它告知浏览器「该事件监听器不会调用 preventDefault()」,浏览器无需等待 JS 执行完毕即可立刻进行滚动,显著提升滚动流畅性:

【代码注释】该图上下对比两条时序链:上方红色「无 passive(默认)」中,浏览器收到 touchmove 后必须先等 JS 执行完 、确认没有调用 preventDefault,才敢推进滚动------这一步「查询」就是卡顿的根因,因为浏览器无法预知监听器内部是否会阻止默认动作;下方绿色「有 passive: true」中,开发者用修饰符向浏览器承诺绝不调用 preventDefault ,浏览器于是立即开始滚动、同时异步通知 JS,滚动与 JS 并行,零延迟。中间灰色虚线箭头点明二者切换的本质:一句「承诺」省掉了浏览器的查询等待。底层上 Vue 还做了 supportsPassive 特性检测------用一个带 get 拦截的 options 对象试探浏览器是否支持对象形式的第三参,不支持则降级为布尔 useCapture(参考 MDN --- addEventListener)。市面应用 :Vue 框架内部在处理 touchstart/touchmove 时默认使用 passive: true;移动端无限滚动列表、图片懒加载监听器均应加 @scroll.passive。注意 .passive 与 .prevent 互斥------同时使用时 .prevent 会被忽略并触发控制台警告。
2.7 修饰符串联
多个修饰符可以链式写在一起,Vue 按从左到右的顺序执行:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>修饰符串联</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
<div id="app">
<div @click="log('父层被点击')" style="padding:30px;background:#fef3c7;border:2px solid #eab308">
父层
<!-- 同时:阻止默认、阻止冒泡、只触发一次 -->
<button @click.prevent.stop.once="log('按钮:prevent+stop+once')"
style="padding:8px 18px;margin:10px">
.prevent.stop.once
</button>
</div>
<div v-for="msg in logs" :key="msg">{{ msg }}</div>
<button @click="logs=[]">清空</button>
</div>
<script>
new Vue({
el: '#app',
data: { logs: [] },
methods: {
log(msg) { this.logs.push(msg); }
}
});
</script>
</body>
</html>
【代码注释】.prevent.stop.once 的执行顺序:① 阻止默认行为;② 阻止冒泡(父层不会触发);③ 注册为 once(只触发一次)。顺序不同时行为可能有细微差异(如 .once.prevent vs .prevent.once 的语义相同,但 .stop.prevent vs .prevent.stop 顺序影响 handler 内部的处理流程)。市面应用 :表单搜索按钮常用 @click.prevent.stop 同时阻止表单提交和冒泡至父容器的点击事件。
【本章小结】
| 修饰符 | DOM API 等价 | addEventListener 选项 | 典型场景 |
|---|---|---|---|
.prevent |
e.preventDefault() |
--- | 阻止链接跳转、表单提交 |
.stop |
e.stopPropagation() |
--- | 弹窗内容区防止关闭 |
.once |
--- | { once: true } |
防重复提交 |
.capture |
--- | { capture: true } |
捕获阶段监听 |
.self |
e.target === e.currentTarget 判断 |
--- | 遮罩点击关闭弹窗 |
.passive |
--- | { passive: true } |
移动端滚动性能优化 |
记忆口诀:「阻止默认 prevent,阻止冒泡 stop,只发一次 once,捕获 capture,仅自身 self,提升性能 passive」
【面试考点】
Q1:.stop 和 .self 的区别是什么?
A:.stop 在当前元素的 handler 执行时立刻调用 e.stopPropagation(),阻止事件继续向上冒泡;.self 不阻止冒泡,而是在 handler 内做 e.target === e.currentTarget 的判断------只有事件来源就是当前元素本身(非子元素冒泡上来)才执行逻辑。实现「点击遮罩关闭弹窗」时,.self 比「在内容区到处加 .stop」更优雅,也不会影响内容区自身的事件冒泡链。
Q2:为什么 .passive 能提升滚动性能?
A:浏览器在收到 touchmove/wheel 事件时,默认需要等待所有监听器执行完毕,检查是否有 preventDefault() 调用,才能决定是否滚动------这会造成明显的输入延迟。passive: true 向浏览器承诺不会调用 preventDefault(),浏览器可以立刻推进滚动,无需等待 JS 线程。Chrome 58+ 已对 document/body 的 touchstart/touchmove 默认启用 passive,Vue 的 .passive 修饰符让开发者可以在任意元素上显式声明这一优化。
三、键盘与系统修饰符
名词解释
- keyCode :键盘事件中按键的数字编码(如 Enter=13,Esc=27),来自历史规范,已在 DOM Level 3 中被废弃,建议使用
KeyboardEvent.key。 - 键盘别名 :Vue 为常用按键提供的语义化名称,如
.enter、.esc、.space、.tab、.delete、.up、.down、.left、.right。 - 系统修饰键 :
.ctrl、.alt、.shift、.meta(Windows 键/Command 键),必须与其它键组合使用,如.ctrl.c。 - keyup / keydown / keypress :三种键盘事件类型,
keydown最先触发,keypress仅针对字符键(已废弃),keyup在按键松开时触发。 - .exact 修饰符 :Vue 2.5.0 引入的「精确匹配」修饰符,要求有且仅有 指定的系统修饰键被按下时才触发,用于区分
@click.ctrl(按下 Ctrl 即可,哪怕同时按了 Alt)与@click.ctrl.exact(只能按 Ctrl,多按任何系统键都不触发)。
概念与底层原理
键盘与系统修饰符在编译期同样不是「运行时 hook」,而是 Vue 模板编译器把过滤判断直接注入进 handler 包裹函数体。区分两类机制是理解本章的关键:
第一类------按键过滤(.enter / .esc / .13 等) :编译器把它们转为对运行时辅助函数 _k(即 checkKeyCodes,源码 src/core/instance/render-helpers/check-keycodes.js)的调用。伪代码如下:
js
// 模板:@keyup.enter="submit"
// 编译结果(伪代码):
on: {
keyup: function($event) {
// _k = checkKeyCodes,不匹配则提前 return null,handler 不执行
if (!$event.type.indexOf('key') &&
_k($event.keyCode, 'enter', 13, $event.key, 'Enter')) return null
return submit($event)
}
}
【代码注释】_k 接收五个参数:实际 keyCode、别名字符串('enter')、内置默认 keyCode(13)、实际 KeyboardEvent.key($event.key)、内置默认 key 名('Enter')。它优先用 $event.key 与内置 key 名比对 (因为 W3C 已废弃 keyCode),只有在 key 不可用时才回退到数字 keyCode 比对------这是 Vue 为兼容 keyCode 废弃做的双轨设计。Vue.config.keyCodes.f1 = 112 注册的自定义别名会被合并进 _k 查询的映射表。判断不通过时 return null,handler 被「短路」跳过。
第二类------系统修饰键过滤(.ctrl / .alt / .shift / .meta) :编译器直接注入对事件对象上 ctrlKey、altKey、shiftKey、metaKey 四个布尔属性的检查:
js
// 模板:@keydown.ctrl.s="save"
// 编译结果(伪代码):
on: {
keydown: function($event) {
if (!$event.ctrlKey) return null // .ctrl 要求 ctrlKey 为真
if (_k($event.keyCode, 's', 83, $event.key, 's')) return null // .s 按键过滤
return save($event)
}
}
【代码注释】系统修饰键依赖 KeyboardEvent 自带的 ctrlKey/altKey/shiftKey/metaKey 属性,这些属性在按键按下期间持续为 true,因此 .ctrl.s 表示「Ctrl 处于按下状态时按下 S」。加 .exact 后,编译器会额外注入对其余三个修饰键属性「必须为 false」的检查,从而实现「有且仅有 Ctrl」的精确匹配。系统修饰键自 Vue 2.1.0 引入、.exact 自 2.5.0 引入。
3.1 键盘修饰符机制
Vue 键盘修饰符支持两种写法:
- 数字 keyCode :
@keyup.13="fn"→ 等同于在 handler 中if(e.keyCode === 13) fn() - 语义别名 :
@keyup.enter="fn"→ 更可读,推荐使用

【代码注释】该图从左到右展示键盘修饰符的运行时判断链路,核心是中间黄色的 checkKeyCodes 节点------它在编译产物里被简写为 _k,是所有键盘修饰符共用的过滤函数。三条分支分别对应红色「数字 keyCode」(直接 e.keyCode === 13)、绿色「语义别名」(查内置 keyCodes 映射表,如 enter → [13]、tab → [9],并优先用 KeyboardEvent.key 比对 以兼容 keyCode 废弃)、紫色「自定义别名」(Vue.config.keyCodes.f1 = 112 会被合并进映射表)。三条分支汇入橙色终点:条件满足才执行 handler,否则 return null 提前退出。checkKeyCodes 的源码在 src/core/instance/render-helpers/check-keycodes.js。市面应用 :搜索框的 @keyup.enter="doSearch" 是最普遍的使用场景;后台系统用 Vue.config.keyCodes 注册业务快捷键别名(如 .f5 刷新)。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>键盘修饰符完整示例</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<style>
input { display: block; margin: 8px 0; padding: 6px 10px; border: 1px solid #ccc;
border-radius: 4px; width: 240px; }
label { font-size: 13px; color: #666; }
.log { background: #f1f5f9; padding: 8px; margin-top: 12px;
border-radius: 4px; min-height: 40px; font-family: monospace; }
</style>
</head>
<body>
<div id="app">
<label>按 Enter 触发:</label>
<input type="text" @keyup.enter="log('Enter 键触发', $event)" placeholder="按 Enter">
<label>按 Esc 触发:</label>
<input type="text" @keyup.esc="log('Esc 键触发', $event)" placeholder="按 Esc">
<label>按 Space 触发:</label>
<input type="text" @keyup.space="log('Space 键触发', $event)" placeholder="按空格">
<label>按方向键 ↑ 触发:</label>
<input type="text" @keyup.up="log('Up 键触发', $event)" placeholder="按上方向键">
<div class="log">
<div v-for="(msg,i) in logs" :key="i">{{ msg }}</div>
</div>
<button @click="logs=[]">清空日志</button>
</div>
<script>
new Vue({
el: '#app',
data: { logs: [] },
methods: {
log(name, e) {
this.logs.unshift(`${name} --- keyCode: ${e.keyCode}, key: "${e.key}"`);
}
}
});
</script>
</body>
</html>
【代码注释】该示例同时打印 keyCode(数字)和 key(字符串),可以直观对比两种标识方式。.esc 对应 keyCode=27,.space 对应 keyCode=32,.up 对应 keyCode=38。市面应用 :富文本编辑器中 .enter 用于换行、.tab 用于缩进、.esc 用于退出全屏,是组合键处理的典型场景。
3.2 系统修饰键
系统修饰键(.ctrl、.alt、.shift、.meta)只有在对应的修饰键按下时才触发:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>系统修饰键示例</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
<div id="app" @keydown.ctrl.s.prevent="save" tabindex="0"
style="outline:2px solid #3b82f6;padding:20px;border-radius:6px;min-height:120px">
<p>点击此区域激活,然后按 <kbd>Ctrl+S</kbd> 触发保存(阻止浏览器另存为)</p>
<p>按 <kbd>Ctrl+Z</kbd> 撤销</p>
<p>{{ message }}</p>
<!-- keydown 阶段:ctrl+z 撤销 -->
<div @keydown.ctrl.z="undo" style="display:none"></div>
</div>
<script>
new Vue({
el: '#app',
data: { message: '等待操作...' },
methods: {
save() {
this.message = `[${new Date().toLocaleTimeString()}] 保存成功!`;
},
undo() {
this.message = `[${new Date().toLocaleTimeString()}] 撤销操作!`;
}
}
});
</script>
</body>
</html>
【代码注释】@keydown.ctrl.s.prevent 组合了三个修饰符:.ctrl 要求 Ctrl 键按下、.s 要求同时按 S 键、.prevent 阻止浏览器的「另存为」默认行为。tabindex="0" 让 div 可以获得焦点从而接收键盘事件。市面应用:在线文档编辑器(如腾讯文档)中 Ctrl+S 保存、Ctrl+Z 撤销都是通过类似机制实现的。
【实战要点】
- 经典应用场景 :搜索框
@keyup.enter触发搜索是最常见的键盘修饰符使用场景;表单中@keydown.tab自定义 Tab 键焦点跳转顺序也很常见。 - 常见坑 :
keyCode在不同操作系统/键盘布局下可能存在差异,且 W3C 已废弃keyCode属性,Vue 3 也移除了 keyCode 数字修饰符。应优先使用语义别名(.enter、.esc等),不要在新项目中使用数字形式。 - 最佳实践 :使用
@keydown而非@keypress(keypress已废弃,不触发功能键),方向键、功能键等应用@keydown。
【本章小结】
| 类别 | 示例 | 说明 |
|---|---|---|
| 别名 | .enter .esc .tab .space .delete .up .down .left .right |
语义化,推荐使用 |
| keyCode | .13 .27 .32 |
已废弃,Vue 3 移除 |
| 系统键 | .ctrl .alt .shift .meta |
需与其它键组合 |
| 组合键 | .ctrl.s .ctrl.enter |
多修饰符叠加 |
记忆口诀:「别名语义最清晰,系统键要配搭使,数字 keyCode 已废弃,Vue 3 请用 key 值」
【面试考点】
Q1:Vue2 的键盘修饰符中 keyCode 和别名分别是怎么实现的?
A:Vue 编译器将 @keyup.13 转换为 if(e.keyCode === 13) handler(e);将 @keyup.enter 转换为在 Vue 内部别名映射表中查找 enter 对应的 keyCode(13),再作比较。Vue 源码在 src/compiler/codegen/events.js 中处理这一转换。Vue 3 已移除数字 keyCode 修饰符,转为使用 KeyboardEvent.key 对应的连字符小写写法(如 .arrow-up)。
Q2:为什么 .ctrl 修饰符需要与字母键组合,而不能单独用 @keydown.ctrl?
A:从技术上看 @keydown.ctrl 是可以单独使用的,但 .ctrl 的语义是「在 Ctrl 键按下的状态下,某个键事件触发时满足条件」。单独的 @keydown.ctrl 在 Ctrl 本身被按下时就触发,与 @keydown.ctrl.s(Ctrl+S 组合键)在行为上不同。Vue 文档建议将 .ctrl 等系统修饰键与普通按键修饰符组合使用,形成有意义的快捷键。
四、v-bind 的 class 与 style 绑定
名词解释
- v-bind:class:动态绑定 class 属性,支持字符串、对象、数组三种值类型,Vue 会自动将其合并为最终的 class 字符串。
- v-bind:style:动态绑定 style 属性,支持字符串、对象(camelCase 属性名)、数组(多个对象合并)。
- 对象语法 :
{ 'class-name': Boolean }形式,Boolean 为true时类名生效,false时移除。 - 数组语法 :
['class-a', 'class-b']形式,或混合对象['class-a', { 'class-b': isActive }]。 - camelCase :驼峰命名,如
backgroundColor对应 CSS 的background-color,Vue 的 style 对象写法采用 camelCase。
概念与底层原理
v-bind:class 和 v-bind:style 是 Vue 为这两个特殊 HTML 属性提供的增强绑定。普通的 v-bind:href 只是简单的属性替换,而 class 和 style 具有合并 语义------静态 class="base" 和动态 :class="{ active: true }" 可以同时存在,Vue 会将它们合并为 class="base active":
html
<!-- 静态 class + 动态 class 共存 -->
<div class="card" :class="{ selected: isSelected, disabled: isDisabled }">
</div>
<!-- 当 isSelected=true, isDisabled=false 时,渲染为:-->
<!-- <div class="card selected"> -->
【代码注释】静态 class="card" 与动态 :class="{ selected: isSelected }" 共存是 Vue 对 class 属性做特殊合并处理的结果:Vue 在编译阶段将静态 class 和动态 class 分别存储,渲染时拼接为最终字符串。其它普通属性(如 :id 和 id 同时出现)则会相互覆盖,只有 class 和 style 享有这一合并特权。市面应用 :Element UI 的 <el-button class="my-btn" type="primary"> 中,class="my-btn" 是用户自定义类,type="primary" 由组件内部转换为 :class="{ 'el-button--primary': true }" 动态追加,两者合并显示在最终的 DOM 上。
这一行为依赖 Vue 编译器对 class 和 style 属性的特殊处理,普通属性若同时有静态和动态绑定会互相覆盖。

【代码注释】该图展示 :class 的四种值类型(字符串、对象、数组、混合数组)如何各自处理、最终都汇入底部蓝色「Vue 合并算法」归一为一个 class 字符串。重点在绿色「对象」分支------{active: isActive} 按布尔值开关类名,是组件化开发中最常用的形式;以及最终合并节点强调的一点:静态 class 与动态 :class 会被合并而非覆盖,这是 Vue 对 class/style 两个属性的特殊待遇 (底层由 genData 阶段把 staticClass 与 class 分开存储、运行时再拼接)。市面应用 :几乎所有 UI 组件库(Element UI、Vant、Ant Design Vue)的组件内部都大量使用对象写法控制状态相关的样式类,详见 Class 与 Style 绑定。
4.1 class 绑定三种写法
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>v-bind:class 完整示例</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<style>
.base { padding: 8px 16px; border-radius: 4px; display: inline-block;
margin: 4px; border: 2px solid transparent; }
.primary { background: #3b82f6; color: #fff; }
.success { background: #22c55e; color: #fff; }
.danger { background: #ef4444; color: #fff; }
.outline { background: transparent; border-color: currentColor; }
.large { padding: 12px 24px; font-size: 16px; }
.disabled { opacity: 0.5; cursor: not-allowed; }
</style>
</head>
<body>
<div id="app">
<h4>① 字符串写法</h4>
<!-- staticClass + 动态字符串合并 -->
<span class="base" :class="strClass">字符串绑定</span>
<select v-model="strClass">
<option value="primary">primary</option>
<option value="success">success</option>
<option value="danger">danger</option>
</select>
<h4>② 对象写法(最常用)</h4>
<!-- 属性名是类名,值是布尔------控制类名是否生效 -->
<span class="base" :class="{
primary: type === 'primary',
success: type === 'success',
danger: type === 'danger',
outline: outline,
large: large,
disabled: disabled
}">
对象绑定
</span>
<br>
<label><input type="checkbox" v-model="outline"> outline</label>
<label><input type="checkbox" v-model="large"> large</label>
<label><input type="checkbox" v-model="disabled"> disabled</label>
<select v-model="type">
<option value="primary">primary</option>
<option value="success">success</option>
<option value="danger">danger</option>
</select>
<h4>③ 数组写法</h4>
<!-- 数组中可以混合字符串和对象 -->
<span class="base" :class="['primary', { large: large, outline: outline }]">
数组绑定
</span>
</div>
<script>
new Vue({
el: '#app',
data: {
strClass: 'primary',
type: 'primary',
outline: false,
large: false,
disabled: false
}
});
</script>
</body>
</html>
【代码注释】三种写法各有适用场景:字符串适合从数据中直接读取类名(如 type 字段直接就是类名);对象适合需要同时控制多个状态类的组件;数组适合合并固定类和动态条件类。注意静态 class="base" 和动态 :class 可以共存,Vue 会自动合并。市面应用 :Button 组件通常用对象写法 :class="{ 'el-button--primary': type==='primary', 'is-disabled': disabled }" 来控制变体样式。
4.2 style 绑定三种写法
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>v-bind:style 完整示例</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<style>
.box { width: 150px; height: 80px; display: flex; align-items: center;
justify-content: center; border-radius: 4px; margin: 8px;
font-weight: bold; color: #fff; }
</style>
</head>
<body>
<div id="app">
<h4>① 字符串写法(不推荐,失去响应式优势)</h4>
<div class="box" :style="strStyle">字符串</div>
<h4>② 对象写法(推荐,属性名用 camelCase)</h4>
<div class="box" :style="objStyle">对象</div>
<div class="box" :style="{ backgroundColor: bg, fontSize: size + 'px' }">
内联对象
</div>
<h4>③ 数组写法(合并多个样式对象)</h4>
<div class="box" :style="[baseStyle, extraStyle]">数组合并</div>
<hr>
<label>背景色:<input type="color" v-model="bg"></label>
<label>字体大小:<input type="range" min="12" max="28" v-model.number="size"> {{ size }}px</label>
</div>
<script>
new Vue({
el: '#app',
data: {
bg: '#3b82f6',
size: 14,
// 字符串:不推荐,响应式差
strStyle: 'background: #22c55e; font-size: 14px;',
// 对象:推荐
objStyle: { backgroundColor: '#a855f7', fontSize: '14px' },
// 数组:合并多个对象
baseStyle: { backgroundColor: '#f97316', color: '#fff' },
extraStyle: { fontSize: '16px', borderRadius: '8px' }
}
});
</script>
</body>
</html>
【代码注释】style 绑定的核心点:① 对象写法的属性名必须是 camelCase(backgroundColor 而非 background-color),也可以用带引号的 kebab-case('background-color');② Vue 2 会自动添加浏览器前缀(如 -webkit-transform),无需手写;③ 数组写法将多个样式对象合并,后面的对象属性会覆盖前面的同名属性,适合基础样式 + 扩展样式的组合模式。市面应用 :进度条组件 :style="{ width: progress + '%' }" 是最经典的动态 style 使用场景。
【实战要点】
- 经典应用场景 :主题换肤功能通常通过
:style="{ '--primary': themeColor }"绑定 CSS 变量实现;卡片组件的选中态通过:class="{ selected: isSelected }"切换。 - 常见坑 :在
:class对象中将类名写错(如{ 'btn-primary ': true }末尾有空格),导致类名不匹配 CSS 选择器,样式不生效,且难以调试。另一个坑是:style中数字值忘记拼接单位------fontSize: size是数字,应写fontSize: size + 'px',否则无效。 - 最佳实践 :复杂的 class 逻辑应提取到
computed属性中,保持模板简洁:<div :class="buttonClasses">配合computed: { buttonClasses() { return { ... } } }。
【本章小结】
| 绑定目标 | 写法 | 特点 |
|---|---|---|
:class 字符串 |
:class="'a b'" |
直接作为类名字符串 |
:class 对象 |
:class="{a: true, b: false}" |
按布尔控制类名,最常用 |
:class 数组 |
:class="['a', {b: true}]" |
合并固定类 + 条件类 |
:style 字符串 |
:style="'color:red'" |
不推荐,失去响应式优势 |
:style 对象 |
:style="{color: 'red'}" |
属性名 camelCase,推荐 |
:style 数组 |
:style="[obj1, obj2]" |
合并多个样式对象 |
记忆口诀:「class 对象控状态,style 对象 camelCase,数组合并最灵活,复杂逻辑提 computed」
【面试考点】
Q1:为什么 Vue 的 :style 属性名要用 camelCase,而不是 CSS 中的 kebab-case?
A:因为 JS 对象的属性名若使用 - 连字符需要加引号(如 {'background-color': 'red'}),而 camelCase 可以不加引号({backgroundColor: 'red'}),更符合 JS 的书写习惯。Vue 两种写法都支持,但官方推荐 camelCase 以减少引号书写。底层处理时,Vue 的 setStyle 工具函数(源码 src/platforms/web/runtime/modules/style.js)会自动将 camelCase 转为连字符形式设置到 element.style 上,并在需要时自动添加浏览器厂商前缀。
Q2:静态 class 和动态 :class 可以同时写在同一个元素上吗?
A:可以,Vue 对 class 和 style 做了特殊的合并处理。静态 class="base" 与动态 :class="{ active: true }" 共存时,渲染结果为 class="base active"。这是 Vue 对这两个属性的特殊处理------其它属性(如 id、href)若同时有静态值和 v-bind 则会互相覆盖。
五、v-cloak 与 FOUC 问题
名词解释
- FOUC(Flash of Unstyled Content) :页面加载时短暂出现未处理内容的闪烁问题。在 Vue 中特指 Vue 实例尚未接管 DOM 之前,模板中的
{{ }}插值表达式被当作普通文本显示出来的闪烁现象。 - v-cloak :Vue 提供的特殊指令,不需要表达式,Vue 实例接管 DOM 后会自动移除该属性,可配合 CSS 选择器
[v-cloak] { display: none }在 Vue 启动前隐藏模板内容。 - Mustache(胡须)语法 :双花括号
{{ }}插值表达式的别称,源于其外形像胡须。
5.1 FOUC 的根因
浏览器渲染 HTML 是线性的:解析 HTML → 遇到 <script> 停止解析(若未加 defer/async)→ 下载并执行 JS → 继续解析。当 Vue 的 JS 放在 <body> 底部时,HTML 先被渲染,{{ message }} 等插值被直接显示为字符串,直到 Vue 实例创建完毕后才被替换。在网络较慢或 JS 较大时,这段「闪烁」时间可达数秒:

【代码注释】该图自上而下呈现一次页面加载的时间线:蓝色「解析 HTML 生成 DOM」后,由于 vue.js 尚未执行,红色两步「显示原始模板字符串 → 用户看到 mustache 插值闪烁」正是 FOUC 发生的窗口;直到黄色「加载执行 vue.js」、紫色「Vue mount 接管并自动移除 v-cloak」,最终绿色「显示正确内容」FOUC 才结束。重点看那条绿色虚线旁路------[v-cloak]{display:none} 让元素在整个红色窗口内保持隐藏,直接从「闪烁前」跳到「正确内容」,把闪烁彻底跳过。网速越慢、JS 体积越大,红色窗口越长,这也是 v-cloak 价值最大的场景。市面应用:用 CDN 引入 Vue 的老项目以及低端设备上容易复现此问题;脚手架项目由于打包处理、Vue 先于 DOM 初始化,一般不会出现。
5.2 v-cloak 的解决原理
v-cloak 的解决方案分两步:
- 用 CSS 选择器将带有
v-cloak属性的元素隐藏 - Vue 实例挂载完成后自动移除
v-cloak属性,元素恢复显示
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>v-cloak 解决 FOUC</title>
<!-- 第一步:CSS 声明------有 v-cloak 属性时隐藏 -->
<style>
[v-cloak] {
display: none;
}
body { font-family: sans-serif; padding: 20px; }
.container { border: 2px solid #3b82f6; padding: 16px; border-radius: 8px; }
</style>
</head>
<body>
<!-- 没有 v-cloak 的区域(会 FOUC) -->
<div id="no-cloak" style="border:2px solid #ef4444;padding:16px;border-radius:8px;margin-bottom:16px">
<p>没有 v-cloak:{{ message }}</p>
<p>在 Vue 启动前会看到 "{{ message }}" 字符串</p>
</div>
<!-- 有 v-cloak 的区域(防 FOUC) -->
<div id="app" v-cloak class="container">
<h3>{{ title }}</h3>
<p>用户名:{{ user.name }}</p>
<p>角色:{{ user.role }}</p>
<ul>
<li v-for="item in items" :key="item.id">{{ item.text }}</li>
</ul>
</div>
<!-- 故意延迟加载 Vue 来演示效果 -->
<script>
// 模拟 Vue 延迟 1.5 秒才加载(生产中是网络延迟)
setTimeout(() => {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js';
script.onload = () => {
new Vue({
el: '#app',
data: {
title: 'v-cloak 防闪烁演示',
user: { name: '张三', role: '管理员' },
items: [
{ id: 1, text: '内容条目一' },
{ id: 2, text: '内容条目二' },
{ id: 3, text: '内容条目三' }
]
}
});
// no-cloak 区域不用 Vue 接管,只是演示对比
};
document.head.appendChild(script);
}, 1500);
</script>
</body>
</html>
【代码注释】示例通过 setTimeout 模拟 Vue 延迟加载(对应真实场景的网络延迟)。1.5 秒内,红框区域(无 v-cloak)会显示原始模板字符串;蓝框区域(有 v-cloak)因为 CSS [v-cloak]{display:none} 而完全隐藏,Vue 就绪后 v-cloak 属性被移除,内容才渲染出来,彻底消除闪烁。市面应用:直接引入 CDN Vue 的传统后台页面、PHP/Java 模板与 Vue 混用的项目,必须使用 v-cloak;Webpack/Vite 工程化项目因为 Vue 先于 DOM 初始化,通常无需使用。
【实战要点】
- 经典应用场景:v-cloak 最适用于「Vue 代码直接写在 HTML 页面中」的场景,如 Django/Spring MVC 项目中嵌入局部 Vue 交互。
- 常见坑 :
[v-cloak]的 CSS 必须放在<head>中,且不能被display:none的父元素影响(否则 Vue 挂载后内容仍然不可见);<style>写在<body>底部时,CSS 晚于 DOM 渲染,v-cloak 无效。 - 最佳实践 :在实际工程化项目(Vite/Webpack)中,Vue 通过 JS 动态挂载,不存在 FOUC,无需使用 v-cloak。遇到 FOUC 问题应首先考虑「将
<script>标签移到</body>前」,若仍有闪烁再使用 v-cloak。
【本章小结】
| 对比项 | 不使用 v-cloak | 使用 v-cloak |
|---|---|---|
| Vue 启动前 | {{ message }} 明文显示 |
整块元素隐藏 |
| Vue 启动后 | 正常渲染 | v-cloak 移除,正常渲染 |
| 适用场景 | 工程化项目(无 FOUC) | CDN 引入 Vue 的传统页面 |
| CSS 要求 | --- | 必须在 head 中声明 [v-cloak]{display:none} |
记忆口诀:「cloak 遮住未就绪,Vue 启动自摘除,CSS 要放 head 里,工程化项目用不到」
【面试考点】
Q1:v-cloak 的工作原理是什么?与直接用 display:none 隐藏有何区别?
A:v-cloak 是 Vue 的特殊指令,不需要表达式。当 HTML 解析完成时,带有 v-cloak 的元素拥有该属性,CSS 选择器 [v-cloak]{display:none} 匹配并隐藏它;当 Vue 实例完成 $mount 挂载后,Vue 会遍历所有 DOM 节点,自动移除 v-cloak 属性,元素恢复显示。与手动 display:none 的区别在于:手动隐藏需要 Vue 启动后再手动改回 display:block;v-cloak 则是「自动解除」的,与 Vue 生命周期绑定,无需额外代码。
Q2:v-cloak 和 v-pre、v-once 这几个「无表达式指令」分别解决什么问题?容易混淆吗?
A:三者都是「不需要绑定值」的指令,但目标完全不同。v-cloak 解决的是渲染时机 问题------在 Vue 接管前隐藏未编译模板,防止 FOUC 闪烁;v-pre 解决的是跳过编译 问题------告诉编译器「这块原样输出,不解析 {{ }}」,常用于展示插值语法本身或优化大量纯静态节点的编译性能;v-once 解决的是渲染次数问题------元素及其子节点只渲染一次,后续数据变化不再更新,用于纯静态内容的性能优化。记忆要点:cloak 管「先藏后显」、pre 管「不编译」、once 管「只渲一次」。
Q3:在 Webpack/Vite 工程化项目中为什么通常不需要 v-cloak?什么情况下工程化项目仍会出现闪烁?
A:工程化项目里 Vue 通过 JS 在 #app 挂载点上动态生成并替换 DOM,挂载点初始往往只是一个空的 <div id="app"></div>,模板字符串根本不会以明文形式出现在初始 HTML 中,因此不存在 {{ }} 闪烁,自然无需 v-cloak。但若项目改用了 SSR/预渲染 或在 index.html 中手写了带插值的静态骨架,又或首屏 JS 包过大导致 hydration(水合)延迟,仍可能出现「先看到骨架/旧内容再被替换」的闪烁------此时应通过骨架屏(Skeleton)、首屏占位或 SSR 直出来优化,而非依赖 v-cloak。
六、综合案例:动态新闻列表
本节将 class 绑定、v-on 事件、v-for、v-show 综合运用,实现一个带标签页切换的动态新闻列表。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>动态新闻列表</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif;
background: #f8fafc; padding: 24px; }
.news-app { max-width: 640px; margin: 0 auto;
background: #fff; border-radius: 12px;
box-shadow: 0 4px 24px rgba(0,0,0,.08); overflow: hidden; }
/* 标签栏 */
.tabs { display: flex; border-bottom: 2px solid #e2e8f0; }
.tab-btn { flex: 1; padding: 14px 0; border: none; background: none;
cursor: pointer; font-size: 15px; font-weight: 500; color: #64748b;
transition: color .2s, border-bottom .2s; position: relative; }
.tab-btn.active { color: #3b82f6; }
.tab-btn.active::after {
content: ''; position: absolute; bottom: -2px; left: 0; right: 0;
height: 2px; background: #3b82f6; }
.tab-btn:hover:not(.active) { color: #1e40af; background: #f1f5f9; }
/* 新闻列表 */
.news-panel { padding: 0; }
.news-item { display: flex; padding: 16px 20px; border-bottom: 1px solid #f1f5f9;
align-items: flex-start; transition: background .15s; }
.news-item:hover { background: #f8fafc; }
.news-item:last-child { border-bottom: none; }
.news-num { width: 24px; height: 24px; border-radius: 50%;
background: #e2e8f0; color: #64748b; font-size: 12px; font-weight: bold;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0; margin-top: 2px; margin-right: 12px; }
.news-num.hot { background: #ef4444; color: #fff; }
.news-title { font-size: 14px; color: #1e293b; line-height: 1.5;
text-decoration: none; }
.news-title:hover { color: #3b82f6; }
.news-empty { padding: 40px; text-align: center; color: #94a3b8; }
</style>
</head>
<body>
<div id="app">
<div class="news-app">
<!-- 标签栏:v-for 生成,@click 切换,:class 控制激活态 -->
<div class="tabs">
<button
v-for="(tab, i) in newsList"
:key="tab.id"
class="tab-btn"
:class="{ active: currentIndex === i }"
@click="currentIndex = i">
{{ tab.typeName }}
</button>
</div>
<!-- 新闻面板:v-show 控制显示 -->
<div
v-for="(tab, i) in newsList"
:key="tab.id"
class="news-panel"
v-show="currentIndex === i">
<div v-if="tab.items.length === 0" class="news-empty">
暂无新闻
</div>
<div
v-for="(item, j) in tab.items"
:key="item.id"
class="news-item">
<!-- 前三条标红 -->
<span class="news-num" :class="{ hot: j < 3 }">{{ j + 1 }}</span>
<a class="news-title" :href="item.href" target="_blank">
{{ item.title }}
</a>
</div>
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
currentIndex: 0,
newsList: [
{
id: 1,
typeName: '科技',
items: [
{ id: 101, title: '人工智能大模型推理效率提升三倍,推动端侧部署普及', href: '#' },
{ id: 102, title: '开源操作系统内核发布重大安全更新,修复多个高危漏洞', href: '#' },
{ id: 103, title: '新一代芯片制程工艺突破 2nm,量产计划提前至明年', href: '#' },
{ id: 104, title: '量子计算领域再获突破,相干时间延长至分钟级', href: '#' }
]
},
{
id: 2,
typeName: '财经',
items: [
{ id: 201, title: '央行公布第三季度货币政策报告,M2 增速保持稳定', href: '#' },
{ id: 202, title: '新能源车企季度交付数据出炉,整体同比增长 35%', href: '#' },
{ id: 203, title: '消费电子行业出货量环比回升,库存去化基本完成', href: '#' }
]
},
{
id: 3,
typeName: '体育',
items: [
{ id: 301, title: '全国乒乓球锦标赛男单决赛落幕,张三以 4:1 夺冠', href: '#' },
{ id: 302, title: '国内篮球职业联赛新赛季球队名单公布,引援动作频繁', href: '#' },
{ id: 303, title: '马拉松大赛创历史参赛规模,完赛率达新高', href: '#' },
{ id: 304, title: '青少年足球培训体系调整,校园联赛扩大覆盖范围', href: '#' },
{ id: 305, title: '冬季运动项目备战工作全面启动,重点聚焦速度滑冰', href: '#' }
]
}
]
}
});
</script>
</body>
</html>
【代码注释】本示例综合运用了本篇所有核心知识点:
- v-for + :key :遍历
newsList生成标签按钮和内容面板,key使用数据id而非 index,帮助 Vue 的 Diff 算法精确复用 DOM。 - :class 对象写法 :
.active类通过{ active: currentIndex === i }控制激活态高亮,.hot类通过{ hot: j < 3 }将前三条标红,是对象语法的经典应用。 - @click 语句写法 :
@click="currentIndex = i"直接更新索引,逻辑简单无需抽取为方法。 - v-show :切换面板用
v-show而非v-if,因为标签页切换频繁,v-show(CSS display 切换)比v-if(DOM 销毁/重建)性能更好。
市面应用 :新闻门户、电商平台的商品分类列表、管理后台的 Tab 面板,都是这套「标签页 + 列表」模式的变体。Element UI 的 <el-tabs> 组件底层也是类似实现。
【实战要点】
- 经典应用场景:Tab 标签页是最常见的 UI 模式之一,适合「同一区域展示不同分类内容」的场景,如商品分类、新闻分类、设置面板。
- 常见坑 :标签页用
v-if实现时,切换会销毁/重建组件,导致内部状态(如滚动位置、表单输入)丢失;应根据业务需求选择v-show(保留状态)或v-if(重置状态)。 - 最佳实践:数据量大时,将各标签页内容分开请求(懒加载),而非一次性加载所有数据;当前已渲染的标签页数据缓存在 Vue 实例中,切回时无需再次请求。
【本章小结】
| 技术点 | 在案例中的体现 |
|---|---|
| v-for + :key | 遍历标签和面板,key 用 id |
| :class 对象 | .active 切换激活态,.hot 高亮前三条 |
| @click 语句 | currentIndex = i 直接更新状态 |
| v-show | 切换面板,保留 DOM,性能更好 |
| v-if | 空列表降级显示「暂无新闻」 |
总结
知识点回顾(思维导图)

【代码注释】该图以蓝色「Vue2 事件系统与指令进阶」为中心,从左向右辐射出五大主题容器:绿色 v-on 事件绑定(三种写法 + $event 传递 + 与 onclick 的区别)、黄色事件修饰符(按「运行时注入 / 注册配置」分类)、紫色键盘修饰符(别名 / keyCode / 系统键)、橙色 class/style 绑定(三种值类型 + 合并语义)、红色 v-cloak(FOUC 解决方案)。每个叶节点都对应本篇的一个示例。学习建议:把本图与实际代码对照复习,遇到模糊的点直接回到对应颜色的章节;按颜色块自测能否复述每个叶节点的底层原理,是检验掌握程度的快捷方式。
高频面试题速查
| 题目 | 核心答案关键词 |
|---|---|
| v-on:click="fn" 和 v-on:click="fn()" 的区别 | 函数引用 vs 函数调用;自动传 event vs 手动 $event |
| .stop 和 .self 的区别 | stopPropagation vs target===currentTarget |
| .prevent 和 return false 的区别 | Vue 中 return false 无效,必须用 .prevent 或 e.preventDefault() |
| .passive 的作用 | options.passive=true,浏览器提前滚动,不等待 JS |
| .capture 和默认冒泡的顺序 | capture 先于 bubble;捕获阶段从外向内,冒泡从内向外 |
| 为什么 :class 和 :style 可以与静态 class/style 共存 | Vue 对这两个属性做了特殊的合并处理 |
| v-cloak 的工作原理 | Vue 挂载前保留属性,CSS 选择器隐藏;挂载后自动移除属性 |
| 键盘修饰符中别名 vs keyCode 数字 | 别名更语义化,keyCode 已废弃(W3C),Vue 3 已移除数字形式 |
学习建议
-
动手优先 :把本篇的每个示例都保存为
.html文件在浏览器中运行,打开 DevTools 观察事件监听器(Elements 面板 → Event Listeners),亲眼验证.capture、.passive等选项的设置。 -
源码参考 :条件允许时,阅读 Vue 2 编译器源码(
vue-template-compiler/src/codegen/events.js)中修饰符的代码生成部分,可以彻底理解编译时转换机制。 -
刷题巩固:按本篇「高频面试题速查」表逐题在纸上作答,重点练习「原理级」回答而非定义级回答------面试官区分候选人的核心是「知其然也知其所以然」。
-
延伸阅读:
- MDN --- EventTarget.addEventListener --- 深入理解
options参数 - MDN --- KeyboardEvent --- keyCode 废弃说明与 key 属性
- Vue 2 官方文档 --- 事件处理 --- 权威参考
- Vue 2 官方文档 --- Class 与 Style 绑定 --- 完整用法说明
- MDN --- EventTarget.addEventListener --- 深入理解