一、表单元素的v-model绑定(核心场景)
v-model
是Vue实现“表单元素与数据双向同步”的语法糖,不同表单元素的绑定规则存在差异,需根据元素类型选择正确的绑定方式。
1.1 四大表单元素的绑定规则对比
表单元素类型 | 绑定数据类型 | 核心规则 | 初始值设置 | 关键注意事项 |
---|---|---|---|---|
单选框(radio) | 字符串/数字 | 绑定值与单选框的value 属性一致时,该选项选中 | 设为目标value (如'male' ) | 必须为每个单选框显式设置value 属性 |
单个多选框(checkbox) | 布尔值 | 选中时数据为true ,未选中为false ,无需设置value | true (默认选中)/false | 常用于“同意协议”“记住密码”等场景 |
多个多选框(checkbox) | 数组 | 选中项的value 自动加入数组,取消选中时自动移除 | 空数组[] (默认无选中) | 所有多选框需绑定同一数组,且必须设置value |
下拉框(select) | 字符串/数组 | - 单选下拉框:绑定选中项的value ; - 多选下拉框(加multiple ):绑定选中项value 组成的数组 | 单选:目标value ;多选:[] | 若未设value ,默认用<option> 的文本作为value |
1.2 详细用法与示例
1. 单选框(radio):性别选择
<div id="app"><label><input type="radio" name="gender" value="male" v-model="selectedGender"> 男</label><label><input type="radio" name="gender" value="female" v-model="selectedGender"> 女</label><p>选中性别:{{ selectedGender }}</p>
</div>
<script>
new Vue({el: "#app",data: {selectedGender: "male" // 初始选中“男”}
});
</script>
- 关键:
name
属性确保单选框互斥(同一组只能选一个),v-model
绑定值与value
匹配即选中。
2. 单个多选框(checkbox):协议同意
<div id="app"><label><input type="checkbox" v-model="isAgree"> 我已阅读并同意用户协议</label><button :disabled="!isAgree" style="margin-left: 8px;">提交</button><p>同意状态:{{ isAgree }}</p>
</div>
<script>
new Vue({el: "#app",data: {isAgree: false // 初始未选中,按钮禁用}
});
</script>
- 关键:无需设置
value
,v-model
直接绑定布尔值,常用于控制按钮禁用状态。
3. 多个多选框(checkbox):爱好选择
<div id="app"><p>选择爱好:</p><label><input type="checkbox" value="game" v-model="selectedHobbies"> 游戏</label><label><input type="checkbox" value="reading" v-model="selectedHobbies"> 阅读</label><label><input type="checkbox" value="sports" v-model="selectedHobbies"> 运动</label><p>选中爱好:{{ selectedHobbies }}</p>
</div>
<script>
new Vue({el: "#app",data: {selectedHobbies: ["reading"] // 初始选中“阅读”}
});
</script>
- 关键:所有多选框绑定同一数组,
value
必须唯一,数组自动维护选中状态。
4. 下拉框(select):省份选择
<div id="app"><!-- 单选下拉框 --><select v-model="selectedProvince"><option value="">请选择省份</option> <!-- 空value对应初始未选 --><option value="beijing">北京</option><option value="shanghai">上海</option><option>广东</option> <!-- 未设value,默认用文本“广东”作为value --></select><!-- 多选下拉框(按住Ctrl选择) --><select v-model="selectedProvinces" multiple style="margin-top: 8px;"><option value="beijing">北京</option><option value="shanghai">上海</option></select><p>单选省份:{{ selectedProvince }}</p><p>多选省份:{{ selectedProvinces }}</p>
</div>
<script>
new Vue({el: "#app",data: {selectedProvince: "shanghai", // 初始选中“上海”selectedProvinces: ["beijing"] // 初始选中“北京”}
});
</script>
- 关键:多选下拉框需加
multiple
属性,绑定数组;未设value
时,v-model
值为<option>
的文本。
二、v-model修饰符(简化表单处理)
表单输入默认是字符串类型,且可能包含无效空格,Vue提供3个常用修饰符,无需手动写JS逻辑即可优化输入体验。
修饰符 | 核心作用 | 适用场景 | 示例代码与效果 |
---|---|---|---|
.trim | 自动去除输入值的首尾空格(中间空格保留) | 用户名、手机号、邮箱等需过滤无效空格的场景 | <input v-model.trim="username"> 输入“ 小明 ”→ 变量值“小明” |
.number | 自动将输入值转为数字类型(非数字保留字符串) | 年龄、金额、数量等需数值运算的场景 | <input v-model.number="age" type="number"> 输入“18”→ 变量值18 (数字) |
.lazy | 从“input事件”触发同步改为“change事件”触发(失去焦点/回车时同步) | 输入内容无需实时同步的场景(如长文本输入) | <input v-model.lazy="desc"> 输入时不实时同步,失去焦点后更新 |
示例:带修饰符的注册表单
<div id="app"><input v-model.trim="username" placeholder="用户名(去首尾空格)"><br><input v-model.number="age" type="number" placeholder="年龄(转数字)"><br><textarea v-model.lazy="desc" placeholder="描述(失焦同步)"></textarea><br><p>用户名:{{ username }}(类型:{{ typeof username }})</p><p>年龄:{{ age }}(类型:{{ typeof age }})</p><p>描述:{{ desc }}</p>
</div>
<script>
new Vue({el: "#app",data: {username: "",age: "", // 初始为空,输入后转为数字desc: ""}
});
</script>
三、核心:计算属性(computed)
计算属性是Vue中“处理派生数据”的核心特性,本质是依赖其他数据动态计算出的属性,具有缓存机制,可避免重复计算,保持模板简洁。
3.1 为什么需要计算属性?(解决的痛点)
直接在模板中写复杂JS逻辑会导致模板臃肿、难维护,例如:
<!-- 模板中直接写复杂运算:臃肿且无缓存 -->
<p>{{ msg[0].toUpperCase() + msg.substring(1) }}</p> <!-- 首字母大写 -->
<p>{{ gender === 1 ? "男" : gender === 2 ? "女" : "未知" }}</p> <!-- 性别转换 -->
计算属性可将这些逻辑移到JS层,模板仅需引用属性名,实现“关注点分离”:
<!-- 模板简洁清晰 -->
<p>{{ formattedMsg }}</p>
<p>{{ formattedGender }}</p>
<script>
new Vue({computed: {// 计算属性:首字母大写formattedMsg() {return this.msg[0].toUpperCase() + this.msg.substring(1);},// 计算属性:性别转换formattedGender() {return this.gender === 1 ? "男" : this.gender === 2 ? "女" : "未知";}}
});
</script>
3.2 计算属性的核心机制:缓存与依赖追踪
1. 缓存机制(性能关键)
- 原理:计算属性会缓存计算结果,只有当它依赖的data属性变化时,才会重新执行计算函数;若依赖未变,多次调用会直接返回缓存值。
- 对比methods:methods每次调用都会重新执行函数,无缓存,性能较差。
示例:缓存机制验证
<div id="app"><p>计算属性结果:{{ computedResult }}</p><p>计算属性结果:{{ computedResult }}</p> <!-- 复用缓存,不重新计算 --><p>方法结果:{{ methodResult() }}</p><p>方法结果:{{ methodResult() }}</p> <!-- 重新执行,打印2次“方法执行” --><button @click="msg = 'new message'">修改依赖</button>
</div>
<script>
new Vue({el: "#app",data: { msg: "hello" },computed: {computedResult() {console.log("计算属性执行"); // 仅依赖变化时打印1次return this.msg.toUpperCase();}},methods: {methodResult() {console.log("方法执行"); // 每次调用都打印,共2次return this.msg.toUpperCase();}}
});
</script>
- 输出结果:初始时打印“计算属性执行”1次、“方法执行”2次;点击按钮修改
msg
后,打印“计算属性执行”1次、“方法执行”2次。
2. 依赖追踪
Vue会自动追踪计算属性依赖的所有data
属性(如formattedMsg
依赖msg
),当任一依赖变化时,计算属性会“自动重新计算”并更新视图。
3.3 计算属性 vs methods vs watch(核心区别)
三者都能处理数据逻辑,但适用场景不同,需精准选择:
特性 | 计算属性(computed) | 方法(methods) | 侦听器(watch) |
---|---|---|---|
核心作用 | 处理派生数据(如格式化、过滤、计算) | 处理事件逻辑、异步操作、无返回值的操作 | 监听单个数据变化,执行异步/开销大的操作 |
缓存机制 | 有(依赖不变时复用结果) | 无(每次调用重新执行) | 无(数据变化时执行) |
调用方式 | 模板中直接用属性名(不加括号) | 需加括号调用(如method() ) | 自动执行(数据变化时触发) |
返回值 | 必须有返回值(否则无意义) | 可无返回值(如仅执行事件) | 无需返回值(仅执行副作用) |
适用场景 | 1. 数据格式化(如日期、金额); 2. 数组过滤排序; 3. 多数据组合计算 | 1. 事件处理(如点击、输入); 2. 同步逻辑执行 | 1. 异步操作(如数据变化后请求接口); 2. 数据变化时执行复杂逻辑 |
示例:watch的适用场景(异步操作)
<div id="app"><input v-model="username" placeholder="输入用户名"><p>用户名合法性:{{ usernameValid }}</p>
</div>
<script>
new Vue({el: "#app",data: {username: "",usernameValid: true},watch: {// 监听username变化,执行异步验证(计算属性无法处理异步)username(newVal) {// 模拟后端接口验证(异步)setTimeout(() => {this.usernameValid = newVal.length >= 3; // 用户名≥3位为合法}, 500);}}
});
</script>
3.4 计算属性的进阶用法
1. 计算属性的setter(修改计算属性)
默认情况下,计算属性是“只读”的(仅实现getter
),若需“修改计算属性”,需显式定义setter
:
<div id="app"><p>全名:{{ fullName }}</p><input v-model="fullName" placeholder="修改全名"><p>姓:{{ firstName }},名:{{ lastName }}</p>
</div>
<script>
new Vue({el: "#app",data: {firstName: "张",lastName: "三"},computed: {fullName: {// getter:读取fullName时执行(默认)get() {return this.firstName + this.lastName;},// setter:修改fullName时执行set(newFullName) {// 拆分全名为姓和名(如“李四”→ firstName=“李”,lastName=“四”)const arr = newFullName.split("");this.firstName = arr[0];this.lastName = arr.slice(1).join("");}}}
});
</script>
- 效果:修改
fullName
输入框时,setter
会拆分输入值,同步更新firstName
和lastName
。
2. 计算属性传参(间接实现)
计算属性本质是“属性”,不能直接传参(如computedProp(123)
会报错),需通过“返回函数”间接实现:
<div id="app"><p>年龄判断(18):{{ checkAge(18) }}</p> <!-- 成年 --><p>年龄判断(16):{{ checkAge(16) }}</p> <!-- 未成年 -->
</div>
<script>
new Vue({el: "#app",data: { adultAge: 18 }, // 依赖的基准年龄computed: {// 计算属性返回函数,函数接收参数checkAge() {// 函数内可访问Vue实例的属性(如this.adultAge)return (age) => {return age >= this.adultAge ? "成年" : "未成年";};}}
});
</script>
- 注意:返回的函数无缓存,每次调用都会重新执行,适用于“需动态传参且计算逻辑简单”的场景。
四、实战案例:待办事项列表(Todo List)
综合运用v-for
、v-model
、数组变异方法、计算属性,实现“添加-删除-统计”的完整功能。
需求分析
- 输入框输入待办内容,回车添加到列表;
- 点击待办项,删除该条目;
- 统计“已完成/总数量”(用计算属性实现)。
完整代码实现
<div id="app"><div style="margin-bottom: 16px;"><input v-model.trim="newTodo" placeholder="输入待办内容,回车添加"@keyup.enter="addTodo"style="padding: 8px; width: 300px;"></div><!-- 待办列表 --><ul style="list-style: none; padding: 0; max-width: 300px;"><li v-for="(todo, index) in todoList" :key="todo.id" style="padding: 8px; border-bottom: 1px solid #eee; cursor: pointer; display: flex; justify-content: space-between;"@click="deleteTodo(index)"><span>{{ todo.text }}</span><span style="color: #f44336;">×</span></li></ul><!-- 统计信息(计算属性) --><p style="max-width: 300px; text-align: right;">总数量:{{ totalCount }} | 已完成:{{ completedCount }} | 未完成:{{ uncompletedCount }}</p>
</div>
<script src="https://cdn.staticfile.org/vue/2.7.0/vue.min.js"></script>
<script>
new Vue({el: "#app",data: {newTodo: "", // 新待办输入值todoList: [{ id: 1, text: "学习v-model绑定", completed: false },{ id: 2, text: "掌握计算属性缓存", completed: true }]},computed: {// 总数量totalCount() {return this.todoList.length;},// 已完成数量completedCount() {return this.todoList.filter(todo => todo.completed).length;},// 未完成数量uncompletedCount() {return this.totalCount - this.completedCount;}},methods: {// 添加待办(回车触发)addTodo() {if (!this.newTodo) return; // 空内容不添加this.todoList.push({id: Date.now(), // 时间戳作为唯一ID(避免index冲突)text: this.newTodo,completed: false // 初始未完成});this.newTodo = ""; // 清空输入框},// 删除待办(点击触发)deleteTodo(index) {this.todoList.splice(index, 1); // splice是变异方法,触发视图更新}}
});
</script>
关键要点
- 数组变异方法:用
push
(添加)、splice
(删除)确保视图响应式更新; - 唯一ID:用
Date.now()
作为待办项的id
,避免index
作key
的缺陷; - 计算属性统计:
completedCount
依赖todoList
,待办状态变化时自动重新计算,无需手动更新统计。
小练习(巩固核心知识点)
练习1:表单绑定综合(用户信息收集)
需求
- 实现用户信息表单,包含:
- 性别(单选框:男/女);
- 爱好(多选框:电影、音乐、旅行);
- 城市(下拉框:北京、上海、广州);
- 年龄(输入框,自动转数字,范围18-60);
- 点击“提交”按钮,控制台打印完整用户信息;
- 年龄输入非数字或超出范围时,按钮禁用。
练习2:计算属性缓存(商品价格计算)
需求
- 定义商品列表
products
(含name
、price
、count
字段); - 用计算属性
totalPrice
计算“所有商品的总价”(price * count
求和); - 验证缓存机制:多次引用
totalPrice
,观察是否重复计算; - 修改任一商品的
count
,验证totalPrice
是否重新计算。
练习3:计算属性传参(数组过滤)
需求
- 定义数组
users
(含name
、age
字段); - 用计算属性
filterUsers
返回函数,函数接收minAge
参数; - 调用函数时,过滤出
age ≥ minAge
的用户(如filterUsers(18)
返回成年用户); - 在模板中分别显示“≥18岁”和“≥25岁”的用户列表。
练习4:计算属性setter(全名修改)
需求
- 定义
firstName
(姓)和lastName
(名); - 用计算属性
fullName
实现:getter
:返回“姓+名”(如“张三”);setter
:输入“全名”时拆分出姓和名(如输入“李四”→firstName=“李”
,lastName=“四”
);
- 用输入框绑定
fullName
,修改输入框时同步更新firstName
和lastName
。
小练习参考答案
练习1:表单绑定综合(用户信息收集)
<div id="app"><div style="margin-bottom: 8px;"><label>性别:</label><input type="radio" name="gender" value="男" v-model="user.gender"> 男<input type="radio" name="gender" value="女" v-model="user.gender"> 女</div><div style="margin-bottom: 8px;"><label>爱好:</label><input type="checkbox" value="电影" v-model="user.hobbies"> 电影<input type="checkbox" value="音乐" v-model="user.hobbies"> 音乐<input type="checkbox" value="旅行" v-model="user.hobbies"> 旅行</div><div style="margin-bottom: 8px;"><label>城市:</label><select v-model="user.city"><option value="">请选择</option><option value="北京">北京</option><option value="上海">上海</option><option value="广州">广州</option></select></div><div style="margin-bottom: 8px;"><label>年龄:</label><input type="number" v-model.number="user.age" placeholder="18-60"></div><button @click="submit" :disabled="!isFormValid"style="padding: 8px 16px; background: #2196F3; color: white; border: none; border-radius: 4px;">提交</button>
</div>
<script src="https://cdn.staticfile.org/vue/2.7.0/vue.min.js"></script>
<script>
new Vue({el: "#app",data: {user: {gender: "",hobbies: [],city: "",age: ""}},computed: {// 表单合法性判断isFormValid() {const { gender, hobbies, city, age } = this.user;// 性别、城市必选,爱好至少选1个,年龄18-60return !!gender && hobbies.length > 0 && !!city && age >= 18 && age <= 60;}},methods: {submit() {console.log("用户信息:", this.user);alert("提交成功!");}}
});
</script>
练习2:计算属性缓存(商品价格计算)
<div id="app"><div v-for="(product, index) in products" :key="product.id" style="margin-bottom: 8px;"><span>{{ product.name }} - ¥{{ product.price }} × </span><input type="number" v-model.number="product.count" min="1" style="width: 50px;"></div><!-- 多次引用计算属性,验证缓存 --><p>总价1:{{ totalPrice }}</p><p>总价2:{{ totalPrice }}</p><p>总价3:{{ totalPrice }}</p><p>计算属性执行次数:{{ computeCount }}</p>
</div>
<script src="https://cdn.staticfile.org/vue/2.7.0/vue.min.js"></script>
<script>
new Vue({el: "#app",data: {computeCount: 0, // 统计计算属性执行次数products: [{ id: 1, name: "Vue教程", price: 59, count: 1 },{ id: 2, name: "前端手册", price: 39, count: 2 }]},computed: {totalPrice() {this.computeCount++; // 每次计算+1// 求和:price * countreturn this.products.reduce((sum, product) => {return sum + product.price * product.count;}, 0);}}
});
</script>
- 验证结果:初始时
computeCount=1
(3次引用仅执行1次);修改任一商品的count
,computeCount
变为2(仅重新执行1次)。
练习3:计算属性传参(数组过滤)
<div id="app"><p>≥18岁用户:</p><ul><li v-for="user in filterUsers(18)" :key="user.id">{{ user.name }}({{ user.age }}岁)</li></ul><p>≥25岁用户:</p><ul><li v-for="user in filterUsers(25)" :key="user.id">{{ user.name }}({{ user.age }}岁)</li></ul>
</div>
<script src="https://cdn.staticfile.org/vue/2.7.0/vue.min.js"></script>
<script>
new Vue({el: "#app",data: {users: [{ id: 1, name: "小明", age: 17 },{ id: 2, name: "小红", age: 20 },{ id: 3, name: "小刚", age: 26 },{ id: 4, name: "小丽", age: 30 }]},computed: {// 计算属性返回函数,接收minAge参数filterUsers() {return (minAge) => {// 过滤出age ≥ minAge的用户return this.users.filter(user => user.age >= minAge);};}}
});
</script>
练习4:计算属性setter(全名修改)
<div id="app"><p>姓:{{ firstName }}</p><p>名:{{ lastName }}</p><p>全名:</p><input v-model="fullName" placeholder="输入全名"style="padding: 8px; width: 200px;">
</div>
<script src="https://cdn.staticfile.org/vue/2.7.0/vue.min.js"></script>
<script>
new Vue({el: "#app",data: {firstName: "张",lastName: "三"},computed: {fullName: {// getter:读取全名时执行get() {return this.firstName + this.lastName;},// setter:修改全名时执行set(newFullName) {// 处理特殊情况(空输入或单字)if (newFullName.length === 0) {this.firstName = "";this.lastName = "";} else if (newFullName.length === 1) {this.firstName = newFullName;this.lastName = "";} else {// 拆分:首字符为姓,剩余为名this.firstName = newFullName[0];this.lastName = newFullName.slice(1);}}}}
});
</script>
- 效果:输入“李四”→
firstName=“李”
,lastName=“四”
;输入“王”→firstName=“王”
,lastName=“”
。