这篇博客记录了学习Vue数据响应式过程中的一些理解和实践,主要参考了Vue官方文档的深入响应式原理,在梳理并写下这些内容的过程中,我收获了很多,其中包括JS中getter和setter,以及Object.defineProperty()的用法。
何为响应式
响应式:事物的特征会根据条件变化,自动作出对应变化 响应式页面:页面内容能够根据设备尺寸的变化,自动显示不同样式(字体、布局)
Vue数据响应式:当数据发生变化后,使用到该数据的视图也会相应进行自动更新。
一个小实验
用Vue创建实例,传入参数options选项-数据里有一种是data,我们需要深入理解
data是Vue 实例的数据对象。Vue 会递归地把 data 的 property 转换为getter/setter,从而让 data 的 property 能够响应数据变化。
const vm=new Vue(options)
首先看这个例子:myData是一个普通的对象
const myData={
n:0
}
console.log(myData) //{n:0}
把myData对象传入Vue实例,作为data选项
const vm=new Vue({
data: myData,
template:`<div>{{n}}<button @click="add">+10</button></div>`,
methods:{
add(){
this.n=this.n+10 //this指向vm,this.n===vm.n===myData.n
}
}
}).$mount('#app')
setTimeout(()=>{
myData.n=myData.n+10
console.log(myData) //{n:(...)}
},3000)
分析两次打印的结果:
- 第一次打印出
{n:0}
对象 - 在传给new Vue之后,打印出也是一个对象,但是属性n却成为了
n:(...)
,并且多了get n、set n方法:
Vue对data做了什么?要搞清这个问题,首先要明确getter和setter,以及Object.defineProperty方法
getter与setter
let obj1 = {
姓: "张",
名: "三",
姓名() {
return this.姓 + this.名;
}
};
console.log(obj1.姓名()); // 姓名后面的括号不能删掉,因为它是函数
打印出obj1的内容,有三个属性,姓,名,姓名
getter:不加括号的函数,仅此而已。
let obj2 = {
姓: "张",
名: "三",
get 姓名() {
return this.姓 + this.名;
}
};
console.log(obj2.姓名); //get使得 姓名 是一个属性
setter :属性 也可以被写
let obj3 = {
姓: "张",
名: "三",
get 姓名() {
return this.姓 + this.名;
},
set 姓名(xxx){
this.姓 = xxx[0]
this.名 = xxx.slice(1)
}
};
obj3.姓名 = '李四' //给这个属性赋值时调用
console.log(`姓 ${obj3.姓},名 ${obj3.名}`) //姓李,名四
打印出obj3这个对象细节:有属性“姓”和属性“名”,姓名:(...)
表示并不存在姓名这个属性,而是通过get 姓名和set 姓名对姓名这个虚拟属性进行读和写。
这个姓名:(...)
与实验一中的n:(...)
有些类似,但又有些不同之处:obj3中定义了get set方法,使得姓名是一个虚拟属性,实验一种的myData经过new Vue之后的数据怎么就被加上了get set方法,使得n也变成虚拟属性了呢?请接着往下看。
Object.defineProperty( )
对这个对象的定义已经完成,但还想继续添加一些属性和方法
创建了一个obj3对象的虚拟属性xxx,可以通过get xxx set xxx的方法读写这个虚拟属性:
var _xxx=0 //先声明一个变量 给它赋值
Object.defineProperty(obj3,'xxx',{
get(){
return _xxx //如果对属性xxx读,读出的是_xxx的值
},
set(value){
_xxx=value //如果对属性xxx进行写,值也会写入_xxx中
}
})
Object.defineProperty(对象,虚拟属性xxx,{ get set }) 会给这个对象自动加上get set方法来操作这个虚拟属性xxx,从而能悄悄改变变量_xxx的值
console.log(obj3.xxx) //0
obj3.xxx=20 //对obj3.xxx属性进行写操作,也能写入到_xxx这个变量
console.log(_xxx) //20
也可以添加一个真实属性并给出赋值:
Object.defineProperty(obj3,'xxx1',{
value:0
})
由此可以总结出,Object.defineProperty()的作用是:
(1)给对象追加真实属性 (2)给对象追加虚拟属性,给对象添加getter/setter方法,对这个虚拟属性进行读写
看到这里,思路逐渐清晰,Object.defineProperty(对象,虚拟属性xxx,{ get set }) 会给这个对象自动加上get set方法来操作这个属性xxx,从而能悄悄改变变量_xxx的值。
是否new Vue时也会对myData对象,加上了虚拟属性n,然后get n,set n来对这个虚拟属性进行操作?从而悄悄改变了另一处的_n?如果是,这样做的目的是什么呢?请接着往下看。
另一个实验
let data0={ n:0 }
- 用Object.defineProperty实现为对象添加属性
let data1={ }
Object.defineProperty(data1,'n',{
value:0
})
- 加约束条件:n 不能小于 0:即 data2.n = -1 应该无效,但 data2.n = 1 有效
let data2 = {}
data2._n = 0 // _n是真实属性 _n用来偷偷存储 n 的值
Object.defineProperty(data2, 'n', {
get(){
return this._n //this就是data2
},
set(value){
if(value < 0)
return
else
this._n = value
}
})
console.log(data2.n) //0
data2.n = -1
console.log(data2.n) //0 设置为 -1 失败
data2.n = 1
console.log(data2.n) //1 设置为 1 成功
在本例中 ,_n才是我们要关心的真实属性,对data2的虚拟属性n的get set ,在修改真实属性 _n,做到让 _n的值不能<0
问题:data2.n是虚拟属性,值不能<0,但是直接对data2._n赋值,则可以修改
- 使用代理:不暴露这个对象任何可以修改的属性
let data3 = proxy({ data:{n:0} }) // 括号里是匿名对象,因为没有名字,就无法访问
function proxy({data}){
const obj = {}
Object.defineProperty(obj, 'n', {
get(){
return data.n
},
set(value){
if(value<0)return
data.n = value
}
})
return obj // obj 就是代理
}
// data3 就是 obj
console.log(data3.n) //0
data3.n = -1
console.log(data3.n) //-1
data3.n = 1
console.log(data3.n) //1
obj3就是代理,所有操作data对象值的操作都要通过obj3,对obj3的虚拟属性n操作,都是对data的真实属性n操作,通过proxy函数返回这个代理,为data3,以上做法只会暴露代理obj3,不会暴露对象data。
问题:如果绕过了代理呢?
let myData = {n:0}
let data4 = proxy({ data:myData }) //给了这个对象一个名字myData
console.log(data4.n) // 0
myData.n = -1 //修改myData
console.log(data4.n) // -1
因为proxy只能代理data的属性不被修改,如果对象的存储时被给了一个名字myData引用了这个属性n
4.代理+监听:就算用户擅自修改 myData,也要拦截他
let myData5 = {n:0}
let data5 = proxy2({ data:myData5 }) // 括号里是匿名对象,无法访问
function proxy2({data}){
// 监听 data
let value = data.n //先把这个n的值存下来 监听
//这里这个旧data就已经没用了,data.n也没有了,这样就没有办法操控原始数据了
Object.defineProperty(data, 'n', { //声明一个新data对象的虚拟属性n
get(){
return value
},
set(newValue){
if(newValue<0)return
value = newValue
}
})
// 原来的代理
const obj = {}
Object.defineProperty(obj, 'n', {
get(){
return data.n
},
set(value){
if(value<0) return
data.n = value
}
})
return obj // obj 就是代理
}
将原来的对象data重新赋值一个n,原来的n就删掉了,myData也访问不到这个n,旧data和新data仍然是一个data,但是里面的n已经不是从前那个了,是被加了监听和代理的虚拟属性n。
console.log(data5.n) //0
myData5.n = -1
console.log(data5.n) //0
myData5.n = 1
console.log(data5.n) //1
等等,这些代码看着眼熟吗?
let myData = {n:0}
let data5 = proxy2({ data:myData }) //实验二
let vm = new Vue({ data: myData }) //实验一
是不是new Vue也使用类似proxy的方法在传入内部数据data前后,也对这个data里的属性加了监听 & 代理?
原理如下:
-
给data内部数据传入一个对象
{ n:0 }
,new Vue会对这个data内部进行改造:加监听data:{ n:0 } -------------------------------------------------- //删掉了n 以下内容是新data(仍然是同一个对象) let value=0 { get n(){ return value } //读取value set n(v){ value=v } //写入value } //如果data内部属性除了n 还有m,k 那么还会有get n( )、get m( )、get k( )
-
vm对data进行代理:全权负责新data的读和写,加get set封装返回了vm对象
new Vue 对data做了哪些事:
-
监听:对myData的所有属性进行监控。为myData安装虚拟属性n,以及getter/setter方法,无论对myData的哪个属性进行读写,都会被Vue监听到。
为何要监听?为了防止myData的属性值被修改,vm不知道,如果vm知道了,调用UI=render(data),UI自动刷新页面
-
代理:让vm成为myData的代理:对myData对象属性的读写,由另一个对象vm全权负责,(vm是房东,myData是中介)对vm中虚拟属性n的读、写就相当于对myData的n进行读写,我们必须用
vm.n
或this.n
来操作myData.n
。
以上就是Vue的数据响应式的原理,Vue通过Object.defineProperty()
来实现对data的代理 & 监听。
data中存在的bug
代理时需要使用Object.defineProperty(obj,"n",{...})
,对这个代理对象obj.n的操作就相当于对data.n操作,但是必须要有一个 n
,才能够监听和代理obj.n。
bug1:Vue监听不到后添加的数据属性
但是如果我们原来传入的data没有提前定义n怎么办?Vue没办法监听到一开始不存在的数据对象的属性。
new Vue({
data: {
obj: {
a: 0 // obj.a 会被 Vue 监听 & 代理
// b:undefined //方法一
}
},
template: `
<div>
{{obj.b}}<button @click="setB">set b</button>
</div>
`,
methods: {
setB() {
this.obj.b = 1; // 页面中不会显示 1
//Vue.set(this.obj,'b',1)
//this.$set(this.obj,'b',1) 第二种写法
}
}
}).$mount("#app");
点击按钮,页面中不会显示 1 。因为Vue没办法监听一开始就不存在的b。
有两种办法解决:
-
提前把所有的key在data里声明好
-
用
Vue.set()
或者this.$set()
代替this.obj.b=1
的操作,做了三件事:- 新增key b
- 自动创建代理和监听(如果没有创建过)
- 触发UI更新(不会立即更新)
bug2:对数组的修改
let vm = new Vue({
data: {
array: ['a', 'b', 'c']
}
})
vm.array[3] = 'd' //没办法做到
因为数组数据array=['a','b','c']
相当于 array:{0:'a',1:'b',2:'c'}
。
-
方法1:
array=['a','b','c',undefined]
无效,数组长度很难知道 -
方法2:
this.$set(this.array,3,'d')
,可以,但是set没有自动创建代理和监听,也不会更新UI -
方法3:
vm.array.push('d')
,数组被传给Vue之后,会被篡改,push会调以前的push,并加入了Vue.set,这就是数组的变异Vue将传入的数组的原型链上加了一层原型VueArray,里面有七个API,覆盖了数组的七个API,对数组进行增删,并且可以自动处理监听和代理,并更新UI,因此建议用这7个API来更新UI,大致的思想如下:
class VueArray extends Array{
push(...args){
super.push(...args) //首先继承Array的push方法
/*这里就可以篡改这个数组的原型,增加Vue.set()去代替Push的操作 */
}
}
const arr=new VueArray(1,2,3,4)
这七个API有:push()
、pop()
、shift()
、unshift()
、splice()
、sort()
、reverse()
总结:何为Vue的数据响应式
- 当数据发生变化后,使用到该数据的视图也会相应进行自动更新,这就是数据响应式。
let vm = new Vue({ data: myData })
Vue通过Object.defineProperty()
来实现数据响应式,对内部数据data添加了监听 & 代理。对vm.n的读写修改,就是通过get n、set n方法对data.n的修改,从而触发视图的重新渲染。- Vue不能检测到对象属性的添加或删除,如果一开始没有在Data上声明属性,就算你对这个属性做出更改,也不会更新UI。应该调用Vue.set或者this.$set来对这个属性代理 & 监听,并更新UI。
- 对于数组,也可以用 Vue.set 或 this.$set 来新增,但更推荐使用7个变更过的数组API:push(), pop(), shift(), unshift(), splice(), sort(), reverse()。