我们讨论的问题是:我们修改了this.data中的数据,vue往往能监测到这个数据发生变化,并且渲染在页面上,vue是怎么监测数据的变化的呢?
阅读本文需要了解一下数据代理的知识点
vue数据劫持 监测对象
我们先来思考一个问题:这里有一个对象obj,我们希望当它其中的内容发生变化的时候,控制台就输出它发生了变化
这里主要使用的是数据劫持,数据劫持我们常用的api就是Object.defineProperty
最简单的想法:劫持一个name属性,然后在setter中输出一句话,那好了,设置了name属性(name属性变化)就调用setter,就会显示这段话了
但是我们发现有什么问题呢?我们发现由于缺少getter,我们根本无法查看obj.name的值
好,那么我们就加上getter,大家最最常见的想法就是getter直接返回obj.name,这样不就可以获得其当前的name属性值了嘛?但我们发现查看后直接报错了,原因是内存溢出
为什么会内存溢出呢?我们用图解释一下,当你查看name属性的时候就调用了getter,但是return obj.name的时候,也查看了name属性,就形成了循环调用,自然就内存溢出了
所以在getter/setter中我们不能直接调用当前劫持对象的属性,这会造成内存溢出
但是我们知道,如果想要监测数据变化就必须使用数据劫持,所以我们得用到一些技巧,来确保数据劫持正常运行
观察下面的代码:我们的思路就是创建一个副本,通过这个副本操作data的读/写,作为Object.defineProperty中操作的对象,这个对象有什么要求?它必须拥有data中所有的属性名,有了这些属性名之后以便添加到这个副本上,我们为这个副本上的属性分别创建getter/setter,但getter返回的是data中的数据,setter修改值的时候,修改的是data中的数据,接下来我们只对这个副本进行操作即可。
当这个副本被访问的时候,直接返回data中当前的数据(为啥现在可以返回了?因为我们Object.defineProperty操作的对象是这个副本,读取的是另一个对象data中的数据,当然不会造成循环引用),这样,我们就可以进行我们之前无法进行了操作:在getter/setter中 访问/修改 data中的数据
<script> let data = { name: 'zhouzhou', age: 18 } //数据劫持 //使用构造函数创建一个监视对象的实例 let obs = new Observer(data) console.log(obs); //构造函数 function Observer(obj) { //获取obj中的所有属性名 const keys = Object.keys(obj) keys.forEach(item => { //最大的注意点:我们这里向this添加属性,而不是直接向data中添加属性 //构造函数的this是指向它本身的,所以这里this是指向Observer这个构造函数创建的实例 //其中item代表data中的所有属性名 Object.defineProperty(this, item, { get() { //这里是返回对象的值 return obj[item] }, set(v) { obj[item] = v } }) } ) } </script>
其中我们使用obj[item]这种形式来返回变量的值,一般我们获取变量值的方式是使用obj.name则会中方式,这里使用obj[item]这种方式的原因是,.运算符后面无法跟变量名
编写完代码之后,回到控制台输出一下obs对象
回到我们的标题,vue是怎么监测对象变化的?为何我们修改data中的数据,vue可以监测到并且返回到页面上?其中基本原理就是数据劫持
我们来输出一个vue的实例对象
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no,maximum-scale=1.0,minimum-scale=1.0"> <title>Title</title> </head> <body> <script src="../js/vue.js"></script> <div id="app"> </div> <script> const vm = new Vue({ el: '#app', data: { msg : { name: 'zhouzhou', age: 18 } } }) console.log(vm); </script> </body> </html>
可以看到,Vue实例上有个_data,这也是vue进行数据劫持之后的副本
只不过,vue的getter/setter函数都变成了reactiveGetter/reactiveSetter,其中进行了监测到数据改变之后,数据渲染到页面上等操作,当然vue的代码比我们刚才演示的小demo复杂的多,我们还有很多问题没有解决,比如:如果我们data中包含有对象呢?我们的demo无法监测到其中的变化,但是vue可以,具体怎么搞的我也不太明白,日后阅读vue源码之后再来分享,我们这里使用的数据劫持api是Object.defineProperty,这是vue2使用的数据劫持api,而vue3换装了proxy,这有助于vue3监测数组中的变化
Vue.set()
先贴一张文档介绍
观察下面这个例子,我们message中没有sex属性,那么我们该怎么加上sex并且让其显示呢?
页面中没有输出message.sex因为vue默认undefined不显示
对象中没有的属性sex,为undefined,但是如果没有这个对象,vue就会报错
我们在控制台尝试向这个对象中添加数据
可以看到我们自己添加的sex属性没有getter/setter方法,因为没有其方法,所以页面上无法显示
这时候,不改变data中的数据代码的情况下,我们可以使用Vue.set方法(在vue实例对象身上名叫$set)
注意:set方法,不能往data中直接创建对象,也就是说,不能创建message同级的对象
使用示例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no,maximum-scale=1.0,minimum-scale=1.0"> <title>Title</title> </head> <body> <script src="../js/vue.js"></script> <div id="app"> <div> <button @Click="showSex()">点击添加sex属性</button> {{message.name}}+{{message.age}}+{{message.sex}} </div> </div> <script> const vm = new Vue({ el: '#app', data: { message: { name: 'zhouzhou', age: 21 } }, methods: { showSex() { this.$set(this.message, 'sex', '男') } } }) </script> </body> </html>
vue监测数组
我们来看一个例子
我们点击添加爱好页面中的数据是否会更新呢?
我们发现,页面上的爱好依旧是三个,但是vue实例上,却有四个爱好
那说好的响应式呢?咋没用了?我们阅读vue2的官方文档,我们可以发现
如果要支持响应式,那么操作数组的话请使用以下方法,而不是直接使用[]来操作数组
收集表单信息
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>收集表单数据</title> <script type="text/javascript" src="../js/vue.js"></script> </head> <body> <!-- 收集表单数据: 若:<input type="text"/>,则v-model收集的是value值,用户输入的就是value值。 若:<input type="radio"/>,则v-model收集的是value值,且要给标签配置value值。 若:<input type="checkbox"/> 1.没有配置input的value属性,那么收集的就是checked(勾选 or 未勾选,是布尔值) 2.配置input的value属性: (1)v-model的初始值是非数组,那么收集的就是checked(勾选 or 未勾选,是布尔值) (2)v-model的初始值是数组,那么收集的的就是value组成的数组 备注:v-model的三个修饰符: lazy:失去焦点再收集数据 number:输入字符串转为有效的数字 trim:输入首尾空格过滤 --> <!-- 准备好一个容器--> <div id="root"> <form @submit.prevent="demo"> 账号:<input type="text" v-model.trim="userInfo.account"> <br/><br/> 密码:<input type="password" v-model="userInfo.password"> <br/><br/> 年龄:<input type="number" v-model.number="userInfo.age"> <br/><br/> 性别: 男<input type="radio" name="sex" v-model="userInfo.sex" value="male"> 女<input type="radio" name="sex" v-model="userInfo.sex" value="female"> <br/><br/> 爱好: 学习<input type="checkbox" v-model="userInfo.hobby" value="study"> 打游戏<input type="checkbox" v-model="userInfo.hobby" value="game"> 吃饭<input type="checkbox" v-model="userInfo.hobby" value="eat"> <br/><br/> 所属校区 <select v-model="userInfo.city"> <option value="">请选择校区</option> <option value="beijing">北京</option> <option value="shanghai">上海</option> <option value="shenzhen">深圳</option> <option value="wuhan">武汉</option> </select> <br/><br/> 其他信息: <textarea v-model.lazy="userInfo.other"></textarea> <br/><br/> <input type="checkbox" v-model="userInfo.agree">阅读并接受<a href="http://www.atguigu.com">《用户协议》</a> <button>提交</button> </form> </div> </body> <script type="text/javascript"> Vue.config.productionTip = false new Vue({ el:'#root', data:{ userInfo:{ account:'', password:'', age:18, sex:'female', hobby:[], city:'beijing', other:'', agree:'' } }, methods: { demo(){ console.log(JSON.stringify(this.userInfo)) } } }) </script> </html>