Vue 数据响应式原理 defineProperty

222 阅读5分钟

es6中的getter setter属性

getter setter是一种语法;
一个属性被设置了getter setter方法之后,就变成了【getter setter属性】;
【getter setter属性】声明时以函数的形式去,但在使用时仍把他当作属性去使用;
【getter setter属性】的特点是能动态获取最新结果,适合用作【计算属性】
以下是一个计算属性的例子,也就是把原有的普通属性拼凑在一起
let obj3 = {
  姓: "高",
  名: "圆圆",  
  old姓名() {
    return this.姓 + this.名;
  },
  get 姓名() {
    return this.姓 + this.名;
  },
  set 姓名(xxx){
    this.姓 = xxx[0]
    this.名 = xxx.slice(1)
  }
};

console.log(obj3.old姓名())
console.log(obj3.姓名) // 姓名是一个计算属性,把他当作一个普通的属性一样去访问就好
obj3.姓名 = '高媛媛' // 姓名是一个计算属性,把他当作一个普通的属性一样去设置就好

console.log(`需求三:姓 ${obj3.姓},名 ${obj3.名}`)
  • es6之前要得到姓名 需要定义函数old姓名并且调用obj3.old姓名()注意,因为这是传统的函数,所以肯定是要圆括号的
  • 但现在你只需要console.log(obj3.姓名)
  • 而很显然es6前如果我们还要修改姓名,那肯定还要写一个函数,但有了getter setter就方便很多

defineProperty用法解析 (vue全解 class3 - 3rd)

  • 重要结论1:defineProperty设置【getter setter属性】必须有一个数据源
  • 重要结论2:所使用的数据源也就是中间变量!

defineProperty的作用是

  1. 可以给对象添加普通属性和值
  2. 可以给对象添加【getter setter属性】

以下示例告诉你defineProperty设置普通属性

let obj={};
Object.defineProperty(obj,'name',{  // 第三个参数要对象的形式
	value:'ryan' // 赋值的话要记得写value
})

用defineProperty的getter setter方法设置一个【getter setter属性】就需要用到第三者 | 数据源写在全局上 var xxx | 数据源写在自身对象上,_xxx | 数据源+中间变量value | |------|------------|------------| |xxx容易被篡改| obj身上需要多一个下划线属性 _age |不需要身上多一个下划线属性| || obj._age可被篡改 |但中间变量value会被篡改|

  • 下面先引出前两种设置【getter setter属性】的方式,至于第三种方式就隐藏在后面例子中的proxy函数中
// 例1 数据源写在全局上
let obj={}
var _age; 
Object.defineProperty(obj,'age',{
	get(){
		return _age;
	},
	set(value){
		_age=value;
	}
})
// 例2 数据源设置在对象上 
let obj={_age:0}
Object.defineProperty(obj,'age',{
	get(){
		return this._age;
	},
	set(value){
    	value>20&& (this._age=value) // 只接受大于20的值
	}
})
// 例3 数据源+中间变量 
let obj={age:0} 
let value=obj.age // 中间变量
     Object.defineProperty(obj,'age',{
		get(){
        	return value
		},
        set(newValue){
        	newValue>20 && (value=newValue)
		}
	})

问题:数据被篡改怎么办

  • 例一例二中的数据源 一旦被修改,【getter setter属性】也会受牵连 比如: obj._age=100 会影响obj.age; window._age=100 会影响obj.age
  • 例三中的中间变量 一旦被修改 value=999【getter setter属性】obj.age也会受牵连

使用代理 取消外部引用 防止数据篡改

  • 把数据源作为函数参数传入 不会在外部留下任何引用,确保了不可被访问和修改,以防止obj._age=100的操作

什么是代理?

  • 下面的 vm 叫做代理,因为 vm 全权代理一切关于 对象参数 的操作,对象参数 无法被操作(为了数据安全性的考虑),因此暴露出代理来操作
  • 下面这个例子基本上已经是 Vue数据响应式的绝大部分基本原理
let vm = proxy({data:{_age:0}}) // 1处 data获取到一个对象,代理数据源

// proxy函数的作用是新建一个对象 把数据源中的属性改为新对象中的【getter setter属性】
function proxy({data}){ 
	const obj = {}
    Object.defineProperty(obj,'age',{
		get(){
			return data._age
		},
        set(value){
        	value>20&& (data._age=value)
		}
	})
    return obj 
}

// 此时 vm 已全权拿到数据源的代理权,谁也无法篡改传递的参数!
console.log(vm.age) // 0 
vm.age=30 
console.log(vm.age) // 30 
现在,基本需求已满足,给函数直接传 数据源 已经能确保数据完全安全了;
那么,我们再提一个过分的要求吧!

作为一个框架,你总得让人把外部数据也能传进去吧!


// **********************************
let myData = {_age:0} // 外面定义好 数据源myData
let vm = proxy({data:myData}) // 把外部数据传进来!
// **********************************

function proxy({data}){ 
	const obj = {}
    Object.defineProperty(obj,'age',{
		get(){
			return data._age
		},
        set(value){
        	value>20&& (data._age=value)
		}
	})
    return obj 
}

console.log(vm.age) // 0
myData._age=999 
console.log(vm.age) // 999 
vm.age=1000
console.log(myData._age) // 1000
现在,一切都很好!
我们可以发现,外部数据和代理之间完全是互通的!
但是依旧有一点小缺点需要弥补!

你不觉得外部数据源每次都要设置下划线属性这样很麻烦吗?

Vue数据响应式的源码的完整思路!

终极需求如下:

  • 要允许使用外部数据源
  • 数据源与代理之间的数据要保持互通
  • 数据源和代理两者设置的属性名要完全一样的

终极需求的解决方案如下:

  • 使用函数的内部属性value作为中间变量 外部数据源<===> vm代理

let myData={age:0} // 数据源myData 
// 由于用到中间变量value,因此不用下划线属性 _age

let vm = proxy({data:myData}) // {data:{age:0}}
function proxy({data}){ 

	// ************start********
	let value=data.age 
    // 这一份内部属性 value很关键,这是 数据源myData.age <===> data.age 之间的桥梁
  
    
   	// 利用中间变量value 把myData.age这个普通属性重写为为【getter setter属性】 详见上述例三
    Object.defineProperty(data,'age',{ // data是一个对象{age:0}
		get(){
        	return value
		},
        set(newValue){
        	newValue>20 && (value=newValue)
		}
	})
    // ************end********
    
	const obj={}
    Object.defineProperty(obj,'age',{
		get(){
			return value
		},
        set(newValue){
        	newValue>20 && (value=newValue)
		}
	})
    return obj 
}

console.log(vm.age,myData.age) 

myData.age=999;
console.log(vm.age,myData.age) 

vm.age=1000;
console.log(vm.age,myData.age) 

总结如下

上述的讨论结果,

1.【setter getter属性】的作用相当于允许属性获取动态最新的结果

2. 我们知道了Vue作为一个框架之所以在new Vue中用户书写data的name属性
能够在调用this.name时让两边的名字保持一致是因为用到了中间变量nameValue;
同时中间变量value的存在还让数据源与代理之间保持了数据的互通

面试题 数据响应式原理的理解

Vue面试题