知

模块一 · 起步与基础

第 2 / 17 节18 分钟阅读型

模板与表单:指令、修饰符与 v-model 的完整用法

模板是 Vue 唯一的声明式入口。这一节把条件渲染、列表、class/style 绑定、事件修饰符与表单 v-model 讲成一张可查的表,并给出一套三层校验的表单做法。

读完这一节,你能做到

  1. 按场景选对 v-if、v-show、v-for,不再混用。
  2. 写出可读的 class/style 绑定,而不是拼字符串。
  3. 用事件修饰符替代手写 stopPropagation。
  4. 独立完成一个带校验的受控表单。

一、指令全景

指令作用关键点
v-text设置 textContent会覆盖子内容,很少直接用
v-html设置 innerHTML有 XSS 风险,只用于可信内容
v-show切换 display元素始终在 DOM 中,初次渲染成本高
v-if / v-else-if / v-else条件渲染真正销毁与重建节点,会触发组件卸载
v-for列表渲染必须给稳定 key
v-bind(:)绑定属性/Props支持对象语法批量绑定
v-on(@)绑定事件配合修饰符使用
v-model双向绑定语法糖本质是"值 + 输入事件"
v-once只渲染一次适合纯静态区块
v-memo记忆化子树列表项性能优化,需谨慎

二、条件渲染:v-if 还是 v-show

判断标准只有两条:切换频率与首次渲染成本。

场景选择原因
弹窗、抽屉、标签页v-show频繁切换,避免反复创建销毁
权限区块、首屏不展示的大组件v-if不该渲染的内容不要进 DOM
需要保留输入内容v-showv-if 会销毁组件,状态丢失
内含大量子组件v-if减少初始渲染与内存占用

v-if 是"惰性"的:初始为假时,分支内的组件不会创建,也不会执行 onMounted。v-show 只是改 display: none,组件生命周期照常执行。这一点在排查"接口为什么多请求一次"时很关键。

v-if 与 v-for 不要放在同一元素上。在 Vue 3 中 v-if 优先级更高,拿不到 v-for 的循环变量:

<!-- 错误:item 未定义 -->
<li v-for="item in list" v-if="item.active">{{ item.name }}</li>

<!-- 正确:先过滤,再渲染 -->
<li v-for="item in activeList" :key="item.id">{{ item.name }}</li>

三、列表渲染

<ul>
  <li v-for="(item, index) in list" :key="item.id">
    {{ index + 1 }}. {{ item.name }}
  </li>
</ul>

v-for 还能遍历对象与数字:

<span
  v-for="(value, key, i) in user"
  :key="key"
>{{ i }}-{{ key }}:{{ value }}</span>
<span v-for="n in 5" :key="n">{{ n }}</span>

key 的规则:

  • 用业务 id,不要用数组下标。
  • 只在同级兄弟间唯一,不需要全局唯一。
  • 列表项是组件时,key 写在组件标签上,不要写在组件内部的根元素上。

用下标做 key,在"插入到中间"时会让后续节点复用错位:输入框内容、展开状态、动画都会跑到别的行上。这个 bug 在开发时不容易发现,因为纯展示列表看起来完全正常。

四、class 与 style 绑定

<div :class="{ active: isActive, 'is-disabled': disabled }" />
<div :class="['btn', size === 'large' ? 'btn-lg' : '']" />
<div :class="[baseClass, { active: isActive }]" />
<div :style="{ color: textColor, fontSize: `${size}px` }" />

三条实践:

  1. 静态类写在 class 上,动态类写在 :class 上,Vue 会自动合并。
  2. 复杂的类名判断抽成 computed,模板里只留一个变量。
  3. :style 的数值单位要自己补,Vue 不会自动加 px(除非用 CSS 变量)。

五、事件与修饰符

<button @click="handleClick">保存</button>
<button @click="handleClick($event, row)">带参数</button>
<form @submit.prevent="onSubmit">
  <a @click.stop="open">不冒泡</a>
  <div @click.self="close">点空白才关闭</div>
  <input @keyup.enter="search" @keyup.esc="clear" />
</form>

常用修饰符:

修饰符等价于典型场景
.stopevent.stopPropagation()阻止冒泡到父级点击区域
.preventevent.preventDefault()表单提交、链接跳转
.self判断 event.target === event.currentTarget遮罩层点击关闭
.once只触发一次引导、埋点
.capture捕获阶段触发需要在子级之前拦截
.passive提升滚动性能@scroll.passive、@touchmove.passive

按键与系统键修饰符:.enter、.tab、.esc、.space、.up/.down/.left/.right、.ctrl、.alt、.shift、.meta、.exact。

一个常见错误:在 .passive 上同时写 .prevent,浏览器会忽略 preventDefault,并给出警告。滚动拦截要用非 passive 监听或直接改布局。

六、表单:v-model 的六种形态

v-model 是"绑定值 + 监听输入事件"的语法糖,不同类型的表单元素绑定的属性不同:

元素绑定属性绑定事件值类型
input[type=text] / textareavalueinputstring
input[type=checkbox](单个)checkedchangeboolean
input[type=checkbox](多选)checkedchangearray
input[type=radio]checkedchange选中的 value
select(单选/多选)valuechangestring / array
<input v-model="form.name" />
<textarea v-model="form.remark" />
<input type="checkbox" v-model="form.agree" />
<input type="checkbox" v-model="form.tags" value="vue" />
<input type="radio" v-model="form.level" value="P6" />
<select v-model="form.city">
  <option disabled value="">请选择</option>
  <option value="sh">上海</option>
</select>

三个修饰符:

修饰符作用注意
.lazy从 input 改为 change 同步输入过程中不更新
.number尝试转成数字无法转换时返回原字符串
.trim去掉首尾空格只处理首尾,不去中间
<input v-model.number="form.age" type="number" />
<input v-model.trim="form.email" />
<input v-model.lazy="form.keyword" />

七、一个完整表单案例

需求:注册表单,包含手机号、验证码、密码、同意协议;要求即时校验格式、提交前统一校验、服务端错误回填。

<script setup lang="ts">
import { reactive, computed, ref } from "vue";

const form = reactive({ phone: "", code: "", password: "", agree: false });
const errors = reactive<Record<string, string>>({});
const submitting = ref(false);

const phoneValid = computed(() => /^1[3-9]\d{9}$/.test(form.phone));
const canSubmit = computed(
  () =>
    phoneValid.value &&
    form.code.length === 6 &&
    form.password.length >= 8 &&
    form.agree,
);

function validate() {
  errors.phone = phoneValid.value ? "" : "手机号格式不正确";
  errors.code = form.code.length === 6 ? "" : "验证码为 6 位";
  errors.password = form.password.length >= 8 ? "" : "密码至少 8 位";
  errors.agree = form.agree ? "" : "请先同意协议";
  return !Object.values(errors).some(Boolean);
}

async function onSubmit() {
  if (!validate()) return;
  submitting.value = true;
  try {
    await register(form);
  } catch (e: any) {
    errors[toField(e.code)] = e.message; // 服务端错误映射到字段
  } finally {
    submitting.value = false;
  }
}
</script>

这个例子体现了三层校验:

  1. 即时校验:computed 判断格式,错误提示在字段旁,不弹窗。
  2. 提交校验:validate() 统一跑一遍,避免遗漏与绕过。
  3. 服务端校验:接口错误按字段码回填,不能只依赖前端。

三个易被忽略的细节:

  • 提交中禁用按钮,防止重复提交;submitting 同时用于 loading 态。
  • 验证码按钮要倒计时并在组件卸载时清理定时器。
  • 表单数据用 reactive 时不要整体替换对象,否则丢失响应式;用 Object.assign(form, next)。

八、常见坑

  1. v-if 与 v-for 同元素,循环变量取不到。
  2. key 用下标,插入后输入框内容错位。
  3. 复选框多选忘了给每个选项写 value,结果绑成 true/false。
  4. select 没有空值占位,默认选中第一项,用户以为已选择。
  5. v-model.number 在空字符串时得到 "" 而不是 0,后端校验失败。
  6. 表单整体替换(form = {...})后页面不更新,因为 reactive 的引用被换掉。
  7. v-html 渲染用户输入,造成 XSS。

九、动手任务:自写 → AI 写 → 对比 → 取舍

四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。

① 自己写

  1. 写一个搜索栏:输入框 v-model.trim,回车触发搜索,带清空按钮。
  2. 写一个多选标签组件,用 v-model 绑定数组,至少选一项才能提交。
  3. 把上面的注册表单补全错误提示、提交中禁用与验证码倒计时。
  4. 用 v-show 与 v-if 各实现一次弹窗,用 DevTools 观察 DOM 与组件生命周期的差异。

② 让 AI 写同一题

把第 2 题(表单校验)交给 AI,补一句 用 v-model 修饰符,不要手写 change 事件。

③ 对比要盯的点

  • .number / .trim 它用了吗?手写 Number(e.target.value) 会把空字符串变成 0。
  • 它有没有把 v-model 直接绑在 prop 上?prop 只读,必须 emit 上去。
  • 校验时机它是 blur 还是每次输入?后者用户刚敲第一个字就满屏红字。

④ 一句话取舍

哪个差异会让用户在还没输完时就被判错?

十、自测

  1. v-if 与 v-show 的选择标准是什么?
  2. 为什么 v-if 不能和 v-for 同元素?
  3. v-model 在复选框上的值类型有哪两种?
  4. .lazy、.number、.trim 分别改变什么?
  5. 表单三层校验各解决什么问题?

参考答案要点:切换频率与首渲染成本;优先级导致循环变量不可用;单个为布尔、多选为数组;同步时机、类型转换、去空格;即时体验、防遗漏、防绕过。

附:表单可访问性检查清单

表单能不能用,不只取决于逻辑对不对。下面九条是上线前可以逐项打勾的最低要求:

检查项做法不做的后果
每个输入框有标签<label for> 或 aria-label读屏只念“编辑框”
必填有语义required 或 aria-required用户提交后才知道必填
错误与字段关联aria-describedby 指向错误节点读屏读不到错误原因
错误状态可感知aria-invalid="true"仅靠红色边框,色弱用户无感
错误提示可被朗读容器加 role="alert"提交后没有反馈
按钮类型明确<button type="button">在表单里误提交
焦点可见不要 outline: none 而不给替代样式键盘用户不知道焦点在哪
错误后聚焦提交失败后把焦点移到第一个错误字段用户要自己找
禁用与只读区分不可改但需提交用 readonly用 disabled 会导致值不提交

其中“错误后聚焦”最容易被忽略。表单较长时,用户点完提交只看到页面没反应,实际上错误在屏幕外。把焦点移到第一个错误字段,并配合 scrollIntoView,体验差别很大。

另一个细节是 disabled 与 readonly 的选择:disabled 的字段不会随表单提交,也不可聚焦;readonly 可聚焦、可复制、会提交。展示“不可修改但需要提交”的信息(如订单号)时,应该用 readonly。

配套题目

★ 越多越该先背;点题目可看完整答案与深入版。

提示:读完可以顺手做几道对应题目,检验是否真的理解