Vue响应式原理

327 阅读3分钟

下面是从视频课程中学习的总结:www.bilibili.com/video/BV1Zy…

一、Vue2响应式原理

(一)Object.defineProperty(obj, prop, descriptor)

  • 参数——obj:要定义属性的对象。 prop:要定义或修改的属性的名称或 Symbol 。 descriptor:要定义或修改的属性描述符。
  •  返回值 ——被传递给函数的对象。

通过这种方式定义的属性有以下优点:

1、更精细的控制定义的对象的属性

对象字面量定义的属性,enumerable、writable、configurable属性默认都是true。

var obj={
	name:"rectangle",
	width:10,
}
Object.defineProperty(obj,'height',{
	value:20
})
//可枚举enumerable
for (let key in obj){
	console.log("@",key)  //width被打印出,height没有打印出
}
//可修改writable
obj.width=20;                 //width被修改,height修改失败
//可删除configurable
delete obj.width;             //width被删除,height删除失败

通过Object.defineProperty()定义的属性,默认情况下,enumerable、writable、configurable都是为false,并且可以改变其取值。按以下方式定义的才和字面量定义的属性一样可遍历、修改、删除。

Object.defineProperty(obj,'height',{
	value:20,
	enumerable:true,
	writable:true,
	configurable:true
})

2、配置属性的getter和setter,使得属性可以与特定的变量绑定。

  • getter:当访问属性时,会调用此函数。这个方法返回一个值,这个值就是访问属性获得的值。

  • setter:当属性值被修改时,会调用此函数。这个方法有一个参数,这个参数就是属性修改后的值。

    let number=30; var obj={ name:"rectangle", width:10, } //height属性和number绑定,number修改时height修改,height修改时number修改 Object.defineProperty(obj,'height',{ get:function(){ console.log("height属性被读取"); return number; }, set:function(value){ console.log("height属性被设置"); number=value } }) obj.height; //输出30 obj.height=22; //obj.height和number都变成22

(二)什么是数据代理

数据代理:通过一个对象代理对另一个对象中属性的操作(读/写)。

下面的代码就是obj2代理obj1的x属性操作的例子。

let obj={x:100};
let obj2={y:200};
//通过obj2代理obj1的x属性的操作
Object.defineProperty(obj2,'x',{
	get:function(){
		return obj.x;
	},
	set:function(value){
		obj.x=value
	}
})

(三)Vue2的数据代理

Vue的数据代理:通过Vue实例对象(vm)来代理data对象(Vue2:_data属性,Vue3:$data属性)中的属性的操作(读/写)。数据代理的目的是为了存取数据方便。

1、通过Object.defineProperty()把data对象中所有属性添加到vm上;

2、为每一个添加到vm上的属性,都指定一个getter/setter;。

3、在getter/setter内部区操作(读/写)data中对应的属性。

二、Vue2数据监视(响应式原理)

研究data中的属性变化,页面中的内容也发生变化的内容。

(一)如何监视Vue中的数据变化

通过Object.defineProperty()对属性的读取、修改进行拦截(getter和setter方法)。Vue通过setter监视data中所有层次的对象的属性,也就是无论对象是作为数组的元素,还是作为对象的属性,只要是对象其属性都会被监视。下面是一个简单的实现对数据监视的例子。

let data={
	name:"Alice",
	age:20
}
//创建观察者类,接收一个类并对这个类的属性读取进行代理
function Observer(obj){
   for(let key in obj){
		Object.defineProperty(this,key,{
			get(){
				return obj[key]
			},
			set(val){
				obj[key]=val
			}
		})
   }
}
const obs=new Observer(data);
console.log(obs);
let vm={};
vm._data=data=obs;

但是Object.defineProperty()对数据拦截,下面两种场景拦截不到。

  • 存在新增属性、删除属性;
  • 直接通过下标修改数组,界面不会自动更新;

(二)怎么给对象新增响应式属性(被监视到)?

在Vue实例创建后,直接给data中的对象追加属性,是无法进行响应式处理的(没有setter方法)。想要后添加的属性做响应式,需要使用:Vue.set(target,propertyName,value) 或者

vm.$set(target,propertyName,value)

const vm = new Vue({
	el: '#app',
	data(){
		return {
			student:{
				name:'Alice',
				age:20
			}

		}
	},
	methods:{
		//new Vue()创建Vue实例之后,data中的对象直接添加属性是不显示在页面的
		addSignature(){
			this.student.signature="I like summer"
		},
		//new Vue()创建Vue实例之后,通过Vue.set()方法可以给data中的对象添加响应式属性,在页面显示
		addDesc(){
			Vue.set(this.student,"desc","I like winter")
		}
	},
	template:`
	<div>
		{{student.name}}-{{student.age}}
		<div v-if="student.signature">{{student.signature}}</div>
		<div><button @click="addSignature">无效添加属性</button></div>
		<div><button @click="addDesc">有效添加属性</button></div>
	</div>
	`
})

(三)如果对象的属性是数组,如何修改数组才能被监视到?

在Vue修改数组要想被监视到必须要用以下方法: 

  • 使用改变数组的API:push()、pop()、shift()、unshift()、splice()、sort()、reverse()。本质上Vue调用原生数组对应的方法对数组进行更新,重新解析模板,更新页面。
  •  Vue.set(target,index,value)或vm.$set(target,index,value)方法 

我们知道,数组作为对象的属性是被监视到的,但是数组值的每个item,是不会单独添setter进行监视的。

const vm = new Vue({
	el: '#app',
	data(){
		return {
			student:{
				friends:[
					{id:0,name:"Cindy"},
					{id:1,name:"Mike"}
				],
				hobby:['Singing','Swiming'],
			}

		}
	}
})

控制台输入vm.student,输出的内容如下:

可以看到friends和hobby作为data数据对象属性是被监视的(setter方法),但是hobby[0]或hobby[1]被没有被监视,friends[0]和friends[1]也没有没监视,但是friends[0]/friends[1]是对象,其里面的属性id和name是被监视的。这意味着

friends[0]={id:2,name:"May"} //这个方式修改数组是不被承认的

friends[0].name="John" //这个修改数组是被承认的

(四)Vue2响应式原理总结

对象类型:通过Object.defineProperty()对属性的读取、修改进行拦截(数据劫持) ;

数组类型:通过重写数组的一些列方法进行拦截(对数组的变更方法进行了包裹)。

存在问题: 

  • 新增属性、删除属性,界面不会自动更新。 解决方案:Vue.set()和Vue.delete()方法 。
  • 直接通过下标修改数组,界面不会自动更新。 解决方案:Vue.set()或者改变数组的方法。

这两个问题在Vue3都不存在了。本质上是因为Vue3使用Proxy对象取代Object.defineProperty()进行数据拦截。

三、Vue3数据监视(响应式原理)

(一)Proxy构造函数

创建一个对象代理,拦截对象任何属性变化,包括属性的读写、添加、删除等。

const p = new Proxy(target, handler)
  • target 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。(代理对象)
  • handler 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

官方文档:developer.mozilla.org/zh-CN/docs/…

使用Proxy对对象进行代理的例子如下,可以对代理对象的属性进行增删改查,还可以通过数组下标修改数组元素(p.side[0]=9成功改变数组)。

let rect={    name:"rectangle",    width:10,    side:[10,20]}
/*p代理react,即使第二个参数handler只是一个空对象,也能对rect进行增删改查,而
给handler添加get、set、deletePropert的方法,则可以拦截到对rect属性的增删改查,并改变页面。(响应式)
*/
let p=new Proxy(rect,{
    //访问属性时(p.width)调用
    get(target,property){
        console.log(`读取了p身上的${property}属性`)
        return rect[property];
    },
    //修改和新增属性时(p.width=11或p.height=11)都调用,p和rect都变了
    set(target,property,value){
        console.log(`修改了p身上的${property}属性`)
        rect[property]=value
    },
    //删除属性时调用(delte p.width),p和react都变了
    deleteProperty(target,property){
        console.log(`删除了p身上的${property}属性`)
        return delete target[property];
    }
})

对比Vue2数据拦截,如下所示,直接删除对象属性或添加对象属性是不生效的。即便配置上configurable:true,删除的也只是p身上的,rect身上的没有变。

let rect={    
	name:"rectangle",    
	width:10,
}
let p={};//每个属性都要用Object.defineProperty定义
Object.defineProperty(p,"width",{
    //访问属性时(p.width)调用    
	get(){        
		console.log(`读取了p身上的width属性`)        
		return rect.width;    
	},    
	//修改属性时(p.width=11或p.height=11)都调用    
	set(value){        
		console.log(`修改了p身上的width属性`)        
		rect.width=value;    
	},
})

(二)Reflect内置对象

内置对象,提供拦截Object属于语言内部的方法,这些方法与proxy handlers的方法相同。

obj={a:1,b:2}   
Reflect.get(obj,"a") //返回1
Reflect.set(obj,"a",11) //修改a属性值成功
Reflect.deleteProperty(obj,"b") //删除b属性成功

结合使用Reflect和Proxy对对象进行代理,代码如下

let p=new Proxy(rect,{
    //访问属性时(p.width)调用
    get(target,property){
        console.log(`读取了p身上的${property}属性`);
        return Reflect.get(rect,property);
        //return rect[property];
    },
    //修改和新增属性时(p.width=11或p.height=11)都调用,p和rect都变了
    set(target,property,value){
        console.log(`修改或新增了p身上的${property}属性`);
        Reflect.set(rect,property,value);
        //rect[property]=value
    },
    //删除属性时调用(delte p.width),p和react都变了
    deleteProperty(target,property){
        console.log(`删除了p身上的${property}属性`)
        //return delete target[property];
        return Reflect.deleteProperty(target,property)
    }
})