浅谈VUE的数据响应式

186 阅读3分钟

1. getter / setter

ES6中的get和set语法将对象属性绑定到函数,访问/赋值时函数会被调用。

let obj = {
	province: '广东',
    city: '广州',
    get location(){
    	return this.province + this.city
    },
    set location(value){
    	console.log(value)
        this.province = value.substring(0,2)
        this.city = value.substring(2)
    }
}

console.log(obj.location) //广东广州
obj.location = '广东深圳' 

2. Object.defineProperty

Object.defineProperty可以用来给对象添加属性value,添加getter/setter,来对属性的读写进行监控。

先来添加属性value

let data = {}
Object.defineProperty(data, 'n', {value: 1})
console.log(data.n) // 输出1

添加getter / setter, 并对读写进行监控

let data = {}
data._n = 1
Object.defineProperty(data, 'n',{
	get(){
    	return data._n
    },
    set(value){ //对n的赋值进行监控
    	if(value > 0){data._n = value}
        else {console.log('不可为负')}
    }
})

以上代码存在data._n会被修改影响n的值的情况,再改进一下

let data = proxy({ data:{n:0} }) //传入一个匿名对象
function proxy({data}){ //解构赋值
	const obj = {}
    Object.defineProperty(obj, 'n', {
    	get(){
        	return data.n
        },
        set(value){
        	if(value > 0){data.n = value}
        	else {console.log('不可为负')}
        }
    })
    return obj
}

以上代码使用了代理的思路,即为函数中的对象obj,且只暴露代理。

但是以上代码在匿名对象的data属性指向另一个外部对象时仍存在风险,

let myData = {n:0}
let data = proxy({ data:myData })

直接修改myData也会影响n的值

于是再改进一下,对匿名对象的属性的读写也进行监听

let myData = {n:0}
let data = proxy({ data:myData })

function proxy({data}){ //解构赋值
	// 对匿名对象的data进行监听
	let value = data.n
    Object.defineProperty(data, 'n', {
    	get(){
        	return value
        },
        set(value){
        	f(value > 0){data.n = value}
        	else {console.log('不可为负')}
        }
    })
	//以下内容不变
	const obj = {}
    Object.defineProperty(obj, 'n', { //对原有的n属性进行覆盖
    	get(){
        	return data.n
        },
        set(value){
        	if(value > 0){data.n = value}
        	else {console.log('不可为负')}
        }
    })
    return obj
}

3. VUE中对data的监听与代理

const vm  = Vue({data: myData})
  • 代理:vm代理了 data 对象上所有的 property,因此访问 vm.a 等价于访问 vm.$data.a
  • 监听:Vue 将会递归将 data 的 property 转换为 getter/setter,从而让 data 的 property 能够响应数据变化。

4. data中的对象存在的问题

vue实例不会监听一开始不存在的属性。

new Vue({
	data:{
    	obj:{
        	a: 0
        }
    },
    template:`
    	<div>{{obj.b}}</div>
        <button @click='setB'>B</button>
    `,
    methods:{
    	setB(){
        	this.obj.b = 1
        }
    }
}),

点击按钮出发setB方法,为obj添加b属性,但由于初始化时并不存在该属性,所以并未被Vue实例监听与代理,并不会出发重新渲染。

Vue提供了set方法来解决此问题

setB(){
	Vue.set(this.obj, 'b', 1)
    或
    this.$set(this.obj, 'b', 1)
}

Vue.set 和 this.$set 的作用:

  • 新增key
  • 自动创建代理和监听(如果没有创建过)
  • 触发UI更新(但并不会立刻更新)

5. data中的数组存在的问题

new Vue({
	data:{
    	obj:{
        array:[1,2,3]
    },
    template:`
    	<div>{{array}}</div>
        <button @click='setArray'>set</button>
    `,
    methods:{
    	setArray(){
        	this.array[4] = 4
        }
    }
}),

以上代码无法触发视图更新,原因与对象相似,但数组情况会更复杂一些。

针对数组,VUE篡改了数组的API,提供了7个变更方法 push() pop() shift() unshift() splice() sort() reverse()和替换方法filter() concat() slice()

以上代码,应使用变更方法push()

methods:{
    setArray(){
        this.array.push(4)
    }
}

变更方法的实现思路,大致为在数组的原型链中添加一层,所添加的数组对象包含变更方法。 在ES6中可以使用继承

class VueArray extends Array{
	push(...args){
		const oldLength = this.length
		super.push(...args)
		console.log('你push了')
		for(let i = oldLength; i<this.length; i++){
			Vue.set(this, i ,this[i])
		}
	}
}

//这不是Vue的真实实现,只是展示下思路

小结

VUE的数据相应式就是VUE实例对data对象的监听和代理操作,当数据变化,vm就会调用render()函数,实时更新UI。