1. ES语法的getter和setter
在开始了解 Vue
的数据响应式原理前应该先搞清楚 ES语法 中的 getter
和 setter
方法的具体用法。
getter和setter
方法是以 get
和 set
关键字来为对象添加虚拟属性的一种方式。这种属性其实并不真实存在,而是以取值函数 getter
和存值函数 setter
来模拟的一种属性。目的是对某个属性设置存值函数和取值函数,拦截该属性的存取行为,以便于对该属性的存取做一些限定处理。如下所示(以下代码来源于 mdn)
getter 方法
const obj = {
log: ['a', 'b', 'c'],
get latest() { //在正常方法前加 get 关键字
if (this.log.length == 0) {
return undefined;
}
return this.log[this.log.length - 1];
}
}
console.log(obj.latest);// 输出 c,获取的是属性名不用带括号
setter方法
const language = {
set current(name) {
this.log.push(name);
},
log: []
}
language.current = 'EN';
language.current = 'FA';
console.log(language.log);//输出 Array ["EN", "FA"]
2. ES语法的 defineProperty
defineProperty
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象,可用于在一个对象定义好后为其修改或添加属性。
语法为:
Object.defineProperty(obj, prop, descriptor)
添加常规属性:
let data = {
m: 0
}
Object.defineProperty(data, 'n', {
value: 1 //添加属性的 value 就是其值
})
console.log(`${data.n}`) //则会输出n值为1
也可用来添加 getter
和 setter
的虚拟属性
let data1 = {
_n: 0
}
Object.defineProperty(data1, 'n', {
get(){
return this._n
},
set(value){
if(value < 0) return
this._n = value
}//直接写明 get / set 即可
}) //由于指明了虚拟属性为 n,即 get n(){}、set n(value){},因此在函数定义时就不用再写n了
3. Vue对数据的代理和监听
代理,即 proxy
,简单来说我自己的一些事情我自己不亲自处理,而是交给一个人让他去帮我做,那个做事的人就是代理。这个逻辑中有两个关键点需要搞清楚,代理是处理操作的人,而其处理操作的事情不属于他,而是属于委托其代理的人的。
因此类比到 Vue数据代理
,委托代理的是 data{}
数据对象,其找到代理就是 Vue实例vm
,data{}
数据对象要代理 vm
做的事情是管理 data{}
数据对象里数据操作。因此 data{}
数据对象只负责内部数据的生产即可,对生产出来的数据的管理和操作全权交给 vm
处理。
那么 vm
如何对 data{}
数据对象里的数据进行控制和操作呢?换句话说,vm
如何在 data{}
数据对象里面的任意一个属性值变化时都及时知道呢?
于是便用到了 ES 语法中的 getter和setter
方法,通过 getter和setter
方法控制的属性的任何操作都会被这两个函数检测到,而 getter和setter
方法形成的属性是虚拟属性,真实并不存在,因此如果用户想私自不经过代理 vm
直接修改 data{}
数据对象的属性也获取不到对应的实体属性,只能通过 getter和setter
方法修改,那么其修改就必定被 vm
检测到。
因此 vm
为了实现对 data{}
数据对象里数据的全部控制,就必须在 Vue实例
创建的时候对传进来的 data{}
数据对象做一些处理,做的处理就是将 data{}
数据对象里的属性都变成了 getter和setter
方法控制的虚拟属性,并保存在代理数据对象 obj
并返回。
但为了不让用户直接修改原来的 data{}
属性,也将原来的 data{}
对象的实体属性全改变了,添加的虚拟属性名字和实体属性名一样,就会用虚拟属性覆盖原来的实际属性,用户在修改属性值是就是通过 getter和setter
方法修改的虚拟属性。这样一来 data{}
数据对象的全部属性的任何变化都会被 Vue实例vm
检测到。
let myData = {n:0}
let data = proxy({ data:myData }) // 类似于 let vm = new Vue({data: myData})
function proxy({data}/* 解构赋值*/){
let _n = data.n
Object.defineProperty(data, 'n', { //覆盖原来的data.n属性
get(){
return _n
},
set(newValue){
if(newValue<0)return
_n = newValue
}
})// 改变data{}数据对象本身属性,可通过闭包形成上下文,让原来的实际属性值存在闭包的上下文_n中
const obj = {}
Object.defineProperty(obj, 'n', {
get(){
return data.n
},
set(value){
data.n = value
}
}) //添加data{}数据对象的代理,对data{}数据对象操作
return obj // obj 就是data{}的代理
}
4. Vue的数据响应式
所谓响应式就是当事物发生变化时会根据变化做出相应的反映。
Vue
中的数据 data
是响应式的,由上述 Vue
通过 Object.defineProperty()函数
来用 getter和setter方法
对 data
数据做了代理和监听,一旦数据发生变化,Vue
就会改变数据对应的 UI
视图,这就是 Vue的数据响应式
但是 Vue
使用 Object.defineProperty
来设置监听,就只能对在 Vue实例化
时 data
对象里已经存在的属性设置监听,而对不存在的或者后来添加进去的属性没有进行监听。
为了解决这个问题,有两种方法:
1. 将所有属性都提前声明好
2. 使用 Vue.set 和 this.$set 添加属性
使用 Vue.set
和 this.$set
添加属性是会通知 Vue
对这后添加的属性也设置监听操作。
Vue.set('this.data','m','10')
this.$set('this.data','m','10')//为vm的data对象添加属性m值为10
3.数组变异
详细见 vuejs官网
对于数组的数据增加,无法控制其新增个数因此不能提前声明所有数据值,而一个一个 set 又太麻烦,而且数组是常用的对象数据类型中的一种,因此 vue
的作者就对数组的增删函数如 push
和 pop
等进行了篡改,用户在使用 vue
中数组增删时仍是用 push
和 pop
,但是里面进行了额外的处理,这几个被篡改的 API 会对数组新增是数据代理监听并根据数据响应改变 UI 视图。