Vue 3 提供了 14 个内置指令,用于在模板中实现响应式行为、DOM 操作和性能优化。
一 、v-cloak
v-cloak 是 Vue 的一个编译控制指令 ,用于解决 未编译的模板(Mustache 标签)短暂显示 的问题(即"闪烁"现象)。它不依赖于响应式数据,只在组件编译阶段起作用。
v-cloak 的原理非常简单,分为两个层面:
- CSS 隐藏 :开发者需要编写 CSS 规则,例如
[v-cloak] { display: none; }。这样,带有v-cloak属性的元素会一开始就被隐藏。 - Vue 自动移除 :当 Vue 编译完该组件的模板后,会自动移除 元素上的
v-cloak属性,从而让元素显示出来。
因此,用户看到的效果是:模板内容在编译完成前是隐藏的,编译完成后立即显示,不会出现未编译的 {{ message }} 一闪而过。
在典型的基于 SFC(单文件组件)和构建工具(Vite、Webpack)的 Vue 项目中,模板会在编译时 被预编译为 render 函数,浏览器最终执行的是 render 函数生成的虚拟 DOM,不会 包含原始的 {{ message }} 语法。因此不会看到未编译的插值闪烁现象。
二、v-pre
v-pre 是 Vue 的一个编译跳过指令,用于告诉 Vue 编译器跳过该元素及其所有子元素的编译过程,直接输出原始内容。
【示例】
js
<template>
<div v-pre>
{{ message }}
<p v-text="message">这是一个段落</p>
</div>
</template>

js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("div", null, [..._cache[0] || (_cache[0] = [_createTextVNode(
" {{ message }} ",
-1
/* CACHED */
), _createElementVNode(
"p",
{ "v-text": "message" },
"这是一个段落",
-1
/* CACHED */
)])]);
}
解析器遇到 v-pre 指令时,会将当前节点及其所有子节点标记的属性转为普通属性。
因为没有生成任何响应式相关的渲染代码,这些节点在组件更新时不会被重新渲染或 diff。们始终以静态 HTML 的形式存在。
三、v-on
在 Vue 3 的事件处理体系中,v-on 指令是连接用户操作与 JavaScript 代码的核心纽带。无论是点击按钮、输入文本还是按下键盘,v-on 都扮演着"信号接收器"的角色,让开发者能够以声明式的方式响应用户的每一次交互
v-on 是什么?
v-on 是 Vue 3 中用于监听 DOM 事件 并在事件触发时执行指定代码的指令。它的简写形式是 @,这是开发中最常使用的写法
html
<!-- 完整写法 -->
<button v-on:click="handleClick">点击我</button>
<!-- 简写写法(最常用) -->
<button @click="handleClick">点击我</button>
v-on 基本使用方式
模板编译器通过检查 v-on 的值是否是合法的 JavaScript 标识符或属性访问路径来判断使用哪种处理器:
- 有括号的表达式(如
count++、handleClick())→ 内联语句处理器 - 无括号的标识符(如
handleClick、obj.method)→ 方法事件处理器
内联事件处理器
直接将 JavaScript 代码写在 v-on 的值中,适用于简单逻辑。当 v-on 的值是合法的 JavaScript 表达式或标识符时,Vue 会自动识别并处理。
【示例】简单内联表达式
html
<button @click="count++">点击我</button>
js
const count = ref(0);

【示例】多语句,用分号隔开
html
<button
@click="
count--;
console.log(count);
"
>
点击我
</button>
编译结果
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("button", { onClick: _cache[0] || (_cache[0] = ($event) => {
$setup.count--;
console.log($setup.count);
}) }, " 点击我 ");
}
【示例】内联箭头函数
html
<button
@click="
(event) => {
count++;
console.log(event, count);
}
"
>
点击我
</button>
编译结果
js
import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "/node_modules/.vite/deps/vue.js?v=b3e6ce82";
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("button", { onClick: _cache[0] || (_cache[0] = (event) => {
$setup.count++;
console.log(event, $setup.count);
}) }, " 点击我 ");
}
【示例】在内联处理器中可以直接调用方法并传递参数
html
<button @click="handleClick($event)">点击我</button>
【示例】在内联处理器中可以直接调用方法并传递参数
js
<button @click="handleClick($event, 'click')">点击我</button>
js
const handleClick = (event: PointerEvent, type: string) => {
count.value++;
console.log("点击了按钮", event, type);
};
【示例】
js
<template>
<div>
<p>当前数字 {{ count }}</p>
<button
@click.stop="
handleClick();
handleClick2($event);
"
>
增加
</button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);
const handleClick = () => {
count.value++;
};
const handleClick2 = (event: Event) => {
console.log(event);
};
defineOptions({
name: "CloudView",
});
</script>
编译结果
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("div", null, [_createElementVNode(
"p",
null,
"当前数字 " + _toDisplayString($setup.count),
1
/* TEXT */
), _createElementVNode("button", { onClick: _cache[0] || (_cache[0] = _withModifiers(($event) => {
$setup.handleClick();
$setup.handleClick2($event);
}, ["stop"])) }, " 增加 ")]);
}
【示例】
js
<template>
<div>
<p>当前数字 {{ count }}</p>
<button
@click="handleClick3"
@click.stop="
handleClick();
handleClick2($event);
"
>
增加
</button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);
const handleClick = () => {
count.value++;
};
const handleClick2 = (event: Event) => {
console.log(event);
};
const handleClick3 = () => {
console.log("点击了按钮");
};
defineOptions({
name: "CloudView",
});
</script>
编译结果
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("div", null, [_createElementVNode(
"p",
null,
"当前数字 " + _toDisplayString($setup.count),
1
/* TEXT */
), _createElementVNode("button", { onClick: [$setup.handleClick3, _cache[0] || (_cache[0] = _withModifiers(($event) => {
$setup.handleClick();
$setup.handleClick2($event);
}, ["stop"]))] }, " 增加 ")]);
}
方法事件处理器
对于复杂逻辑,推荐使用方法作为事件处理器。方法事件处理器会自动接收原生 DOM 事件对象作为参数。
【示例】默认传递原生事件对象
html
<button @click="handleClick">点击我</button>
<button v-on:click="handleClick">点击我</button>
js
const handleClick = (event: PointerEvent) => {
count.value++;
console.log("点击了按钮", event);
};
编译结果
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("button", { onClick: $setup.handleClick }, "点击我");
}
修饰符
一、事件修饰符
事件修饰符 是 Vue 为 v-on 指令(@)提供的特殊后缀,以声明式的方式解决事件处理中常见的底层 DOM 操作,避免在方法中手动调用 event.preventDefault()、event.stopPropagation() 等。
【示例 一】捕获模式而非冒泡 capture
js
<template>
<button @click.capture="handleClick">点击我</button>
</template>
编译结果
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock(
"button",
{ onClickCapture: $setup.handleClick },
"点击我",
32
/* NEED_HYDRATION */
);
}
【示例 二】事件只触发一次,然后自动解绑 once
js
<button v-on:click.once="handleClick4">点击我</button>
编译结果
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock(
"button",
{ onClickOnce: $setup.handleClick },
"点击我",
32
/* NEED_HYDRATION */
);
}
【示例 三】提示浏览器不会调用 preventDefault(),提升滚动性能
js
<template>
<button @click.passive="handleClick">click</button>
</template>
编译结果
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock(
"button",
{ onClickPassive: $setup.handleClick },
"click",
32
/* NEED_HYDRATION */
);
}
【示例 四】阻止默认行为 prevent event.preventDefault()
html
<template>
<a target="_blank" href="https://baidu.com" @click.prevent="handleClick">点击我</a>
</template>
编译结果
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("a", {
target: "_blank",
href: "https://baidu.com",
onClick: _withModifiers($setup.handleClick, ["prevent"])
}, "点击我");
}
【示例 五】仅当 event.target === 当前元素 时触发 self
js
<template>
<button @click.self="handleClick">click</button>
</template>
编译结果
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("button", { onClick: _withModifiers($setup.handleClick, ["self"]) }, "click");
}
【示例 六】停止事件冒泡 stop event.stopPropagation()
html
<button v-on:click.stop="handleClick">点击我</button>
编译结果
js
import { withModifiers as _withModifiers, openBlock as _openBlock, createElementBlock as _createElementBlock } from "/node_modules/.vite/deps/vue.js?v=b3e6ce82";
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("button", { onClick: _withModifiers($setup.handleClick, ["stop"]) }, "点击我");
}
【 Why?】once、passive、capture修饰符会编译成onClickCapture这样的属性,而其他修饰符则通过_withModifiers辅助函数处理?
capture, once, passive 是 addEventListener 的底层配置项。它们是浏览器在事件绑定初期 就需要确定的参数,可以在一个事件监听器上同时生效,所以 Vue 可以通过在编译阶段 修改属性名(如 onClick -> onClickCapture),在最终生成的原生事件绑定中一次性配置。
而 stop、prevent 则是对 事件回调函数行为的包装 。它们必须在事件触发时的 回调执行阶段 才能判断并生效,因此需要 Vue 在运行时(runtime)动态地创建一个包装函数(wrapper) ,在这个函数里按顺序执行 stopPropagation()、preventDefault() 等操作,最后才调用你定义的回调。
二、鼠标修饰符
.left左键(默认).right右键.middle中键
【示例】
js
<template>
<button @click.left="handleClick">click</button>
</template>
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("button", { onClick: _withModifiers($setup.handleClick, ["left"]) }, "click");
}
【示例】
js
<template>
<button @click.middle="handleClick">click</button>
</template>
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock(
"button",
{ onMouseup: _withModifiers($setup.handleClick, ["middle"]) },
"click",
32
/* NEED_HYDRATION */
);
}
【示例】
js
<template>
<button @click.right="handleClick">click</button>
</template>
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock(
"button",
{ onContextmenu: _withModifiers($setup.handleClick, ["right"]) },
"click",
32
/* NEED_HYDRATION */
);
}
对比编译区别?
.left: 保留原有事件(如click),修饰符仅用于运行时筛选。.right: 事件名被替换为contextmenu,这是一个专用于处理右键菜单的浏览器原生事件。.middle: 事件名被替换为mouseup,用于监听鼠标按键(包括中键)的释放动作
三、系统修饰符
Vue 为 v-on 指令提供了系统修饰符,用于实现仅在按下指定按键时才触发鼠标或键盘事件的监听器。
| 修饰符 | 对应按键 (Windows) | 对应按键 (macOS) |
|---|---|---|
.ctrl |
Ctrl 键 |
Control 键 |
.alt |
Alt 键 |
Option (⌥) 键 |
.shift |
Shift 键 |
Shift 键 |
.meta |
Windows (⊞) 键 |
Command (⌘) 键 |
【示例】ctrl
在 Windows 操作系统上,使用 Vue 的 @click.ctrl 修饰符时,click 事件会正常触发,并且事件处理函数会执行。 在 macOS 系统中,Control + 点击 的默认行为是触发右键菜单(上下文菜单) ,而不是 click 事件。因此,浏览器会优先派发 contextmenu 事件,而 click 事件根本不会被触发。
js
<template>
<button @click.ctrl="handleClick">click</button>
</template>
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("button", { onClick: _withModifiers($setup.handleClick, ["ctrl"]) }, "click");
}
【示例】 meta
在 Windows 系统中,.meta 修饰符对应的是代表 Win 键 (⊞); 在 macOS 系统中,Vue 的 .meta 修饰符对应的是 Command (⌘) 键。
js
<template>
<button @click.meta="handleClick">click</button>
</template>
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("button", { onClick: _withModifiers($setup.handleClick, ["meta"]) }, "click");
}
四、按键修饰符
| 修饰符 | 对应按键 | 说明 |
|---|---|---|
.enter |
Enter 键 | 回车 |
.tab |
Tab 键 | 制表符 |
.delete |
Delete 或 Backspace | 删除键(两者都匹配) |
.esc |
Escape | 退出键 |
.space |
Space | 空格键 |
.up |
上箭头 | ArrowUp |
.down |
下箭头 | ArrowDown |
.left |
左箭头 | ArrowLeft |
.right |
右箭头 | ArrowRight |
.page-up |
PageUp | 上翻页 |
.page-down |
PageDown | 下翻页 |
.home |
Home | 行首 |
.end |
End | 行尾 |
可以直接使用字母或数字作为修饰符(例如 .a、.1),Vue 会将其转换为对应的 event.key 值。
【示例】
js
<template>
<input @keyup.a="handleClick" />
</template>
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock(
"input",
{ onKeyup: _withKeys($setup.handleClick, ["a"]) },
null,
32
/* NEED_HYDRATION */
);
}
【示例】
js
<template>
<input @keyup.1="handleClick" />
</template>
编译阶段
一、解析指令 (Parse)
将<button @click="count++">这类模板解析为包含v-on指令信息的AST(抽象语法树)节点,包括事件名click、处理函数表达式count++等。
二、转换格式 (Transform)
这是核心环节,由transformOn函数完成。
- 处理事件名:将静态的
@click规范化为onClick,用于VNode的props。 - 处理动态事件名:处理
@[eventName],生成可解析动态事件名的代码。 - 包装处理函数:将如
count++的内联语句,包装成接收$event参数的事件处理函数$event => (count++)。
三、生成代码 (Codegen)
最终生成render函数,包含一个VNode,其props对象里有一个onClick属性,值为包装后的事件处理函数。
运行时阶段
当组件在浏览器中运行时,核心任务就是高效地将虚拟DOM映射到真实DOM上。
一、首次挂载
渲染器执行render函数生成VNode,patch过程中会为真实DOM绑定事件,主要依赖createInvoker这个工厂函数。
createInvoker:事件更新的性能关键 。createInvoker创建了一个特殊的函数(invoker),充当连接Vue虚拟DOM事件和真实浏览器事件的稳定桥梁
二、更新阶段
当父组件重新渲染导致事件处理函数改变时,渲染器会发现onClick属性变了,进入patchEvent逻辑。
- 发现改变:
onClick属性的新值和旧值不同。 - 更新
invoker:Vue不会 调用removeEventListener再addEventListener(传统方式性能差),而是找到该事件对应的invoker对象,直接更新其value属性。 - 自动生效:由于
invoker函数本身没有变,DOM上的监听器无需任何改动。下次事件触发时,执行的invoker会调用其value属性上已指向的新的事件处理函数。
四、v-once
在 Vue 3 的指令集中,v-once 是一个用于性能优化 的内置指令。它的核心作用是告诉 Vue: "这个元素及其所有子元素,只渲染一次,后续无论数据如何变化,都不要再更新它们了。"
html
<p>更新{{ title }}</p>
<p v-once>静态 {{ title }}</p>
示例
js
<template>
<div v-once>
<p>当前计数: {{ count }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);
defineOptions({
name: "CloudView",
});
</script>
编译结果
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _cache[0] || (_setBlockTracking(-1, true), (_cache[0] = _createElementVNode("div", null, [_createElementVNode(
"p",
null,
"当前计数: " + _toDisplayString($setup.count),
1
/* TEXT */
)])).cacheIndex = 0, _setBlockTracking(1), _cache[0]);
}
编译阶段
一、解析与标记
在编译的转换(Transform)阶段,transformOnce 函数会识别带有 v-once 指令的节点。 找到后,会为其打上"静态"标记,并在上下文中设置标记 context.inVOnce = true,同时将该指令从AST节点中移除。 该标记还会被递归地应用到其所有子节点上,确保整个子树都被视为静态内容。
二、代码生成
在生成(Generate)阶段,被打上标记的节点会通过 context.cache() 方法进行缓存。 最终,生成的渲染函数会直接引用一个模块级常量(即被静态提升的节点),而不是在每次渲染时重新创建,从而最大程度地减少运行时开销。
运行阶段
- 首屏渲染:组件首次渲染时,
render函数会正常执行,并使用缓存的VNode来创建DOM。 - 跳过更新:当组件内的响应式数据发生变化,触发重新渲染时,Vue 的
diff算法会检测到这些静态节点上的PatchFlags.STATIC标志。一旦识别到该标志,渲染器会完全跳过对该节点及其子树的所有更新流程,直接复用第一次渲染时缓存的VNode和对应的真实DOM
transformOnce
vue3-core/packages/compiler-core/src/transforms/vOnce.ts
transformOnce 是 Vue 3 编译器(compiler-core)中用于处理 v-once 指令的节点转换函数。它在 AST 转换阶段识别带有 v-once 指令的元素,并标记该子树为"一次性渲染"区域,最终通过缓存机制使其在后续渲染中直接被复用。
js
const transformOnce: NodeTransform = (node, context) => {
// 只处理元素节点,查找节点上的 v-once 指令
if (node.type === NodeTypes.ELEMENT && findDir(node, 'once', true)) {
// 检查节点是否已经被处理过,避免重复处理
if (seen.has(node) || context.inVOnce || context.inSSR) {
return
}
seen.add(node)
context.inVOnce = true // 标记为 v-once 处理中
// 注入运行时辅助函数,用于暂时禁用块追踪(Block Tracking)
context.helper(SET_BLOCK_TRACKING)
return () => {
context.inVOnce = false
const cur = context.currentNode as ElementNode | IfNode | ForNode
if (cur.codegenNode) {
cur.codegenNode = context.cache(
cur.codegenNode,
true /* isVNode */, // 缓存 VNode 节点
true /* inVOnce */, // 标记为 v-once 处理中,运行时会在首次渲染后永久复用该 VNode
)
}
}
}
}
五、v-memo
v-memo 的核心机制是:它接收一个依赖值数组,并缓存该元素及其子树的虚拟 DOM(VNode)。只有当数组中的某个依赖项的值与上一次渲染不同时,Vue 才会重新渲染该子树;否则,将直接复用缓存,跳过整个渲染和差异比对(diff)过程。
使用
js
<template>
<div v-memo="[count]">
{{ count }}
</div>
</template>
编译结果
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _withMemo([$setup.count], () => (_openBlock(), _createElementBlock("div", null, [_createTextVNode(
_toDisplayString($setup.count),
1
/* TEXT */
)])), _cache, 0);
}
编译阶段
在编译阶段,Vue 的 transformMemo 转换器会识别并处理 v-memo 指令。它只对元素节点生效,并跳过服务端渲染(SSR)场景。
其核心任务是,将带有 v-memo 的节点(_createVNode 调用)包裹在一个名为 _withMemo 的运行时辅助函数调用中。
运行阶段
运行时,渲染函数开始执行。当执行到编译阶段生成的 _withMemo 函数时,核心逻辑如下:
1、执行 _withMemo 函数:该函数接收编译时传入的依赖数组、渲染函数、缓存对象和缓存索引。
2、判断是否命中缓存:它会根据索引查找 _cache 对象中是否已存在 VNode。如果存在,则通过 isMemoSame 函数,使用 Object.is 逐一比较新旧依赖数组中的每一项是否完全一致。
3、渲染或复用
- 命中缓存:如果依赖数组各项均未改变,
_withMemo将直接返回缓存的 VNode,从而完全跳过了执行渲染函数、创建新 VNode 以及后续的 diff 和 DOM 更新。 - 未命中缓存:如果依赖数组发生变化,
_withMemo则会执行传入的渲染函数,生成新的 VNode,并将其更新到缓存中,以供下一次渲染使用
六 、v-if | v-else-if | v-else
Vue 3 中的 v-if、v-else-if、v-else 是用于条件渲染的指令,它们根据表达式的真假值,决定是否将元素或组件渲染到 DOM 中。
使用
【示例】 基本使用
js
<template>
<div>
<h3>v-if 指令</h3>
<div v-if="show">{{ title }}</div>
<div v-else>暂无数据</div>
<button @click="show = !show">切换显示</button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const title = ref("列表");
const show = ref(false);
</script>

【示例】
html
<template>
<div>
<div v-if="type === 1">primary</div>
<div v-else-if="type === 2">暂无数据</div>
<div v-else>其他</div>
<button @click="type++">切换显示</button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const type = ref(0);
</script>
编译结果
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("div", null, [
$setup.type === 1 ? (_openBlock(), _createElementBlock("div", _hoisted_1, "primary")) : $setup.type === 2 ? (_openBlock(), _createElementBlock("div", _hoisted_2, "\u6682\u65E0\u6570\u636E")) : (_openBlock(), _createElementBlock("div", _hoisted_3, "\u5176\u4ED6")),
_createElementVNode("button", {
onClick: _cache[0] || (_cache[0] = ($event) => $setup.type++)
}, "\u5207\u6362\u663E\u793A")
]);
}

编译阶段
一、解析阶段:构建条件链表
编译器在解析模板时,会将连续的 v-if、v-else-if、v-else 节点合并为一个 条件节点 (NodeTypes.IF),其 branches 属性存储各个分支。
二、转换阶段:生成条件表达式
在 transformIf 函数(packages/compiler-core/src/transforms/vIf.ts)中,编译器将条件节点转换为一个 三元运算符链 ,并为每个分支包裹 openBlock() / createBlock() 调用,因为每个分支都是一个独立的 Block。
三、代码生成阶段:输出 JavaScript
最终生成的代码是一个嵌套的三元运算符,每个分支都使用 openBlock() / createBlock() 来创建 Block。
v-if 会被编译器转换为条件语句 (三元运算符或 if 分支),在生成的渲染函数中,根据条件返回不同的虚拟 DOM。
运行阶段
- 当条件改变时,Vue 的响应式系统触发重新渲染。
- 渲染函数重新执行,如果条件从假变为真,则创建新的 VNode 并挂载到 DOM;如果从真变为假,则移除对应的 VNode。
七、v-show
v-show 是用于条件显示的内置指令。与 v-if 不同,v-show 不会销毁或重建元素,而是通过切换 CSS 的 display 属性来控制元素的可见性。这意味着元素始终存在于 DOM 中,只是被隐藏或显示。
v-show 的使用
【示例】基本使用
js
<template>
<div>
<div v-show="show">this 展示区</div>
<button @click="show = !show">切换显示</button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const show = ref(true);
</script>


使用限制
v-show不支持<template>元素,因为<template>不会生成真实的 DOM 节点,无法应用display样式。

v-show没有v-else或v-else-if的配套指令,它仅单独控制单个元素的显隐。- v-show 必须要有表达式。

- 不要与 v-for 同时使用。
v-for和v-show同时使用虽然不会报错,但会导致每次列表变化时重新计算显示状态,性能较差。推荐将v-show放在v-for内部的元素上,或者使用计算属性过滤后再用v-for。
编译阶段
v-show 会被编译为一个指令,生成一个用于控制 display 的绑定。
运行阶段
当绑定的值变化时,Vue 会直接更新该元素的 style.display 属性(设为 none 或移除/恢复原值)。
八、v-for
【示例】基础使用
js
<template>
<div>
<ul>
<li v-for="item in books" :key="item">
<span>
{{ item }}
</span>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const books = ref(["book1", "book2", "book3", "book4", "book5"]);
</script>
编译结果
js
import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, toDisplayString as _toDisplayString, createElementVNode as _createElementVNode } from "/node_modules/.vite/deps/vue.js?v=efe42f93";
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("div", null, [
_createElementVNode("ul", null, [
(_openBlock(true), _createElementBlock(
_Fragment,
null,
_renderList($setup.books, (item) => {
return _openBlock(), _createElementBlock("li", { key: item }, [
_createElementVNode(
"span",
null,
_toDisplayString(item),
1
/* TEXT */
)
]);
}),
128
/* KEYED_FRAGMENT */
))
])
]);
}

【示例】
js
<template>
<div>
<ul>
<li v-for="(item, index) in books" :key="item + index">
<span>
{{ item }}
</span>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const books = ref(["book1", "book2", "book3", "book4", "book5"]);
</script>

注意事项
- 必须为每个列表项提供一个唯一的
key,且不建议使用index作为key(除非列表是静态的且不会重新排序) - 在 Vue 3 中,
v-if的优先级高于v-for(与 Vue 2 相反)。这意味着当它们同时用在同一个元素上时,v-if会先执行,无法访问v-for作用域内的变量。 v-for可以遍历对象的属性,顺序基于Object.keys()的返回值。不建议直接遍历对象。v-for可以接受整数,渲染指定数量的元素。- 避免在
v-for内部使用复杂的计算属性或方法:每次重新渲染都会重新执行,建议将计算结果提前到列表数据源中。 - 使用
v-memo缓存子树(Vue 3.2+):当子树的依赖很少变化时,使用v-memo跳过不必要的更新。
编译阶段
1、在 parse 阶段,v-for="..." 中的表达式会以原始的字符串形式被记录在 AST 节点的 props 属性中,尚未被处理。
2、转换。
解析 v-for="(item, index) in list" 字符串,提取出 source(数据源,如 list)、value(迭代项,如 item)和 key(索引,如 index),并存入一个专门的 ForParseResult 对象中。
构建 ForNode:基于解析结果,原始的节点会被替换成一个新的、类型为 ForNode 的 AST 节点。
3、生成代码。
最终生成的渲染函数会包含对 _renderList 这个运行时辅助函数的调用。
九、v-text
v-text 是 Vue 3 中用于更新元素文本内容 的内置指令。它将数据绑定到 DOM 元素的 textContent 属性,确保视图与数据保持同步。与插值语法 {{ }} 相比,v-text 提供了一种更显式的方式来控制元素的全部文本内容,并且会完全覆盖元素原有的子节点。
基本使用
js
<template>
<div v-text="title"></div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const title = ref("这是一段话题。<p>这是段落1。</p>");
</script>
展示效果

编译结果

【示例】
js
<template>
<div v-text="count"></div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);
defineOptions({
name: "CloudView",
});
</script>
编译结果
js
const _hoisted_1 = ["textContent"];
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("div", { textContent: _toDisplayString($setup.count) }, null, 8, _hoisted_1);
}
【示例】插值{{}}
js
<template>
<div>
{{ count }}
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);
defineOptions({
name: "CloudView",
});
</script>
编译结果
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock(
"div",
null,
_toDisplayString($setup.count),
1
/* TEXT */
);
}
使用限制
- 避免与子节点共存:
v-text会覆盖元素的所有子内容,因此在同一个元素上,不应同时使用v-text并编写其他子节点

- 仅用于纯文本:
v-text会将 HTML 标签作为纯文本转义输出,不能解析 HTML 结构。 - 相比于插值语法,
v-text避免了模板编译时的碎片化文本节点,性能上微乎其微,但可读性较差,通常推荐使用{{ }}。
编译阶段
v-text 会被解析为指令,生成代码直接设置 textContent。
运行阶段
当绑定的数据变化时,Vue 会更新该元素的 textContent 属性。
十、v-html
v-html 是 Vue 3 中用于将原始 HTML 字符串渲染为真实 DOM 元素 的内置指令。与 v-text 或插值语法不同,v-html 会将其内容作为 HTML 解析并插入到元素中,而不是作为纯文本。
【示例】基本使用
js
<template>
<div v-html="title"></div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const title = ref("这是一段话题。<p>这是段落1。</p>");
</script>
编译结果

十一、v-bind
v-bind 指令是数据驱动视图的核心桥梁。能够将 JavaScript 数据动态绑定到 HTML 属性、组件属性(props)甚至 CSS 样式上。当绑定的数据发生变化时,视图会自动更新------你只需关心数据的变化,Vue 会高效地完成 DOM 的更新工作,无需手动操作 DOM。
v-bind是什么?
v-bind 是 Vue 3 中用于动态绑定一个或多个属性 的指令,可以绑定 HTML 元素的原生属性(如 src、href、title 等),也可以绑定组件的 props,还能绑定 class 和 style 等特殊属性
v-bind 使用
语法
html
<!-- 完整语法 v-bind:属性名="JavaScript表达式" -->
<div v-bind:title="title">这是一个div</div>
<!-- 缩写语法(推荐使用) :属性名="JavaScript表达式" -->
<div :title="title">这是一个div</div>
<!-- 当属性名与变量名完全相同时,可以省略表达式,直接写成 `:属性名` -->
<div :title>这是一个div</div>
基础使用
HTML 属性 和 DOM 属性的区别?
- HTML 属性只负责初始状态,不会自动同步到 DOM 属性。
- DOM 属性是当前状态 ,用户交互或 JS 修改后会更新。
例如:用户在<input>中输入新内容,DOM 属性value改变,但 HTML 属性value不会变
【示例】绑定 HTML 属性
写在 HTML 标签上的静态文本,由浏览器解析后成为 DOM 节点的初始值。属性名通常是全小写。
html
<div :aria-label="title">hello</div>
因为 DOM 中不存在 ariaLabel 属性(虽然可以通过 setAttribute 设置),Vue 会将其作为 HTML 属性处理。

【示例】data-* 自定义属性
js
<div :data-id="title">ID</div>
【示例】 绑定 DOM 属性
浏览器解析 HTML 后生成的 DOM 对象上的动态属性,可以通过 JavaScript 读写,值会随用户交互变化。
html
<div v-bind:title="title">这是一个div</div>
<div :title="title">这是一个div</div>
<div :title>这是一个div</div>

js
const title = ref("hello vue3");
绑定 class 和 style
它们既是 HTML 属性,又是 DOM 属性,但 Vue 做了特殊增强(支持对象、数组语法),最终仍然通过 DOM 属性机制应用。
html
<!-- 根据 isActive 动态切换 active 类 -->
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
html
<div :class="[activeClass, errorClass]"></div>
html
<div :class="[{ active: isActive }, errorClass]"></div>
html
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
js
<div :style="styleObject"></div>
const styleObject = reactive({
color: 'red',
fontSize: '13px'
})
修饰符
1、.camel 将短横线命名的属性名转换为驼峰式
将 HTML 属性名中的短横线(kebab-case)转换为驼峰式(camelCase),以便绑定到使用驼峰命名的 DOM 属性。
2、.prop 将绑定绑定为 DOM 属性而非 HTML 属性
强制将绑定值设置为 DOM 属性(Property),而不是 HTML 属性(Attribute)。
3、.attr 将绑定强制绑定为 HTML 属性
强制将绑定值设置为 HTML 属性(Attribute),通过 setAttribute 设置。
十二、v-model
v-model 是 Vue.js 中用于实现双向数据绑定的核心指令。
v-model 的使用
【示例】在原生表单元素上:v-model 等价于 :value(或对应属性)加上 @input(或对应事件)事件监听。
js
<template>
<div>
<input type="text" v-model="roleName" placeholder="请输入角色名称" />
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const roleName = ref("");
</script>
在原生元素上:v-model="username" 等价于 :value="username" @input="username = $event.target.value"

【示例】多个v-model
js
<template>
<div>
<input type="text" v-model="roleName" placeholder="请输入角色名称" /><br />
<input type="text" v-model="roleID" placeholder="请输入角色ID" />
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const roleName = ref("");
const roleID = ref("");
</script>

【示例】组件上使用
js
<template>
<TabOne v-model:name="name" v-model:id="id" />
</template>
<script setup lang="ts">
import TabOne from "@/pages/cloud/components/tabOne.vue";
import { ref } from "vue";
const name = ref("CloudView");
const id = ref("ccd");
defineOptions({
name: "CloudView",
});
</script>

【示例】组件上使用
js
// 父组件
<template>
<TabOne v-model:tabName="name" v-model:tabId="id" />
</template>
<script setup lang="ts">
import TabOne from "@/pages/cloud/components/tabOne.vue";
import { ref } from "vue";
const name = ref("CloudView");
const id = ref("ccd");
defineOptions({
name: "CloudView",
});
</script>
js
// 子组件
<template>
<div>
<input v-model="tabName" />
<input v-model="tabId" />
</div>
</template>
<script setup lang="ts">
const tabName = defineModel("tabName");
const tabId = defineModel("tabId");
defineOptions({
name: "TabOneView",
});
</script>

原生元素修饰符
.lazy,改为 change 事件同步。默认情况下,v-model在input事件触发时同步数据(输入框每次按键都更新)。添加.lazy修饰符后,改为在change事件触发时同步(通常是失焦或回车时)。.number,自动转为数字。将用户的输入自动转换为数字类型。如果输入无法被parseFloat()转换,则返回原始字符串。.trim,自动去除首尾空格。自动过滤用户输入内容首尾的空白字符(空格、制表符、换行符等)。
【示例】
js
<template>
<div>
<input v-model.trim="tabName" />
<input v-model.number="tabId" />
</div>
</template>

组件修饰符
【示例】组件添加修饰符
js
// 父组件
<template>
<TabOne v-model:tabName.max="name" v-model:tabId.upper="id" />
</template>
<script setup lang="ts">
import TabOne from "@/pages/cloud/components/tabOne.vue";
import { ref } from "vue";
const name = ref("CloudView");
const id = ref("ccd");
defineOptions({
name: "CloudView",
});
</script>

js
// 子组件
<template>
<div>
<input v-model="tabName" />
<input v-model="tabId" />
</div>
</template>
<script setup lang="ts">
const [tabName, tabNameModifiers] = defineModel("tabName", {
set(val: string) {
console.log("tabName", tabNameModifiers, val);
if (tabNameModifiers.max) {
return val.slice(0, 10);
}
return val;
},
});
const [tabId, tabIdModifiers] = defineModel("tabId", {
set(val: string) {
return tabIdModifiers.upper ? val.toUpperCase() : val.toLowerCase();
},
});
defineOptions({
name: "TabOneView",
});
</script>

使用限制
- 必须有表达式。
- 不能绑定 props
- 不能是常量
编译阶段:模板转化
一、 解析(Parse)
parse 函数将模板代码解析成抽象语法树(AST) 。此时,v-model 指令还是一个特殊的节点。
二、转换(Transform)
transform 函数会识别出 v-model 节点,并调用 transformModel 函数,把节点转换成两条 props:
- 对于原生元素:
value和on:input。 - 对于自定义组件:
modelValue和on:update:modelValue。
三、生成(Generate)
generate 函数将转化后的 AST 生成最终的 render 函数。至此,v-model 指令已不复存在,AST 已被静态展开。
运行阶段:渲染与更新
- 执行
render函数:浏览器执行编译阶段生成的render函数,生成虚拟 DOM(VNode)。 - 处理
props:在生成 VNode 的过程中,render函数会识别modelValue和onUpdate:modelValue,并将其作为普通的props和event处理。 - 挂载与更新:Vue 的运行时系统会根据 VNode 创建或更新真实 DOM。当用户交互触发
update:modelValue事件时,父组件中绑定的数据就会被更新,从而触发新一轮的渲染。
十三、v-slot
Vue 3 中的 v-slot 指令用于定义插槽(slot),它是 Vue 组件化体系中实现内容分发和组件复用的核心机制。
插槽的使用?
插槽允许父组件向子组件传递模板内容 ,子组件通过 <slot> 元素定义内容的放置位置。v-slot 指令用于在父组件中声明传递给子组件的内容。
- 默认插槽:没有名字的插槽。
- 具名插槽:有名字的插槽,用于多内容分发。
- 作用域插槽:子组件可以将数据回传给父组件,父组件利用这些数据渲染内容。
默认插件
【示例】父组件直接嵌套内容
js
// 父组件
<template>
<TabTwo>
<p>插槽内容-默认</p>
</TabTwo>
</template>
<script setup lang="ts">
import TabTwo from "./cloud/components/tabTwo.vue";
defineOptions({
name: "CloudView",
});
</script>
编译结果
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createBlock($setup["TabTwo"], null, {
default: _withCtx(() => [..._cache[0] || (_cache[0] = [_createElementVNode(
"p",
null,
"插槽内容-默认",
-1
/* CACHED */
)])]),
_: 1
});
}
js
// 子组件
<template>
<div>
<slot></slot> <!-- 插槽出口 -->
</div>
</template>
<script setup lang="ts">
defineOptions({
name: "TabTwoView",
});
</script>

【示例】<template> 配合 v-slot:default 或简写 #default
v-slot:default
js
// 父组件
<template>
<TabTwo>
<template v-slot:default>
<p>插槽内容-默认</p>
</template>
</TabTwo>
</template>
简写方式
js
// 父组件
<template>
<TabTwo>
<template #default>
<p>插槽内容-默认</p>
</template>
</TabTwo>
</template>
js
<template>
<div>
<slot></slot>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: "TabTwoView",
});
</script>
具名插槽
子组件定义多个 <slot>,用 name 属性区分。
【示例】
js
// 父组件
<template>
<TabTwo>
<template v-slot:default>
<p>插槽内容-默认</p>
</template>
<template #header>
<p>这里是头部</p>
</template>
<template #footer>
<p>这里是脚部</p>
</template>
</TabTwo>
</template>
<script setup lang="ts">
import TabTwo from "./cloud/components/tabTwo.vue";
defineOptions({
name: "CloudView",
});
</script>

js
// 子组件
<template>
<div>
<slot name="header"></slot>
<slot></slot>
<slot name="footer"></slot>
</div>
</template>

【示例】条件插槽
js
// 子组件
<template>
<div>
<slot name="header" :header="info.header"></slot>
<slot :list="info.list"></slot>
<slot v-if="$slots.footer" name="footer" :footer="info.footer"></slot>
</div>
</template>
<script setup lang="ts">
import { reactive } from "vue";
const info = reactive({
header: "这里是头部",
footer: "这里是脚部",
list: ["item1", "item2", "item3"],
});
defineOptions({
name: "TabTwoView",
});
</script>
编译结果
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("div", null, [
_renderSlot(_ctx.$slots, "header", { header: $setup.info.header }),
_renderSlot(_ctx.$slots, "default", { list: $setup.info.list }),
_ctx.$slots.footer ? _renderSlot(_ctx.$slots, "footer", {
key: 0,
footer: $setup.info.footer
}) : _createCommentVNode("v-if", true)
]);
}
【示例】动态插槽名称
js
// 父组件
<template>
<button @click="handleClick">切换</button>
<TabTwo>
<template v-slot:[slotName]="{ data }"> {{ data }} </template>
</TabTwo>
</template>
<script setup lang="ts">
import TabTwo from "./cloud/components/tabTwo.vue";
import { ref } from "vue";
const slotName = ref("header");
const handleClick = () => {
const flag = Math.random() > 0.5;
slotName.value = flag ? "header" : "footer";
};
defineOptions({
name: "CloudView",
});
</script>
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock(
_Fragment,
null,
[_createElementVNode("button", { onClick: $setup.handleClick }, "切换"), _createVNode(
$setup["TabTwo"],
null,
{
[$setup.slotName]: _withCtx(({ data }) => [_createTextVNode(
_toDisplayString(data),
1
/* TEXT */
)]),
_: 2
},
1024
/* DYNAMIC_SLOTS */
)],
64
/* STABLE_FRAGMENT */
);
}
作用域插槽
子组件在 <slot> 上绑定属性(称为插槽 prop ),父组件通过 v-slot 接收这些数据,从而实现父组件模板使用子组件内部数据。
js
// 父组件
<template>
<TabTwo>
<template v-slot:default="{ list }">
<p>插槽内容-默认</p>
<ul v-for="item in list" :key="item">
<li>{{ item }}</li>
</ul>
</template>
<template #header="{ header }">
<h1>这里是头部</h1>
<p>{{ header }}</p>
</template>
<template #footer="{ footer }">
<h1>这里是脚部</h1>
<p>{{ footer }}</p>
</template>
</TabTwo>
</template>
编译结果
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createBlock($setup["TabTwo"], null, {
default: _withCtx(({ list }) => [_cache[0] || (_cache[0] = _createElementVNode(
"p",
null,
"插槽内容-默认",
-1
/* CACHED */
)), (_openBlock(true), _createElementBlock(
_Fragment,
null,
_renderList(list, (item) => {
return _openBlock(), _createElementBlock("ul", { key: item }, [_createElementVNode(
"li",
null,
_toDisplayString(item),
1
/* TEXT */
)]);
}),
128
/* KEYED_FRAGMENT */
))]),
header: _withCtx(({ header }) => [_cache[1] || (_cache[1] = _createElementVNode(
"h1",
null,
"这里是头部",
-1
/* CACHED */
)), _createElementVNode(
"p",
null,
_toDisplayString(header),
1
/* TEXT */
)]),
footer: _withCtx(({ footer }) => [_cache[2] || (_cache[2] = _createElementVNode(
"h1",
null,
"这里是脚部",
-1
/* CACHED */
)), _createElementVNode(
"p",
null,
_toDisplayString(footer),
1
/* TEXT */
)]),
_: 1
});
}
js
// 子组件
<template>
<div>
<slot name="header" :header="info.header"></slot>
<slot :list="info.list"></slot>
<slot name="footer" :footer="info.footer"></slot>
</div>
</template>
<script setup lang="ts">
import { reactive } from "vue";
const info = reactive({
header: "这里是头部",
footer: "这里是脚部",
list: ["item1", "item2", "item3"],
});
defineOptions({
name: "TabTwoView",
});
</script>

注意事项
- v-slot 只能用在 template 标签 或组件上。
- v-slot 用在组件上,只能是默认的情况。
- 如果同时使用默认插槽和具名插槽,默认插槽的内容必须用
<template #default>包裹(除非你只提供默认插槽且不与其他插槽混用)。
编译阶段
一、解析(Parse)
模板中的 <template v-slot:header="slotProps"> 会被解析成 AST 节点,其中包含:
slotName:插槽名(如header)slotProps:作用域变量名(如slotProps)- 子节点:插槽内部的模板内容
二、转换(Transform)
编译器会对 AST 进行转换,将 v-slot 转换为 render 函数中的插槽定义。
转换的结果是:每个插槽会被编译成一个函数,该函数接收子组件传递的插槽 prop 作为参数,并返回插槽内容的虚拟 DOM。
三、生成(Generate)
最终生成可执行的 render 函数。
对于子组件,其 render 函数中会访问 $slots 对象;
对于父组件,render 函数会生成一个插槽对象 作为子组件的第三个参数(即 children 或 slots)。
运行阶段
当父组件的 render 函数执行时,它会计算每个插槽的内容,并为每个插槽生成一个函数。这些函数被收集到一个对象中,作为子组件创建时的 slots 属性传递。
子组件在渲染时,会通过 $slots 属性访问父组件传递的插槽对象。