Skip to content动态绑定:
事件系统:
条件渲染:
列表渲染:
侦听器:
Vue.js 模板语法与指令详解
概述
Vue 的模板部分是一段合法的 HTML,在编译阶段会被转换成渲染函数。这一步转换使得开发者可以用声明式的方式描述 UI 与数据的关系,而无需手动拼接 DOM 字符串或在状态变更时逐个查找节点进行更新。背后是响应式系统负责将数据变化映射为最小量的 DOM 操作[1]。
模板里最常用的操作包括:将数据显示为文本(插值)、将数据绑定到属性或样式(v-bind)、响应用户交互(v-on)、按条件或循环生成节点(v-if / v-for)。为了将复杂的派生状态从模板中抽离,Vue 提供了计算属性和侦听器。下面围绕这些具体能力展开。
插值与表达式边界
模板使用双大括号 {{ }} 包裹一个 JavaScript 表达式,Vue 会计算出该表达式的值并替换为文本内容。双大括号里只能放置单个表达式,不能写语句或控制流。下面的写法会直接导致编译错误:
html
<!-- 这是语句,不允许 -->
{{ var a = 1 }}
<!-- 条件控制也不允许 -->
{{ if (ok) { return message } }}如果需要条件判断,必须使用三元表达式或短路逻辑:
html
<p>{{ ok ? message : '默认文本' }}</p>模板中可以调用组件暴露的方法,如 {{ formatDate(date) }}。但要注意:绑定在模板里的方法在每次组件更新时都会被重新调用[5]。如果方法里有副作用——比如修改数据、发起异步请求——就可能引发额外的更新,甚至在极端情况下造成无限循环。因此,放进模板的方法最好只做纯计算,不要改变组件的状态。
模板里可以访问的全局对象是受限的。Vue 对表达式做沙盒化处理,只暴露一份白名单,包含 Math、Date、Array 等标准内置对象[6]。未列入白名单的 localStorage、console 或用户自行挂载到 window 上的属性,直接在模板里访问会抛出 ReferenceError,而不是静默返回 undefined。如果确实需要让所有模板都能使用某些全局属性,应当显式通过 app.config.globalProperties 挂载。
动态绑定:v-bind 的完整用法
双大括号只能处理文本内容,无法在属性值中使用。要让 id、class、style 等属性响应式地跟随数据变化,需要使用 v-bind。基本形式:
html
<div v-bind:id="dynamicId"></div>简写形式为 :id="dynamicId"。当绑定的值是 null 或 undefined 时,该属性会从渲染的 DOM 元素上完全移除[4]。这一行为在需要根据条件增减属性时很有用:
html
<button :disabled="isButtonDisabled">提交</button>对于布尔属性,isButtonDisabled 为真值(包括空字符串)时,元素上会保留 disabled 属性;为假值时属性被移除。这个行为和原生 HTML 的布尔属性表现一致,但完全由数据驱动。
从 Vue 3.4 开始支持同名简写::id 等价于 :id="id",前提是组件内确实存在名为 id 的数据。这种写法在数据名称和属性名相同时可以节省代码量。
动态参数:指令的参数本身也可以是一个动态表达式,用方括号括起来。例如:
html
<a v-bind:[attributeName]="url"> ... </a>如果 attributeName 的值为 "href",就等价于 v-bind:href="url"。事件名同样可以动态化:v-on:[eventName]="handler"。动态参数的值必须是一个字符串,null 代表移除该绑定,其他类型会触发警告。特别限制:动态参数表达式中不能包含空格或引号,否则浏览器会把 v-bind:[some 和 attr] 误认为两个不同的属性。如果需要复杂参数,可以通过计算属性绕开这一限制。
绑定多个属性:可以把一个对象直接传给 v-bind(不带参数),该对象的所有属性会作为 attribute 绑定到元素上:
html
<div v-bind="{ id: 'dynamicId', title: '提示文本' }"></div>在组合式 API 中,经常通过计算属性返回一个对象,然后用 v-bind="a11yAttrs" 的方式批量绑定 ARIA 属性。
事件系统:v-on 与修饰符
v-on(简写 @)用于监听 DOM 事件并执行相应的处理函数。处理函数可以是一个方法名,也可以是一个内联语句:
html
<button @click="handleClick">点击</button>
<button @click="count++">增加</button>简短的操作可以直接写成内联语句;稍微复杂一点的逻辑应当放到方法里。
修饰符是 v-on 的扩展机制,用来把一些常见的 DOM 细节抽象掉,避免在处理函数里重复编写 event.preventDefault() 之类代码。
常用的事件修饰符:
.stop— 调用event.stopPropagation().prevent— 调用event.preventDefault().once— 监听器只触发一次,之后自动移除.capture— 在捕获阶段触发监听器.self— 仅在event.target为元素自身时触发,不包含子元素.passive— 提升滚动性能,不能与.prevent混用
修饰符可以链式调用,如同时阻止默认行为并停止冒泡:
html
<form @submit.prevent.stop="onSubmit">...</form>按键修饰符:监听键盘事件时,可以限定特定的按键。例如 @keyup.enter="submit" 就只在按下 Enter 时触发。Vue 为常用按键提供了别名:.enter、.tab、.delete、.esc、.space、.up、.down、.left、.right。系统修饰符 .ctrl、.alt、.shift、.meta 同样可用,并支持与按键组合:@keyup.ctrl.enter。
.exact 修饰符要求精确匹配修饰键组合。如果指定了 .ctrl.exact,那么只在按住 Ctrl 并且没有按住其他系统键时才会触发。
鼠标事件也有对应的修饰符:.left、.right、.middle。
修饰符链的解析按照从左到右的顺序依次生效,顺序会影响实际的行为效果。
条件渲染:v-if 与 v-show
v-if 会依据绑定值的真假决定是否渲染元素。当值为假时,元素及其子节点根本不会出现在 DOM 树中。v-else-if 和 v-else 提供了多分支链,但它们必须紧跟在带有 v-if 或 v-else-if 的元素之后,中间不能插入其他元素,否则这个结构链就会断掉。
html
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else>C</div>如果需要在一组元素上复用同一个条件,可以用 <template> 包裹,<template> 自身不会生成任何标签:
html
<template v-if="ok">
<h1>标题</h1>
<p>内容</p>
</template>与 v-if 不同,v-show 总是会渲染元素,只是通过切换 CSS 的 display 属性来控制显示与否。因此 v-show 的初始渲染成本是固定的,不会像 v-if 那样在条件为假时销毁子组件并移除所有监听器。如果元素切换非常频繁,使用 v-show 可以减少销毁重建的开销;如果条件的变更很罕见,用 v-if 可以省下初始渲染时不必要的 DOM 创建。
另一个经常被提到的点是:v-if 的优先级高于 v-for,当它们同时出现在同一个元素上时,v-if 会先被评估。这通常意味着你在对每个循环项做一次条件判断,官方建议不要这样写。需要过滤列表时,最好先用计算属性将数据过滤一遍,然后用 v-for 遍历过滤后的结果,而不是把条件判断逻辑散落在模板里。
列表渲染:v-for 与 key 的机制
v-for 可以遍历数组或对象。遍历数组的语法是 item in items,也可以带上索引:
html
<li v-for="(item, index) in items" :key="item.id">
{{ item.text }}
</li>遍历对象时,形式为 (value, key, index) in object,注意 value 在前,这与 Object.keys() 的顺序习惯不同,容易混淆。
key 的特殊性:Vue 默认对 v-for 渲染的列表采用“就地复用”策略。当列表顺序变化时,它会尽量复用已有的 DOM 节点,只更新其中的内容。如果列表项内部含有独立的状态(例如输入框里的已填内容、未提交的表单),这种复用就会造成数据串位。key 属性告诉 Vue 以唯一标识来追踪节点的身份,从而在顺序变动时正确地移动、销毁和创建节点,而不是简单地就地复用。
key 应该是一个原始值(字符串或数字),并且在整个列表范围内保持唯一。通常使用数据中的 id 字段。避免用索引 index 作为 key,因为当列表项的顺序或数量发生变化时,同一个索引可能指向了新的数据项,导致节点身份混乱,进而破坏子组件的状态或表单输入的绑定。对于不含交互和状态的简单静态列表,用索引不会立即出错,但一旦加入交互就很容易出问题;从一开始就绑定稳定的 id 可以避免这一类隐患。
更新列表时,Vue 对部分数组变更方法(push、pop、shift、unshift、splice、sort、reverse)进行了包装,调用它们会触发视图更新。而直接通过索引设置元素(items[index] = newValue)或修改 length 则不会被拦截,此时需要使用 splice 或整体替换数组来触发响应式更新。
计算属性:缓存与依赖追踪
模板里适合放置简单的表达式,但当某个值需要从多个数据源派生,并且在模板中多次使用时,直接内联会导致臃肿和重复计算。计算属性正是用来处理这种派生状态的。
在组合式 API 中,计算属性通过 computed() 创建:
js
const fullName = computed(() => firstName.value + ' ' + lastName.value);计算属性和方法的区别在于缓存。计算属性会追踪它所读取的响应式依赖,仅当依赖发生变化时才会重新求值。多次访问同一个计算属性,只要依赖没变,就会直接返回上一次计算的结果。而调用方法每次都会执行函数,无论依赖是否变化。在大规模重绘时,如果模板中多次使用了同一个派生值,利用计算属性的缓存可以避免重复计算。
一个常见的误用是在计算属性里添加副作用,比如在其中修改其他状态(this.count++)或发起请求。计算属性的职责是算出一个值,副作用会干扰依赖追踪,可能造成不可预期的连锁更新。Vue 内部在重新计算时可能会多次调用 getter,副作用就会被重复触发。
计算属性也可以提供 setter,不过大多数场景只用到只读版。
侦听器:watch 的选项与适用场景
计算属性负责派生数据,而侦听器负责在数据变化时执行副作用,例如获取数据、操作 DOM、记录日志、同步外部状态等。
选项式 API 中通过 watch 配置来监听数据源,组合式 API 中使用 watch() 或 watchEffect()。判断使用计算属性还是侦听器的简单规则:如果需要得到一个值并将其传递或渲染,用计算属性;如果不需要值,而是要“做一件事”,用侦听器。
watch 的几个常用选项:
immediate: true— 创建监听器后立即以当前值执行一次回调。适用于组件初始化时需要根据某个 prop 获取数据等场景。deep: true— 深层监听。默认watch只监听顶层属性的变化,嵌套对象内部的修改不会触发回调。开启deep后,Vue 会递归遍历对象的所有层级,把它们都作为依赖追踪。对于大型对象,这样的遍历开销不小,需要权衡。另一种更经济的做法是直接监听具体的子属性路径,例如watch(() => obj.some.nested.key, ...)。
watchEffect 会自动跟踪回调里访问到的响应式依赖,并在依赖变化时重新执行,不需要显式声明监听什么。它比 watch 更适合不需要对比新老值的场景,比如简单地将某个状态同步到 localStorage。
侦听器回调的 flush 时机也值得注意:默认在下一次 DOM 更新之前(pre)触发。可以改为 post 使其在 DOM 更新后执行,便于操作更新后的 DOM;或是 sync 让响应式依赖变更时同步触发(需要谨慎使用,容易引发多余更新)。
注意点
- 模板沙盒:模板中不能直接访问未在白名单内的全局对象(如
localStorage、console),访问会抛出ReferenceError。需要时必须通过app.config.globalProperties来挂载。 - 方法中的副作用:模板里调用的方法和计算属性的 getter 都可能在每次重新渲染时执行。应避免在这些函数中修改状态或发起异步操作,否则容易产生额外的更新或循环。
v-html安全:v-html会把绑定的字符串作为 HTML 直接植入元素[3]。仅当内容完全可信时才可以使用,永远不要用于展示用户提供的未处理内容,否则会导致 XSS 攻击。v-if与v-for同元素:它们的优先级关系会使得对每个循环项都执行一次条件判断。应该先通过计算属性过滤数据,再对过滤后的列表使用v-for。key的索引陷阱:使用index作为key时,列表变动会导致节点身份错乱,尤其在包含子组件或表单输入时会直接体现为数据错位。应使用稳定的唯一 id。deep监听的性能:开启deep: true会对巨型对象进行全量递归遍历,造成显著的依赖收集开销。更好的做法是只监听所需的具体路径,而不开启深层标志。- 计算属性与侦听器的界限:当看到数据变化后“需要更新另一个数据”时,往往适合用计算属性直接派生,而不是通过
watch去修改另一个数据。
