写业务表单的时候,最容易被 v-model 的「省事」骗过去。一个 v-model="age" 敲上去,输入框和数据就通了,看起来什么都不用管。可等到提交接口的时候你会发现,年龄字段传过去是字符串 "18" 而不是数字 18;多选框绑了个空字符串,选了三项之后 data 里只剩最后一个值;下拉框第一次进页面显示的是空白,用户还以为页面挂了。这些都不是玄学,是 v-model 在不同控件上展开成了不同的东西。这篇把六类常用表单控件的绑定写法逐个过一遍,顺带把每种写法背后到底绑了什么属性、监听了什么事件讲清楚。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
v-model在不同表单控件上分别展开成什么- 文本框、数字框、多行文本框的绑定,以及
.number为什么必须加 - 复选框的三种形态,单个布尔值、自定义真假值、多选数组
- 单选框和下拉框的绑定,动态选项与多选列表
.lazy/.number/.trim三个修饰符各自解决什么问题- Vue 3 里
v-model的语义变化,以及老代码迁移时要动哪里
# 一、v-model 不是魔法,是语法糖
先说结论,v-model 只是「绑一个属性 + 听一个事件」的缩写。
拿最普通的文本框来说,写 v-model="textBox" 和下面这段是等价的:
<input :value="textBox" @input="textBox = $event.target.value">
Vue 帮你做的事情就两件,把数据的值同步到 DOM 上,再把用户输入回写到数据里。所以「双向绑定」这个词容易让人以为有什么特殊通道,其实只是把单向绑定正反各接了一遍。
关键在于,不同的控件绑的属性和听的事件不一样:
| 控件 | 绑定的属性 | 监听的事件 |
|---|---|---|
input[type=text] / textarea |
value |
input |
input[type=checkbox] |
checked |
change |
input[type=radio] |
checked |
change |
select |
value |
change |
搞清楚这张表,后面遇到的绝大多数「为什么绑不上」都能自己推出来。比如复选框听的是 change 不是 input,所以你想在每次勾选时做点别的事,写 @input 是不会触发的。
还有一条容易被忽略的规则,v-model 会忽略所有表单元素上 value、checked、selected 的初始值,一律以 Vue 实例 data 里的值为准。也就是说你在 HTML 里写死的 value="张三",页面渲染出来是空的,因为 data 里是空字符串。想给初值就去 data 里给。
# 二、文本框
# 2.1 普通文本框
最基础的一种,输入什么 data 里就是什么:
<div id="app-1">
<p><input v-model="textBox" placeholder="输入内容...">输入的内容:{{ textBox }}</p>
</div>
<script type="text/javascript">
var vm1 = new Vue({
el: '#app-1',
data: {
textBox: ''
}
})
</script>
这段代码有个细节值得留意,data 里的 textBox 初始化成了空字符串而不是 null 或者干脆不写。Vue 2 的响应式基于 Object.defineProperty,只有在实例创建时就存在于 data 里的属性才会被转成响应式的。后加的属性不会自动响应,得走 Vue.set。所以哪怕暂时没值,也要在 data 里先占个位。
# 2.2 数字文本框
表单里最常见的类型不匹配问题就出在这儿:
<div id="app-1">
<p><input v-model.number="numberTextBox" type="number" placeholder="输入内容..."> 输入的内容:{{ numberTextBox }}</p>
</div>
<script type="text/javascript">
var vm1 = new Vue({
el: '#app-1',
data: {
numberTextBox: ''
}
})
</script>
.number 修饰符会把返回值转成 Number 类型。为什么非要加它?因为就算你写了 type="number",HTML 规范里 input.value 的类型永远是字符串,浏览器不会替你转。不加 .number,你拿到的是 "18",做加法就变成了字符串拼接,age + 1 得到 "181"。
这个我踩过。当时后端接口校验类型,年龄字段传字符串直接 400,翻了半天以为是接口文档不对,最后发现是少写了一个修饰符。
.number 的实现是调用 parseFloat,如果转不出数字(比如用户输了一堆空格或者字母),它会原样返回字符串。所以 .number 只能保证「能转就转」,不能保证「一定是数字」,真要保证还得配校验。
# 2.3 多行输入框
textarea 用法和文本框一样,注意展示的时候要保留换行:
<div id="app-1">
<p><textarea v-model="multiTextBox" placeholder="输入内容..."></textarea></p>
<p>输入的内容:</p>
<p style="white-space:pre">{{ multiTextBox }}</p>
</div>
<script type="text/javascript">
var vm1 = new Vue({
el: '#app-1',
data: {
multiTextBox: ''
}
})
</script>
style="white-space:pre" 表示空白会被浏览器保留,行为类似 HTML 里的 <pre> 标签。不加这一句,用户在文本域里敲的回车在展示区会被折叠成一个空格,看起来像丢了数据。实际项目里更常用 pre-wrap,它既保留换行又允许长行自动折行,纯 pre 遇到长文本会撑出横向滚动条。
另外提一句,textarea 里千万别写插值,像 <textarea>{{ text }}</textarea> 这种在 Vue 里是无效的,官方明确要求用 v-model 代替。
# 三、复选框
复选框是三类控件里变化最多的,因为它有三种完全不同的用法。
# 3.1 单个复选框
一个复选框绑一个布尔值,勾上是 true,取消是 false:
<div id="app-1">
<input type="checkbox" id="checkbox" v-model="singleCheckBox">
<label for="checkbox">{{ singleCheckBox }}</label>
</div>
<script type="text/javascript">
var vm1 = new Vue({
el: '#app-1',
data: {
singleCheckBox: false
}
})
</script>
这种适合「同意用户协议」「记住我」这类开关型场景。
# 3.2 自定义真假值的复选框
问题来了,后端接口经常不收布尔值,它要的是 "Y" / "N",或者 1 / 0。这时候不用在提交前手动转,true-value 和 false-value 就是干这个的:
<div id="app-1">
<input type="checkbox" id="checkbox" v-model="customSingleCheckBox" v-bind:true-value="customTrue" v-bind:false-value="customFalse">
<label for="checkbox">{{ customSingleCheckBox }}</label>
</div>
<script type="text/javascript">
var vm1 = new Vue({
el: '#app-1',
data: {
customTrue: '真',
customFalse: '假',
customSingleCheckBox: '假'
}
})
</script>
v-bind:true-value 设置为真时的属性,v-bind:false-value 设置为假时的属性。
这里有个坑要注意。true-value 和 false-value 不是标准 HTML 属性,是 Vue 自己加的,只在 v-model 场景下生效。而且如果你写成不带 v-bind 的 true-value="Y",传的是字符串字面量 "Y";写成 :true-value="1" 才是数字 1。两者在后续做 === 比较时结果完全不同。
还有一点,用了自定义值之后,未勾选状态下 data 里存的是 false-value 而不是 false。要是你别的地方写了 if (form.agree) 这种真值判断,'假' 这个字符串是真值,判断就翻车了。
# 3.3 多个复选框绑数组
多选场景下,把多个复选框绑到同一个数组上,每个框的 value 就是它选中后进数组的值:
<div id="app-1">
<input type="checkbox" id="tansea" value="TanSea" v-model="multiCheckBox">
<label for="tansea">TanSea</label>
<input type="checkbox" id="google" value="Google" v-model="multiCheckBox">
<label for="google">Google</label>
<input type="checkbox" id="baidu" value="Baidu" v-model="multiCheckBox">
<label for="baidu">Baidu</label>
<p>选择的项:{{ multiCheckBox }}</p>
</div>
<script type="text/javascript">
var vm1 = new Vue({
el: '#app-1',
data: {
multiCheckBox: []
}
})
</script>
multiCheckBox 必须初始化成数组 []。这是文章开头提到的那个坑,初始化成 '' 或者 false 的话,Vue 会按单个复选框的逻辑处理,三个框会一起联动,选一个全选中,data 里永远只有一个布尔值。
数组里元素的顺序是按用户勾选顺序进去的,不是按 DOM 顺序。如果你要提交给后端的顺序有要求,提交前自己排一次。
# 四、单选框
单选框靠 name 分组是原生 HTML 的做法,在 Vue 里靠的是绑同一个 v-model: