1、Vue 对 data 做了什么
Vue 的数据响应式是它的一大特点,也是它的一大优势,Vue 中的 data 数据模型仅仅是普通的 JavaScript 对象,而当你修改它们时,视图会进行更新,这使得状态管理非常简单直接,那 Vue 到底对 data 做了什么呢? Vue 官方解释:传送门
const myData = { //将实例中的data抽出来 n: 0 } console.log(myData); //页面会打印{n:10} new Vue({ data: myData, //将myData传递给Vue实例的data属性 template: ` <div>{{n}}</div> ` }).$mount("#app"); setTimeout(()=>{ myData.n += 10; console.log(myData); //页面会打印{n:(...)} },3000)
上述实例中,第一次打印的 myData 数据是 { n:0 } 的形式,第二次打印的 myData 数据是 { n:(...) } 的形式,这中间经历了将 myData 传递给 Vue 实例的 data 属性的过程,这个过程肯定是 Vue 对 myData 做了什么导致它的形式变化,往下看!
2、ES6 的 getter 和 setter
了在解 Vue 数据响应式原理以及上述实例中的打印 myData 发生变化的原因之前,我们先来了解一下 ES6 的 getter 和 setter,对象属性可以是一个 函数、getter、setter 方法。get 和 set 也是对象的属性,只不过它们是以函数的形式定义的。
let person = { let obj = { //对象属性可以是一个 函数、getter、setter 方法 姓: "程", property: function ([parameters]) {}, 名: "序员", get property() {}, get 姓名() { set property(value) {}, return this.姓 + this.名; }; }, set 姓名(xxx){ this.姓 = xxx[0]; this.名 = xxx.slice(1); } }; console.log(person.姓名); //打印:程序员 //getter 相当于不加 () 的函数 person.姓名 = '舒化奶'; //用 = xxx 触发 set 函数 console.log(person); //打印:{age:18,姓:舒,名:化奶,姓名:(...)} 和Vue中的myData一样的效果
上述实例在打印 person 对象时,会发现 person 对象中多了一个 姓名 属性,它的形式和 Vue 中的 myData 的形式类似。我们将 person 对象的 姓名:(...) 属性展开,里面包含了 get 姓名:f 姓名() 和 set 姓名:f 姓名(xxx) 。
这表示,用户确实可以对 person 的姓名属性进行读写,但实际上 person 对象并不存在该姓名属性。由此类比 Vue 中的 myData 对象,打印后的 { n:(...) } 表示 Vue 实例上的 data 对象中的 n,并不会原始属性,而用户却可以通过 get/set 操作它。
那 Vue 为什么要把 data 变成这种 get/set 的形式呢,这和 Vue 的数据响应式原理有什么关系?往下看!
3、Object.defineProperty
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象,附上该属性的 mdn 文档,感兴趣的同学可以移步了解一下 → 传送门。
let person = { _age: 18 }; //如果一个对象被声明完了之后需要对其新增 get/set... 属性的话,需使用Object.defindProperty Object.defindProperty(person,'age',{ value: 88, //给 person 对象添加一个值为 88 的 age 属性! get(){ return this._age }, set(value){ //这里可对定义的属性进行筛选操作,比如添加 if 语句,可以保证设置的值满足条件才生效 if(value < 0) return; this._age = value } })
上述这种方式可以实现筛选功能,比如在 set() 方法中添加 if 判断语句,从而实现有条件的设置对象的属性值,如果设置的值是正数就进行赋值,如果是负数就不进行设置。但是要注意,getter/setter 是在对象声明的时候直接用的, 如果想在声明后继续给对象添加 getter/setter 属性,则需要通过 Object.defineProperty() 的方法。
4、proxy 代理数据的安全性
上述实例可以实现数据的筛选设置功能,但是并不能保证 person._age 属性的安全性,因为其他开发人员可以通过 persong._age = -1;的方式直接将其设置为一个负值,为了解决这种安全性问题,我们可以使用下面这种 proxy 代理的方法!
let data1 = proxy({ data:{n:0} }); //括号里是匿名对象,无法访问 function proxy({data}){ //{data}是结构赋值的写法 const obj = {} Object.defineProperty(obj, 'n', { //这里的'n'写死了,理论上应该遍历data的所有key get(){ return data.n; }, set(value){ if(value < 0) return; data.n = value } }) return obj; // obj 就是代理,这样可以保证 data 中的数据不会被直接通过 data.n 的方式进行篡改 }; //但是这种方式并不是银弹! console.log(data1.n); //可以拿到 data1.n 属性,但是不能通过 data1.n=-1 进行篡改 let myData = {n:0}; //但是如果将data设置成一个引用值 let data = proxy({ data:myData }); //括号里是匿名对象,无法访问 myData.n = -1; //依然可以对属性 n 进行直接篡改! 往下看!
let myData = {n:0} let data2 = proxy({ data:myData }) // 括号里是匿名对象,无法直接访问,但是可以访问引用 myData function proxy({data}){ //{data}是结构赋值的写法 let value = data.n; //获取data中的myData的n属性 Object.defineProperty(data, 'n', { //创建一个n的虚拟属性,进行监听,防止被偷改:delete data.n get(){ return value; }, set(newValue){ if(newValue < 0) return value = newValue; } }) // 就加了上面几句,这几句话会监听 data const obj = {} Object.defineProperty(obj, 'n', { get(){ return data.n; }, set(value){ if(value < 0) return //这句话多余了 data.n = value; } }) return obj; // obj 就是代理 }
上述实例是只向外暴露代理对象,开发人员无法直接通过 data.n = -1;的方式设置 data 的属性。示例链接:传送门
5、Vue 的数据响应式原理
上述讲到的实例其实和 Vue 的数据响应式原理类似,如下代码:
let data2 = proxy({ data: myData }) let vm = new Vue({ data: myData })
实际上 Vue 在声明实例的时候:const vm = new Vue({data:myData});做了如下两件事情:
一、会让 vm 成为 myData 的代理(proxy)。
二、会对 myData 的所有属性进行监控,实际上是代理的内部虚拟属性。
监控的目的是为了防止 myData 的属性变了,但 vm 却不知道。如果实现监控的话,只要 myData 的属性改变都会被 vm 监听到,然后就可以调用 render(data),将数据在视图层进行重新渲染,实现数据的响应式。
Vue 的 options.data 在传入 Vue 后,会被 vue 监听,data 会被篡改,本来的 n 变成 get(n) set(n)
Vue 的 options.data 在传入 Vue 后,会被 vue 实例代理,const vm = new Vue(),vm 就是 options.data 的代理
options.data 的所有读写都会被 Vue 监控,不管是对 data 本身,还是它的代理都会被监控,vue会在data变化时更新UI
6、实现一个 input 双向绑定
原理都说的差不多了,然后说一个在面试过程中关于 Vue 双向绑定的一个高频题吧,毕竟纸上得来终觉浅,万一面试官让自己手写一个的话,你只会说理论很显然是不行的。
首先我们来看一下如何做到保证 视图层 => 数据层 方向的绑定,当调用属性的地方改变了这个属性(通常是一个表单元素),那么这个对象(或变量)的属性也随之改变,具体代码如下:
<input type="text" id="model"> <script> let user = {age: 1}; model.value = user.age; //设置初始化默认值 model.addEventListener('input', (event) => { //监听 input 事件 user.age = event.target.value; console.log('在 input 框内修改视图层的值!') console.log('此时 model 层的 user.age 为:' + user.age); }) </script>
然后我们再来看一下如何做到保证 数据层 => 视图层 方向的绑定,当一个对象(或变量)的属性改变,那么调用这个属性地方也应该改变,具体代码如下:
<input type="text" id="model"> <button onclick="addAge()">年龄 +1</button> <script> let user = {age: 1}; model.value = user.age; //设置初始化默认值 Object.defineProperty(user, 'age', { get: function() { return model.value; }, set: function(v) { model.value = v; console.log('此时 view 层的 model.value 为:' + model.value); } }) function addAge() { user.age += 1; console.log('点击按钮,修改 model 层的 user.age!'); } </script>