什么叫响应式?Vue的数据响应式如何实现?
- 一个物体对外界的刺激作出的反应,就叫响应式。Vue2主要是通过Object.defineProperty函数来实现数据响应式。
首先来说一下ES6的getter(get)和setter(set)属性,如下例子:
let obj1 = {
姓: "周",
名: "杰伦",
姓名() {
return this.姓 + this.名;
},
age: 18
};
console.log("需求一:" + obj1.姓名());
//log出:需求一:周杰伦
// 需求二,调用对象的姓名时不用括号也能得出值
let obj2 = {
姓: "周",
名: "杰伦",
get 姓名() {
return this.姓 + this.名;
},
age: 18
};
console.log("需求二:" + obj2.姓名);
//log出:需求二:周杰伦
//总结:通过getter(get)就可以调用不加括号的函数,仅此而已。
// 需求三:姓名可以被对象外部赋值
let obj3 = {
姓: "周",
名: "杰伦",
get 姓名() {
return this.姓 + this.名;
},
set 姓名(xxx){
this.姓 = xxx[0]
this.名 = xxx.slice(1)
},
age: 18
};
obj3.姓名 = '周杰伦'
console.log(`需求三:姓 ${obj3.姓},名 ${obj3.名}`)
//log出:需求三:姓周,名杰伦
// 总结:通过setter(set),并传入xxx参数触发set函数
Object.defineProperty
- 语法:Object.defineProperty(obj, prop, descriptor),obj要定义属性的对象,prop要定义或修改的属性的名称或Symbol.
//当我们声明完一个对象时,如何增加属性
let data2 = {}
data2._n = 0 // _n 用来存储 n 的值
Object.defineProperty(data2, 'n', {
get(){
return this._n
},
set(value){
if(value < 0) return
this._n = value
}
})
data2.n = -1//设置失败,原因是传入的对象不符合条件,什么也不返回
console.log(`${data2.n} `)//log出来的结果还是get返回的值0
data2.n = 1
console.log(`${data2.n}`)//满足需求,结果是通过set传入的
//总结:上述方法会存在一个问题,就是如果直接将data._n赋值为1的话,就能设置成功,那么们就要用到监听和代理
- 如何利用Object.defineProperty进行监听和代理
let myData5 = {n:0}
let data5 = proxy2({ data:myData5 }) // 括号里是匿名对象,无法访问
function proxy2(options){
const {data}=options
let value = data.n//存下n的值
Object.defineProperty(data, 'n', {
get(){
return value
},
set(newValue){
if(newValue<0)return
value = newValue//将新的并符合条件的newValue值先赋值给value,接下来通过代理的方法将value值赋给data.n
}
})
// 上面的这一段代码就是监听data
const obj = {}//创建一个空对象
Object.defineProperty(obj, 'n', {
get(){
return data.n
},
set(value){
if(value<0)return//这句话多余了
data.n = value
}
})
return obj // obj 就是代理,返回的obj就是data5
}
myData5.n = -1//就算直接在myData5上赋值,也会被data5监听到
console.log(`需求五:${data5.n},设置为 -1 失败了`)
myData5.n = 1
console.log(`需求五:${data5.n},设置为 1 成功了`)
- 我们不妨比较一下下面两句代码
let data5 = proxy2({ data:myData5 })
let vm = new Vue({data:myData})
根据上面两行代码的对比,我们得出:Object.defineProperty的监听和代理原理就是Vue源代码所体现出来的原理,实际上就是让vm成为myData的代理(proxy),会对myData所有的属性进行监控,无论你对myData的属性进行读写,都会被Vue的set和get读取到。当vm知道了myData改变后,就会调用render(data),这样UI页面就得以重新渲染UI=render(data),同理修改了vm.n,UI中的n也会重新渲染,这就是Vue通过Object.defineProperty来实现数据响应式的基本过程
- 但是Object.defineProperty有个问题,比如:Object.defineProperty(obj,‘n’,{…}),必须要有一个’n’,才能监听和代理obj.n,Vue就用了vue.set和this.$set解决了这个问题,当你忘记写某数据时,他会自动帮你添加这个key,并创建监听和代理,并触发UI更新。
new Vue({
data: {
obj: {
a: 0 // obj.a 会被 Vue 监听 & 代理
}
},
template: `
<div>
{{obj.b}}//由于data上没有b,他是不会被显示的
<button @click="setB">set b</button>
</div>
`,
methods: {
setB() {
//this.obj.b = 1; 不会显示1
Vue.set(this.obj,'b',1)//Vue会自动帮你加b数据,并且进行监听和代理
this.$set(this.obj,'b',1)//两种方式都可以
}
}
}).$mount("#app");
- 但是当我们的data是一个数组时,我们不能确定数组的长度,所以用Vue.set和this.$set不切实际。此时我们应该利用Vue篡改数组的7个API解决,分别push()pop()shift()unshift()splice()sort()reverse(),例子如下
new Vue({
data: {
array: ["a", "b", "c"]
},
template: `
<div>
{{array}}
<button @click="setD">set d</button>
</div>
`,
methods: {
setD() {
this.array.push('d')//注意此时的push和Array的push并不是同一个push
}
}
}).$mount("#app");
我们可以用ES6来简单描述一下篡改的大致原理,但不一定是Vue源代码所显示的那样
class VueArray extends Array{//继承JS的数组类型
push(...args){//
const oldLength = this.length // 记下最开始数组的长度
super.push(...args)//super表示去调用父类的push
console.log(' push ')
for(let i = oldLength; i<this.length; i++){//遍历增加后的数组
Vue.set(this, i, this[i])//利用Vue.set的去告诉Vue,惊醒代理和监听
}
}
}