Vue响应式原理基础——实时侦测Object类型的变化(未完待续)


前言

vue通过各个模块的协调配合,来侦测数据的变化,以便对使用数据的地方(以下简称依赖)进行响应式的更新:
在这里插入图片描述
本问将围绕下面的例子,来阐述实时侦测的基本原理:

vm.$watch('a,b,c',function(newValue,oldValue){
	//这里的代码可以时DOM操作,实时更新页面对vm.a.b.c有依赖的部分;
})
//上述代码的作用在于,当vm.a.b.c属性发生值变化时,调用相应的回调,

阅读本文,你或许应该具备的Js基础知识:

  • Js的访问器属性
  • 函数闭包的原理

一、vue如何实时的侦测数据变化?

vue如何实时的侦测数据变化,对于本文来说就是如何实时的侦测属性vm.a.b.c的变化。实质上,这是通过把对象的数据属性替换成同名的访问器属性来实现的,即通过Object.defineProperty(obj,key,{...});:

//对本文而言,创建一个同名的访问器属性:
Object.defineProperty(vm,'a,b,c',{
 enumerable : true,
 configurable : true,
 get function(){
  //读取该访问器属性时需执行的代码体;
 },
 set function(newValue){
  //试图写入该访问器属性时,需要执行的代码体;
 }
});
//于是,原有的数据属性被同名的访问器属性替代,即依然存在vm.a.b.c,但是该属性是新添加的访问器属性; 

但是,我们必须还得把原来的数据属性值保留下来,于是我们可以封装以上的代码:

function defineReactive(vm,'a,b,c',value){//value为数据属性vm.a.b.c的值;
 Object.defineProperty(vm,'a,b,c',{
 enumerable : true,
 configurable : true,
 get function(){
  //读取该访问器属性时需执行的代码体:
  return value; 
 },
 set function(newValue){   
  //试图写入该访问器属性时,需要执行的代码体:
  if( value === newValue ){
   return ;
  }
  value = newValue ;
 }
});
}
//set和get内引用了value,所以即使函数执行完毕,value也不会被回收,只要访问器属性还存在,它就会一直存在于内存中,和函数闭包的原理一致;

上述代码的意义在于,把数据属性用同名访问器属性所替代,并且,访问器属性有以下的特点:

  • 当读取时,执行get函数;
  • 当写入值时,执行set函数;
  • 类似于一个对特定行为(读或写)执行特定操作的执行器;

最重要的是前两个特点,可以起到以下的功能:

  • 依赖必定会读取数据(在本文中即读取vm.a.b.c)。而对访问器属性而言,读取数据就等同于执行get函数。于是我们可以利用这一点,在get函数体内收集相关的依赖。
  • 当我们试图写入数据时。同上,对访问器属性而言,写入数据等同于执行set函数。利用这一点,我们便可以在set函数体内,触发上面收集到的依赖(例如封装了DOM操作的函数),以便实时的更新相关的地方(例如页面的相关部分)。

二、如何收集依赖?

我们要在get中收集依赖,或许更准确的说我们在本文中所收集的依赖就是那个回调函数,它会执行某些操作(例如DOM操作),从而更新页面中与vm.a.b.c相关联的地方。最原始的方式是通过一个数组实现:
代码如下(示例):

function defineReactive(vm,'a,b,c',value){//value为数据属性vm.a.b.c的值;
	let dep = [];		//用于收集依赖
 	Object.defineProperty(vm,'a,b,c',{
 		enumerable : true,
	 	configurable : true,
 		get function(){
  		//读取该访问器属性时需执行的代码体:
  		dep.push(window.target);  //window.target被用于暂存依赖(本文中就是暂存回调函数);
  		return value; 
 		},
 		set function(newValue){ 
 		//试图写入访问器属性vm.a.b.c时,其实写入的值是作为set function的第一个参数;
  		//试图写入该访问器属性时,需要执行的代码体:
  		if( value === newValue ){
   			return ;
 		}
 		for(let i = 0 ; i < dep.length ; i++){
 			dep[i](newValue,value);	//当写入新值时,触发回调函数来执行相应的操作(例如DOM操作);
 		}
  		value = newValue ;
 		}
	});
}
//相当于函数闭包,dep在函数执行完后依然存在,它的生命周期即是vm.a.b.c的生命周期;

但是上述的操作有些许耦合,事实上我们可以把依赖收集的代码封装成一个 Dep类,它专门帮我们管理依赖。使用这个类,我们可以收集依赖,删除依赖或者向依赖发送通知(调用回调函数)。
代码如下(示例):

class Dep{
	constructor(){
		this.subs = [] ;     //用于存储依赖(回调函数); 
	}
	addSub (sub) {			//用于对数组添加依赖;
		this.subs.push(sub);
	}
	removeSub (sub) {
		remove(this.subs,sub);
	}
	depend () {
		if (window.target){
			this.addSub(window.target);
		}
	}
	notify () {
		const subs = this.subs.slice();
		for(let i = 0 , l = subs.length ; i < 1 ; i++){
			subs[i].update() ;     //update方法用于执行相应的操作,稍后定义;
		}
	}
}
function remove (arr , item){
	if(arr.length){
		const index = arr.indexOf(item);
		if( index > -1 ){
			return arr.splice(index,1);
		}
	}
}

function defineReactive(vm,'a,b,c',value){//value为数据属性vm.a.b.c的值;
	let dep = new Dep();		//用于收集依赖
 	Object.defineProperty(vm,'a,b,c',{
 		enumerable : true,
	 	configurable : true,
 		get function(){
  			//读取该访问器属性时需执行的代码体:
  			dep.depend() ;   //window.target被用于暂存依赖(本文中就是暂存回调函数);
  			return value; 
 		},
 		set function(newValue){ 
 			//试图写入访问器属性vm.a.b.c时,其实写入的值是作为set function的第一个参数;
  			//试图写入该访问器属性时,需要执行的代码体:
  			if( value === newValue ){
   				return ;
 			}
  			value = newValue ;
  			dep.notify();    
 		}
	});
}

2.读入数据

代码如下(示例):


总结

提示:这里对文章进行总结:

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值