当前位置: 首页 > news >正文

【Vue2手录05】响应式原理与双向绑定 v-model

一、Vue2响应式原理(底层基础)

Vue2的“响应式”核心是数据变化自动触发视图更新,其实现依赖Object.defineProperty API,但受JavaScript语言机制限制,存在“数组/对象修改盲区”,这是理解后续内容的关键。

1.1 响应式实现机制

核心流程
  1. 初始化转换:Vue实例创建时,会遍历data中的所有属性,通过Object.defineProperty为每个属性添加getter(数据读取时触发,收集依赖)和setter(数据修改时触发,通知更新);
  2. 依赖收集:当模板渲染(如{{ msg }}v-for)读取data属性时,getter会记录“哪个DOM依赖该属性”;
  3. 更新通知:当通过this.属性名修改数据时,setter会触发,通知所有依赖该属性的DOM重新渲染。
关键限制(JavaScript语言导致)

Object.defineProperty仅能监听已声明属性的“读取”和“修改”,无法监听:

  • 数组的“索引修改”和“长度修改”(如this.arr[0] = 1this.arr.length = 0);
  • 对象的“动态新增属性”和“删除属性”(如this.obj.newKey = 2delete this.obj.oldKey)。

1.2 数组的响应式盲区与解决方案

数组是响应式修改的“重灾区”,需明确“有效/无效操作”,并选择正确的修改方式。

1. 无效操作(不触发视图更新)
无效操作示例原因分析
直接索引修改数组项this.arr[0] = "新值"Object.defineProperty未监听数组索引的setter
直接修改数组长度this.arr.length = 0(清空)长度修改未触发任何setter
直接赋值新数组(引用不变)this.arr = this.arr数组引用未变,Vue无法检测变化
2. 有效操作(触发视图更新)

Vue提供3种有效方式,核心是“让Vue感知到数据变化”:

有效方式语法/示例适用场景原理
数组变异方法push()(末尾加)、pop()(末尾删)、shift()(开头删)、unshift()(开头加)、splice(index, count, 新值)(指定位置改/删)、sort()(排序)、reverse()(反转)新增、删除、修改数组项,排序/反转Vue重写(“包裹”)了这些原生方法,触发时会主动通知更新
Vue.set/$set全局方法:Vue.set(this.arr, 0, "新值");实例方法(别名):this.$set(this.arr, 0, "新值")修改指定索引的数组项(单个修改)手动为索引添加setter,触发依赖更新
整体重新赋值this.arr = ["新值1", "新值2", "新值3"]数组批量更新(如后端返回新数据)数组引用改变,Vue能检测到“整个数组替换”
3. 实际开发选择(重点)
  • 优先用“整体重新赋值”:实际项目中,前端数据多来自后端API(如列表查询),每次请求后直接用this.arr = 后端返回数据,简洁高效(使用率≈99%);
  • 少用“Vue.set”:仅在“单独修改某一个数组项”时使用(如修改列表中某条数据的状态);
  • 避免“索引修改”:即使不报错,也会导致响应式失效,属于“反模式”。

1.3 对象的响应式盲区与解决方案

对象的响应式限制主要是“动态新增/删除属性”,已声明属性的直接修改是支持响应式的。

1. 有效/无效操作对比
操作类型示例是否响应式原因分析
直接修改已声明属性this.obj.name = "新名字"✅ 是初始化时已为name添加getter/setter
动态新增属性(直接加)this.obj.age = 18❌ 否新增属性未被Object.defineProperty处理
动态删除属性(delete)delete this.obj.name❌ 否delete操作未触发setter
嵌套属性修改this.obj.info.address = "北京"✅ 是Vue默认开启“深度监听”,嵌套属性也有getter/setter
2. 解决方案(新增/删除属性)
解决方案语法/示例适用场景
Vue.set/$setthis.$set(this.obj, "age", 18)为对象新增单个响应式属性
整体重新赋值this.obj = { ...this.obj, age: 18 }新增多个属性或批量修改(用ES6扩展运算符)
删除属性后重赋值delete this.obj.name; this.obj = { ...this.obj }删除属性后,通过重赋值触发更新
3. 关键补充:深度监听与性能
  • 深度监听:Vue对data中的对象默认开启“深度遍历”,为所有嵌套属性添加getter/setter(如obj.info.address),因此嵌套属性修改支持响应式;
  • 性能影响:若对象层级极深(如10层以上),深度监听会消耗更多初始化时间,可通过vm.$watch手动设置deep: false关闭(按需监听)。

二、v-model双向绑定(表单专用)

v-model是Vue为“表单元素”设计的语法糖,实现“数据→视图”和“视图→数据”的双向同步,避免手动绑定valueinput事件(原生JS痛点)。

2.1 核心原理:语法糖拆解

v-model本质是“v-bind:value(数据→视图)”和“v-on:input(视图→数据)”的组合,以文本框为例:

<!-- v-model语法糖 -->
<input v-model="username"><!-- 等价于手动绑定(底层实现) -->
<input :value="username" @input="username = $event.target.value">
  • 数据→视图username变化时,value属性自动更新,输入框显示新值;
  • 视图→数据:用户输入时触发input事件,通过$event.target.value获取输入值,同步更新username

2.2 不同表单元素的v-model用法

v-model支持所有表单元素,但不同元素的“绑定逻辑”略有差异,核心是“匹配表单元素的value或选中状态”。

1. 文本类输入框(text/textarea)
  • 语法:直接绑定字符串变量;
  • 示例
<div id="app"><input type="text" v-model="username" placeholder="输入用户名"><textarea v-model="desc" placeholder="输入描述"></textarea><p>用户名:{{ username }}</p><p>描述:{{ desc }}</p>
</div>
<script>new Vue({el: "#app",data: { username: "", desc: "" }});
</script>
2. 单选框(radio)
  • 关键要求:必须为每个单选框设置value属性,v-model绑定的变量值与value匹配时,该单选框选中;
  • 示例
<div id="app"><label><input type="radio" name="gender" value="male" v-model="gender"></label><label><input type="radio" name="gender" value="female" v-model="gender"></label><p>选中性别:{{ gender }}</p>
</div>
<script>new Vue({el: "#app",data: { gender: "male" } // 初始选中“男”});
</script>
3. 复选框(checkbox)

分“单个复选框”(布尔值)和“多个复选框”(数组)两种场景:

  • 单个复选框(如“同意协议”):绑定布尔值,checked状态同步变量;
  • 多个复选框(如“选择爱好”):绑定数组,选中项的value会自动加入/移除数组;

示例

<div id="app"><!-- 单个复选框(布尔值) --><label><input type="checkbox" v-model="isAgree"> 同意用户协议</label><!-- 多个复选框(数组) --><div><p>选择爱好:</p><label><input type="checkbox" value="game" v-model="hobbies"> 游戏</label><label><input type="checkbox" value="reading" v-model="hobbies"> 阅读</label><label><input type="checkbox" value="sports" v-model="hobbies"> 运动</label></div><p>同意协议:{{ isAgree }}</p><p>选中爱好:{{ hobbies }}</p>
</div>
<script>new Vue({el: "#app",data: {isAgree: false,       // 单个复选框初始未选中hobbies: ["reading"]  // 多个复选框初始选中“阅读”}});
</script>
4. 下拉框(select)
  • 单选下拉框:绑定字符串,选中项的value同步变量;
  • 多选下拉框(加multiple):绑定数组,选中项的value加入数组;

示例

<div id="app"><!-- 单选下拉框 --><select v-model="city"><option value="">请选择城市</option><option value="beijing">北京</option><option value="shanghai">上海</option></select><!-- 多选下拉框(按住Ctrl选择) --><select v-model="cities" multiple><option value="beijing">北京</option><option value="shanghai">上海</option><option value="guangzhou">广州</option></select><p>单选城市:{{ city }}</p><p>多选城市:{{ cities }}</p>
</div>
<script>new Vue({el: "#app",data: {city: "",               // 单选初始未选cities: ["beijing"]     // 多选初始选中“北京”}});
</script>

2.3 v-model修饰符(实用补充)

Vue提供3个常用修饰符,简化表单值处理(无需手动写逻辑):

修饰符作用示例效果
.trim自动去除输入值的首尾空格<input v-model.trim="username">输入“ 小明 ”→ 变量值为“小明”
.number自动将输入值转为数字(非数字则保留字符串)<input v-model.number="age" type="number">输入“18”→ 变量值为18(数字类型)
.lazy从“input事件”触发改为“change事件”触发(失去焦点或回车时同步)<input v-model.lazy="username">输入时不实时同步,失去焦点后同步

示例:注册表单用修饰符

<div id="app"><input type="text" v-model.trim="username" placeholder="用户名(去空格)"><input type="number" v-model.number="age" placeholder="年龄(转数字)"><input type="text" v-model.lazy="desc" placeholder="描述(失焦同步)"><p>用户名:{{ username }}(类型:{{ typeof username }})</p><p>年龄:{{ age }}(类型:{{ typeof age }})</p><p>描述:{{ desc }}</p>
</div>

三、实战案例:导航条点击高亮(数据驱动视图)

结合“动态class绑定”“v-for循环”“事件绑定”,实现“点击导航项高亮,其他项取消”的功能,核心是“用数据控制样式,而非操作DOM”。

3.1 需求与实现思路

1. 核心需求
  • 动态渲染导航数据(如["首页", "特惠", "资讯", "我的"]);
  • 点击导航项,当前项添加“高亮样式”(如蓝色背景、白色文字),其他项恢复默认;
  • 支持默认选中(如初始选中“首页”)。
2. 实现思路(数据驱动)
  1. 定义数据navList(导航数据数组)、currentIndex(当前选中项的索引,初始为0);
  2. 循环渲染:用v-for遍历navList,生成导航项;
  3. 动态class:判断“当前项索引 === currentIndex”,为true则添加高亮类(如.active);
  4. 事件绑定:点击导航项时,更新currentIndex为当前项的索引。

3.2 完整代码实现

<div id="app"><!-- 导航容器:清除浮动 --><div class="nav-container"><!-- 导航项:v-for循环 + 动态class + 点击事件 --><div v-for="(item, index) in navList" :key="index"  <!-- 静态导航用index作key,动态数据建议用id -->class="nav-item":class="{ active: currentIndex === index }"  <!-- 高亮条件 -->@click="currentIndex = index"  <!-- 点击更新选中索引 -->>{{ item }}</div></div>
</div>
<style>/* 导航容器:清除浮动 */.nav-container {overflow: hidden;width: 600px;margin: 20px auto;}/* 导航项默认样式 */.nav-item {float: left;width: 120px;height: 50px;line-height: 50px;text-align: center;color: #333;cursor: pointer;background: #f5f5f5;margin-right: 10px;}/* 导航项高亮样式 */.nav-item.active {background: #5696ff;color: white;}
</style>
<script src="https://cdn.staticfile.org/vue/2.7.0/vue.min.js"></script>
<script>new Vue({el: "#app",data: {navList: ["首页", "特惠", "资讯", "游记", "我的"],  // 导航数据currentIndex: 0  // 初始选中第1项(索引0)}});
</script>

3.3 关键优化与扩展

  1. 动态导航数据(后端来源):若导航数据来自后端API,可在created钩子中请求数据后整体赋值:
created() {// 模拟后端请求setTimeout(() => {this.navList = ["首页", "商品", "订单", "个人中心"]; // 后端返回数据this.currentIndex = 0; // 重新设置默认选中}, 1000);
}
  1. 避免用index作key(动态数据):若导航数据可能增删(如权限控制显示/隐藏),需用唯一标识(如id)作key
data: {navList: [{ id: 1, name: "首页" },{ id: 2, name: "特惠" }]
}
// 循环时:v-for="(item, index) in navList" :key="item.id"

小练习(巩固核心知识点)

练习1:数组响应式修改(待办事项列表)

需求
  1. 定义待办数组todoList(含idtextisDone字段,初始2条数据);
  2. 实现“添加待办”:输入框输入内容,点击按钮添加到列表(isDone默认false);
  3. 实现“删除待办”:每条待办后加“删除”按钮,点击删除对应项;
  4. 实现“标记完成”:点击待办文本,切换isDone状态(完成时文本加删除线)。

练习2:对象响应式新增属性(用户信息编辑)

需求
  1. 定义用户对象user(初始含namephone字段);
  2. 实现“新增地址”:输入地址后,点击按钮用Vue.set新增address属性(响应式);
  3. 实现“修改信息”:直接修改namephone,实时显示修改结果;
  4. 显示所有用户信息(包括新增的address)。

练习3:v-model双向绑定(注册表单)

需求
  1. 实现注册表单,含“用户名”(去空格)、“年龄”(转数字)、“密码”、“确认密码”;
  2. 用户名用.trim修饰符,年龄用.number修饰符;
  3. 点击“提交”按钮,验证“密码 === 确认密码”,若不相等提示“两次密码不一致”;
  4. 验证通过后,打印表单数据(控制台输出)。

练习4:导航条高亮扩展(带路由跳转)

需求
  1. 导航数据为[{ id: 1, name: "首页", path: "/" }, { id: 2, name: "商品", path: "/goods" }]
  2. 点击导航项时,除了高亮,还需模拟“路由跳转”(打印跳转路径);
  3. 初始选中“首页”,若路径为/goods(模拟URL参数),则默认选中“商品”。

小练习参考答案

练习1:数组响应式修改(待办事项列表)

<div id="app"><div style="margin-bottom: 16px;"><input type="text" v-model="newTodoText" placeholder="输入待办内容"@keyup.enter="addTodo"  <!-- 回车添加 -->><button @click="addTodo" style="margin-left: 8px;">添加待办</button></div><ul style="list-style: none; padding: 0; max-width: 400px;"><li v-for="(todo, index) in todoList" :key="todo.id" style="padding: 8px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; cursor: pointer;":style="{ textDecoration: todo.isDone ? 'line-through' : 'none', color: todo.isDone ? '#999' : '#333' }"@click="toggleDone(index)"  <!-- 点击标记完成 -->><span>{{ todo.text }}</span><button @click.stop="deleteTodo(index)" style="color: #f44336; border: none; background: transparent; cursor: pointer;">删除</button></li></ul>
</div>
<script src="https://cdn.staticfile.org/vue/2.7.0/vue.min.js"></script>
<script>
new Vue({el: "#app",data: {newTodoText: "",todoList: [{ id: Date.now() - 1000, text: "学习Vue响应式原理", isDone: false },{ id: Date.now(), text: "完成待办练习", isDone: false }]},methods: {// 添加待办(用push,响应式)addTodo() {if (!this.newTodoText.trim()) return; // 空内容不添加this.todoList.push({id: Date.now(), // 时间戳作唯一idtext: this.newTodoText,isDone: false});this.newTodoText = ""; // 清空输入框},// 删除待办(用splice,响应式)deleteTodo(index) {this.todoList.splice(index, 1);},// 标记完成(直接修改嵌套属性,响应式)toggleDone(index) {this.todoList[index].isDone = !this.todoList[index].isDone;}}
});
</script>

练习2:对象响应式新增属性(用户信息编辑)

<div id="app"><h3>用户信息编辑</h3><div style="margin-bottom: 8px;"><label>姓名:</label><input type="text" v-model="user.name"></div><div style="margin-bottom: 8px;"><label>手机号:</label><input type="text" v-model="user.phone"></div><div style="margin-bottom: 8px;"><label>地址:</label><input type="text" v-model="newAddress"><button @click="addAddress" style="margin-left: 8px;">添加地址</button></div><h4>当前用户信息:</h4><p>姓名:{{ user.name }}</p><p>手机号:{{ user.phone }}</p><p>地址:{{ user.address || "未添加" }}</p> <!-- 新增属性默认显示“未添加” -->
</div>
<script src="https://cdn.staticfile.org/vue/2.7.0/vue.min.js"></script>
<script>
new Vue({el: "#app",data: {user: { name: "小明", phone: "13800138000" }, // 初始属性newAddress: "" // 临时存储地址输入},methods: {// 用this.$set新增响应式属性addAddress() {if (!this.newAddress.trim()) return;// 第一个参数:目标对象,第二个参数:新属性名,第三个参数:属性值this.$set(this.user, "address", this.newAddress);this.newAddress = ""; // 清空输入}}
});
</script>

练习3:v-model双向绑定(注册表单)

<div id="app"><h3>注册表单</h3><div style="margin-bottom: 8px;"><label>用户名:</label><input type="text" v-model.trim="form.username" placeholder="请输入用户名(去空格)"></div><div style="margin-bottom: 8px;"><label>年龄:</label><input type="number" v-model.number="form.age" placeholder="请输入年龄(转数字)"></div><div style="margin-bottom: 8px;"><label>密码:</label><input type="password" v-model="form.password" placeholder="请输入密码"></div><div style="margin-bottom: 8px;"><label>确认密码:</label><input type="password" v-model="form.confirmPwd" placeholder="请再次输入密码"></div><button @click="submitForm" style="padding: 8px 16px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer;">提交</button>
</div>
<script src="https://cdn.staticfile.org/vue/2.7.0/vue.min.js"></script>
<script>
new Vue({el: "#app",data: {form: {username: "",age: "",password: "",confirmPwd: ""}},methods: {submitForm() {// 验证用户名if (!this.form.username) {alert("请输入用户名");return;}// 验证年龄(数字类型)if (isNaN(this.form.age) || this.form.age < 18) {alert("请输入18岁以上的有效年龄");return;}// 验证密码一致if (this.form.password !== this.form.confirmPwd) {alert("两次密码不一致");return;}// 验证通过,打印表单数据console.log("注册表单数据:", this.form);alert("注册成功!");}}
});
</script>

练习4:导航条高亮扩展(带路由跳转)

<div id="app"><div class="nav-container"><div v-for="item in navList" :key="item.id"class="nav-item":class="{ active: currentIndex === item.id }"  <!-- 用id匹配选中 -->@click="goToPath(item)">{{ item.name }}</div></div><p style="margin-top: 20px;">当前路径:{{ currentPath }}</p>
</div>
<style>
.nav-container { overflow: hidden; width: 500px; margin: 20px auto; }
.nav-item {float: left; width: 120px; height: 50px; line-height: 50px; text-align: center;color: #333; cursor: pointer; background: #f5f5f5; margin-right: 10px;
}
.nav-item.active { background: #5696ff; color: white; }
</style>
<script src="https://cdn.staticfile.org/vue/2.7.0/vue.min.js"></script>
<script>
new Vue({el: "#app",data: {navList: [{ id: 1, name: "首页", path: "/" },{ id: 2, name: "商品", path: "/goods" },{ id: 3, name: "订单", path: "/order" }],currentPath: "/", // 初始路径currentIndex: 1   // 初始选中“首页”(id=1)},methods: {// 模拟路由跳转goToPath(item) {this.currentPath = item.path; // 更新当前路径this.currentIndex = item.id;  // 更新选中状态// 实际项目中用Vue Router:this.$router.push(item.path)console.log("跳转至路径:", item.path);}},created() {// 模拟URL参数:若路径为/goods,默认选中“商品”const mockUrlPath = "/goods"; // 实际项目中用this.$route.path获取const targetNav = this.navList.find(item => item.path === mockUrlPath);if (targetNav) {this.currentPath = mockUrlPath;this.currentIndex = targetNav.id;}}
});
</script>

文章转载自:

http://SXsdxTME.brqjs.cn
http://TCnPYEJ9.brqjs.cn
http://WVkIgZ1U.brqjs.cn
http://5VwNa5YO.brqjs.cn
http://5Gcnsm44.brqjs.cn
http://vmrM4g4a.brqjs.cn
http://wWvQBXeS.brqjs.cn
http://GEP12UOY.brqjs.cn
http://bDSLpWbY.brqjs.cn
http://pHryl9ac.brqjs.cn
http://0rqfAdGt.brqjs.cn
http://KBqlYNsc.brqjs.cn
http://xZLyFWoM.brqjs.cn
http://dmypb96n.brqjs.cn
http://JuqKAFVy.brqjs.cn
http://7IyqnCiy.brqjs.cn
http://hAleOs4Y.brqjs.cn
http://CJMR9uhH.brqjs.cn
http://sX3BVrdO.brqjs.cn
http://6yyVRfNx.brqjs.cn
http://pXNBnkpU.brqjs.cn
http://cvfaq7lN.brqjs.cn
http://Qo8WXYSW.brqjs.cn
http://Hr4Q6tEs.brqjs.cn
http://tQTp4AND.brqjs.cn
http://m7urL7zh.brqjs.cn
http://iHkLUxqH.brqjs.cn
http://OT5ECNXw.brqjs.cn
http://y4oKqjLz.brqjs.cn
http://zFIOzPTU.brqjs.cn
http://www.dtcms.com/a/377918.html

相关文章:

  • spring项目部署后为什么会生成 logback-spring.xml文件
  • Java 日期字符串万能解析工具类(支持多种日期格式智能转换)
  • 在VS2022的WPF仿真,为什么在XAML实时预览点击 ce.xaml页面控件,却不会自动跳转到具体代码,这样不方便我修改代码,
  • 【数组】区间和
  • Qt 基础编程核心知识点全解析:含 Hello World 实现、对象树、坐标系及开发工具使用
  • 解决推理能力瓶颈,用因果推理提升LLM智能决策
  • 【大前端】常用 Android 工具类整理
  • Gradle Task的理解和实战使用
  • 强大的鸿蒙HarmonyOS网络调试工具PageSpy 介绍及使用
  • C++/QT 1
  • 软件测试用例详解
  • 【ROS2】基础概念-进阶篇
  • 三甲地市级医院数据仓湖数智化建设路径与编程工具选型研究(上)
  • 利用Rancher平台搭建Swarm集群
  • BRepMesh_IncrementalMesh 重构生效问题
  • VRRP 多节点工作原理
  • 运行 Ux_Host_HUB_HID_MSC 通过 Hub 连接 U 盘读写不稳定问题分析 LAT1511
  • Oracle体系结构-控制文件(Control Files)
  • 0303 【软考高项】项目管理概述 - 组织系统(项目型组织、职能型组织、矩阵型组织)
  • Spark-SQL任务提交方式
  • 10、向量与矩阵基础 - 深度学习的数学语言
  • 开发避坑指南(45):Java Stream 求两个List的元素交集
  • React19 中的交互操作
  • 阿里云ECS vs 腾讯云CVM:2核4G服务器性能实测对比 (2025)
  • 网络编程;TCP多进程并发服务器;TCP多线程并发服务器;TCP网络聊天室和UDP网络聊天室;后面两个还没写出来;0911
  • STM32项目分享:基于stm32的室内环境监测装置设计与实现
  • 利用归并算法对链表进行排序
  • GPU 服务器压力测试核心工具全解析:gpu-burn、cpu-burn 与 CUDA Samples
  • Power Automate List Rows使用Fetchxml查询的一个bug
  • Zynq开发实践(FPGA之ddr sdram读写)