Vue数据响应式主要研究的是 Vue 构造选项中 data 属性的特性
深入响应式 官方文档 网址: https://cn.vuejs.org/v2/guide/reactivity.html
1. getter 与 setter
首先我们需要理解 ES6 的 getter 与 setter 语法。
// 创建一个对象,得到姓名
let obj1 = {
姓: "高",
名: "丽丽",
姓名() {
return this.姓 + this.名;
},
age: 18
};
console.log(obj1.姓名()); // 高丽丽
// 姓名后面的括号不能删掉,因为它是函数
// 姓名不要括号也能得出值
let obj2 = {
姓: "高",
名: "丽丽",
get 姓名() {
return this.姓 + this.名;
},
age: 18
};
console.log(obj2.姓名);
// 总结:getter 就是这样用的。不加括号的函数。
// 姓名可以被写
let obj3 = {
姓: "高",
名: "丽丽",
get 姓名() {
return this.姓 + this.名;
},
set 姓名(name){
this.姓 = name[0]
this.名 = name.slice(1)
},
age: 18
};
obj3.姓名 = '张宇'
console.log(`姓 ${obj3.姓},名 ${obj3.名}`)
// 将obj3打印出来后发现 姓名:(...) 是个伪属性。
// 总结:setter 就是这样用的。用 obj.x = xxx 触发 set 函数
get
语法将对象属性绑定到查询该属性时将被调用的函数。
使用get
语法时应注意以下问题:
- 可以使用数值或字符串作为标识;
- 必须不带参数;
- 它不能与另一个
get
或具有相同属性的数据条目同时出现在一个对象字面量中
当尝试设置属性时,set
语法将对象属性绑定到要调用的函数。
使用 set 语法时请注意:
- 它的标识符可以是数字或字符串;
- 它必须有一个明确的参数;
- 在对象字面量中,不能为一个已有真实值的变量使用 set ,也不能为一个属性设置多个 set。
2. Object.defineProperty() 方法
该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
let _hair = null
Object.defineProperty(obj3, 'hair', {
get() {
return _hair
},
set(color) {
_hair = color
}
})
ojb3.hair = 'black'
console.log(ojb3.hair)
小结
关于 Object.defineProperty() 方法
- 可以给对象添加属性value
- 可以给对象添加getter / setter
- getter / setter用于对属性的读写进行监控
其他属性
描述符默认值汇总
拥有布尔值的键 configurable
、enumerable
和 writable
的默认值都是 false
。
属性值和函数的键 value、get 和 set 字段的默认值为 undefined
。
在 Vue 实例创建的时候,Vue会将 data 数据变为添加了getter setter 的数据属性。
3. 模拟 vue 的数据代理原理
需求一:用 Object.defineProperty 定义 n
let data1 = {}
Object.defineProperty(data1, 'n', {
value: 0
})
console.log(`需求一:${data1.n}`) // 需求一:0
需求二:n 不能小于 0 ,比如 data2.n = -1 应该无效,但 data2.n = 1 有效
let data2 = {}
data2._n = 0 // _n 用来存储 n 的值
Object.defineProperty(data2, 'n', {
get(){
return this._n
},
set(value){
if(value < 0) return
this._n = value
}
})
console.log(`需求二:${data2.n}`) //
需求二:0
data2.n = -1
console.log(`需求二:${data2.n} 设置为 -1 失败`) // 需求二:0 设置为 -1 失败
data2.n = 1
console.log(`需求二:${data2.n} 设置为 1 成功`) // 需求二:1 设置为 1 成功
但是需求二中 如果直接修改 data2._n 属性也是无法拦截的,因此还要改进。
需求三:使用代理模式
let data3 = proxy({data:{n:0}}) // 括号里是 匿名对象,无法访问
function proxy({data} /* 解构赋值 */){
const obj = {}
// 这里的n理论上应该遍历data的所有key,这里简化了
Object.defineProperty(obj,'n',{
get(){
return data.n
},
set(value){
if(value < 0) return
data.n = value
}
})
return obj // obj就是代理对象
}
data3.n = -1 // 这里触发了data3 的 setter 函数 不会赋值为负数
console.log(data3.n) // 0
在上面的代理中,若 proxy 函数的参数是有名称的对象(可访问),那么它的值还是会被修改。
需求四:使用代理的加强版——用户修改原始对象也能拦截
let myData = {n:0}
let data4 = proxy({data:myData})
function proxy({data}){
// 这里的n理论上应该遍历data的所有key,这里简化了
let value = data.n
delete data.n // 这行可以不写 因为下面创建的n属性会被覆盖
Object.defineProperty(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
}
myData.n = -5
console.log(myData.n); // 0 被监听拦截
data4.n = -6
console.log(data4.n); // 0 被代理拦截
综上所述,Vue创建实例时对 data 的更改就包含了这个原理。
let data5 = proxy({data:myData}) // 类似于
const vm = new Vue(data:{n:0}) // Vue对于数据的更改原理正是上面解释的那样
小结
vm = new Vue({data: myData})
会让 vm 成为 myData 的代理,并且对 myData 的所有属性监控,当 myData 里的属性变化就可以调用 render(data)
来更新页面。
4. Vue data属性存在的问题
4.1 Object.defineProperty的问题
Object.defineProperty(obj, 'n', {...})
必须要有一个'n',才能监听、代理 obj.n 。如果没有写'n'的话 Vue会给出警告,无法监听后面添加的属性。
new Vue({
data: {
obj: {
a: 0 // obj.a 会被 Vue 监听 & 代理
}
},
template: `
<div>
{{obj.b}}
<button @click="setB">set b</button>
</div>
`,
methods: {
setB() {
this.obj.b = 1;
}
}
}).$mount("#app");
Vue监听不了一开始就不存在的 obj.b (undefine/null),所以页面不会显示。
4.2 解决方法——Vue.set()
可以直接在 obj 里添加 {b:undefined} ,但是实际上页面中有许多元素不可能一一添加,并且会使代码很难看,那么就要用到 Vue.set (或 this.$set)
使用 Vue.set() 方法的作用
- 新增 key
- 自动创建代理和监听(如果没有创建过)
- 触发视图更新(但并不会立刻更新)
new Vue({
data: {
obj: {
a: 0 // obj.a 会被 Vue 监听 & 代理
}
},
template: `
<div>
{{obj.b}}
<button @click="setB">set b</button>
<button @click="addB">add b</button>
</div>
`,
methods: {
setB() {
// this.obj.b = 1; //再次刷新后,页面中会显示 1
Vue.set(this.obj, "b", 1); // 也可以 this.$set(this.obj, "b", 1);
},
addB() {
this.obj.b++;
}
}
}).$mount("#app");
4.3 数组的变更方法
new Vue({
data: {
array: ["a", "b", "c"]
},
template: `
<div>
{{array}}
<button @click="setD">set d</button>
</div>
`,
methods: {
setD() {
//this.array[3] = "d"; //页面中不会显示 'd'
// 这个数组可以理解为array: [0:"a", 1:"b", 2:"c"]
// 等下,你为什么不用 this.array.push('d')
}
}
}).$mount("#app");
Vue 也不能检测新增到新增了下表,我们也不会每次修改的时候都要使用 Vue.set 或者 this.$set 。
所以当数组传给Vue时,数组的这七个方法会被篡改覆盖,文档中叫做变更方法,这些方法会自动对数组新增项添加对应的监听,并且会更新视图。
变更方法的实现
变更方法实际上就是在Vue实例上加了一层原型链,同名的放大会被最底层的原型覆盖掉,这就实现了篡改。
ES6的写法:以 push 方法为例(模拟实现,并非源码)
class VueArray extends Array{
push(...arsgs){
const oldLength = this.length // this就是当前数组
super.push(...arsgs)
console.log('我被篡改了')
for(let i = oldLength; i < this.length; i++){
Vue.set(this,i,this[i]) // 将每个新增的 key 都告诉 Vue 实例
}
}
}