深入浅出vue.js----变化侦测----Array的变化侦测

  • 为什么Array和Object的侦测方式不同

object通过getter/setter来实现侦测,但数组中有许多方法,如push来改变数组,但它并不会触发getter/setter。正因为我们可以通过Array原型上的方法来改变数组的内容,所有object那种通过getter/setter的实现方式就行不通了。

一、如何追踪变化

(1)可以用自定义的方法去覆盖原生的原型方法

(2)可以使用一个拦截器覆盖Array.prototype。之后,每当使用Array原型上的方法操作数组时,其实执行的都是拦截器中提供的方法,比如push方法。然后,在拦截器中使用原生Array的原型方法去操作数组。

二、拦截器

(1)拦截器其实就是一个和Array.prototype一样的object,里面包含的属性一摸一样,只不过这个object中某些可以改变数组自身内容的方法是我们处理过的。

(2)Array原型中可以改变数组自身内容的方法有7个,分别是push、pop、shift、unshif、splice、sort和reverse。

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto)

;[
	'push',
	'pop',
	'shift',
	'unshift',
	'splice',
	'sort',
	'reverse'
]
.forEach(function(method){
	<!-- 缓存原始方法 -->
	const original = arrayProto[method];
	Object.defineProperty(arrayMethods,method,{
		value:function mutator(...args){
			return original.apply(this,args)
		},
		enumerable:false,
		writable:true,
		configurable:true
	})
})

1、创建了变量arrayMethods,它继承自Array.prototype,具备其所有功能。未来,要使用arrayMethods去覆盖Array.prototype。

2、在arrayMethods上使用Object.defineProperty方法将那些可以改变数组自身内容的方法(push、pop、shift、unshif、splice、sort和reverse)进行封装。

3、当使用push方法的时候,其实调用的是arrayMethods.push,而arrayMethods.push是函数mutator,也就是说,实际上执行的是mutator函数。

3、在mutator中执行original(它是原生Array.prototype上的方法,例如Array.push)来做它应该做的事,比如push的功能。

4、因此可以在mutator函数中做一些其他的事,比如发送变化通知。

三、使用拦截器覆盖Array原型

(1)有了拦截器之后,想要让它生效,就需要使用它去覆盖Array.prototype。但是又不能直接覆盖,因为这样回污染全局的Array。我们希望拦截器操作只针对那些被侦测了变化的数据生效,也就是说希望拦截器只覆盖那些响应式数组的原型。

(2)而将一个数据转换成响应式的,需要通过Observer,所以只需在Observer中使用拦截器覆盖那些即将被转换成响应式Array类型数据的原型就好了。

export class Observer{
	constructor(value){
		this.value = value;
		if(Array.isArray(value)){
			value._proto_ = arrayMethods;//新增
		}else{
			this.walk.(value)
		}
	}
}

1、value._proto_ = arrayMethods的作用是将拦截器(加工后具备拦截功能的arrayMethods)赋值给value._proto_,通过_proto_可以很巧妙地实现覆盖value原型的功能。

2、_proto_其实是Objec.getPrototypeOf和Object.setPrototypeOf的早期实现,所以使用ES6的Object.setPrototypeOf来代替_proto_完全可以实现相同的效果。

四、将拦截器方法挂载到数组的属性上

(1)虽然绝大多数浏览器都支持这种非标准的属性(EX6之前并不是标准)来访问原型,但并不是所有浏览器都支持!,因此需要处理不能使用_proto_的情况。

(2)Vue的做法非常粗暴,如果不能使用_proto_,就直接将arrayMethods身上的这些方法设置到被侦测的数组上。

export class Observer{
	constructor(value){
		this.value = value;
		if(Array.isArray(value)){
			const augment = hasProto
			?protoAugment
			:copyAugment
			augment(value,arrayMethods,arrayKeys);
		}else{
			this.walk.(value)
		}
	}
	function protoAugment(target,src,keys){
		target._proto_ = src
	}
	function copyAugment(target,src,keys){
		for(let i=0,l=keys.length;i<l;i++){
			const key = keys[i];
			def(target,key,src[key])
		}
	}
}

1、新增了hasProto来判断当前浏览器是否支持_proto_。

2、新增了copyAugment函数,用来将已经加工了拦截操作的原型方法直接添加到value的属性中。

3、在浏览器不支持_proto_的情况下,会在数组上挂载一些方法。当用户使用这些方法时,其实执行的并不是浏览器原生提供的Array.prototype上的方法,而是拦截器中提供的方法。因为当访问一个对象的方法时,只有其自身不存在这个方法,才会去它的原型上找这个方法。

五、如何收集依赖

(1)数组在getter中收集依赖,在拦截器中触发依赖。

(2)Array的依赖和Object一样,也在defineReactive中收集。

function defineReactive(data,key,val){
        if(typeof val==='object'){
            new Observer(val)
        }
        let dep = new Dep();
        Object.defineProperty(data,key,{
            enumerable:true,
            get:function(){
                dep.depend()
                //这里收集Array的依赖
                return val
            }
            set:function(newVal){
                if(val==newVal){
                    return;
                }
                val = newVal;
                dep.notify();
        })
}

新增一段注释,接下来要在这个位置去收集Array的依赖。

六、依赖列表存在哪儿

(1)Vue.js把Array的依赖存放在Observer中

export class Observer{
	constructor(value){
		this.value = value;
        //新增dep
        this.dep = new Dep();
		if(Array.isArray(value)){
			const augment = hasProto
			?protoAugment
			:copyAugment
			augment(value,arrayMethods,arrayKeys);
		}else{
			this.walk.(value)
		}
	}
	function protoAugment(target,src,keys){
		target._proto_ = src
	}
	function copyAugment(target,src,keys){
		for(let i=0,l=keys.length;i<l;i++){
			const key = keys[i];
			def(target,key,src[key])
		}
	}
}

(2)数组在getter中收集依赖,在拦截器中触发依赖,所以这个依赖保存的位置很关键,它必须在getter和拦截器中都可以访问到。

(3)之所以将依赖保存在Observer实例上,是因为在getter中可以访问到Observer实例,同时在Array拦截器中也可以访问到Observer实例。

七、收集依赖

(1)把Dep实例保存在Obeserver的属性上后,可以在getter中像下面这样访问并收集依赖

function defineReactive(data,key,val){
	<!-- 修改 -->
	let childOb = observe(val);
	let dep = new Dep();
	Object.defineProperty(data,key,{
		enumerable:true,
		get:function(){
			dep.depend();
			<!-- 新增 -->
			if(childOb){
				childOb.dep.depend()
			}
			return val
		}
		set:function(newVal){
			if(val==newVal){
				return;
			}
			val = newVal;
			dep.notify();
            }
	})
}
<!-- 尝试为value创建一个Observer实例,
如果创建成功,直接返回新创建的Observer实例。
如果value已经存在一个Observer实例,则直接返回它 -->
export function observe(value,asRootData){
	if(!isObject(value)){
		return
	}
	let ob;
	if(hasOwn(value,'_ob_')&&value._ob_ instanceof Observer){
		ob = value._ob_
	}else{
		ob = new Observer(value);
	}
	return ob;
}

1、新增了函数observe,它尝试创建一个Observer实例。如果value已经是响应式数据了,不需要再次创建Observer实例,直接返回已经创建的Observer实例即可,避免了重复侦测value变化的问题。

2、在defineReactive函数中调用了observe,它把val当作参数传了进去并拿到一个返回值,那就是Observer实例。

3、defineReactive函数中的val很有可能会是一个数组。通过observe我们得到了数组的Observer实例(childOb),最后通过childOb的dep执行depend方法来收集依赖。

4、通过这种方式,我们就可以实现在getter中将依赖收集到Observer实例的Dep中。更通俗的解释是:通过这样的方式可以为数组收集依赖。

八、在拦截器中获取Observer实例

(1)因为Array拦截器是对原型的一种封装,所以可以在拦截器中访问到this(当前正在被操作的数组)。

(2)而dep保存在Observer中,所以需要在this上读到Observer的实例

<!-- 工具函数 -->
function def(obj,key,val,enumerable){
	Object.defineProperty(obj,key,{
		value:val,
		enumerable:!!enumerable,
		writable:true,
		configurable:true
	})
}

export class Observer{
	constructor(value){
		this.value = value;
		this.dep = new Dep();
		<!-- 新增 -->
		def(value,'_ob_',this)
	}
	if(Array.isArray(value)){
		const augment = hasProto
		?protoAugment
		:copyAugment
		augment(value,arrayMethods,arrayKeys);
	}else{
		this.walk.(value)
	}
	。。。
}

1、在Observer上新增了一段代码,它可以在value上新增一个不可枚举的属性_ob_,这个属性的值就是当前Observer的实例。

2、_ob_的作用不仅仅是为了在拦截器中访问Observer实例,还可以用来标记当前value是否被Observer转换成了响应式数据。

3、所有被侦测了变化的数据身上都会有一个_ob_属性来表示它们是响应式的。

4、当value身上被标记了_ob_之后,就可以通过value._ob_来访问Observer实例。如果是Array拦截器,因为拦截器是原型方法,所以可以直接通过this._ob_来访问Observer实例。

[
	'push',
	'pop',
	'shift',
	'unshift',
	'splice',
	'sort',
	'reverse'
]
.forEach(function(method){
	<!-- 缓存原始方法 -->
	const original = arrayProto[method];
	Object.defineProperty(arrayMethods,method,{
		value:function mutator(...args){
			<!-- 新增 -->
			const ob = this._ob_;
			return original.apply(this,args)
		},
		enumerable:false,
		writable:true,
		configurable:true
	})
})

在上面代码中,我们在mutator函数里通过this._ob_来获取Observer实例。

九、向数组的依赖发送通知

(1)当侦测到数组发生变化时,会向依赖发送通知。只需要在Observer实例中拿到dep属性。然后直接发送通知就可以了。

[
	'push',
	'pop',
	'shift',
	'unshift',
	'splice',
	'sort',
	'reverse'
]
.forEach(function(method){
	<!-- 缓存原始方法 -->
	const original = arrayProto[method];
	def(arrayMethods,method,function mutator(...args){
			<!-- 新增 -->
			const ob = this._ob_;
            //向依赖发送通知
            ob.dep.notify()
			return original.apply(this,args)
	}
}

十、侦测数组中元素的变化

(1)侦测数组的变化,指的是数组自身的变化,比如是否新增一个元素,是否删除一个元素。其实数组中保存了一些元素,它们的变化也是需要侦测的。比如,当数组中object身上某个属性的值发生了变化时,也需要发送通知。

(2)如果用户使用push往数组中新增了元素,这个新增元素的变化也需要侦测。

(3)即所有响应式数据的子数据都要侦测,不论是object中的数据还是Array中的数据。

(4)在Observer中新增一些处理,让它可以将Array也转换成响应式的。

export class Observer{
	constructor(value){
		this.value = value;
		def(value,'_ob_',this)
		if(Array.isArray(value)){
			this.observeArray(value)
		}else{
			this.walk.(value)
		}
	}
	<!-- 侦测Array中的每一项 -->
	observeArray(items){
		for(let i =0,l=items.length;i<l;i++){
			observe(items[i])
		}
	}
}

1、新增了observeArray方法,其作用是循环Array中的每一项,执行observe函数来侦测变化。

2、observe函数,其实就是将数组中的每个元素都执行一遍new Observer,这很明显是一个递归的过程。

3、现在只要将一个数据丢进去,Observer就会把这个数据的所有子数据转换成响应式的。

十一、侦测新增元素的变化

(1)数组中有一些方法是可以新增数组内容的,比如push,而新增的内容也需要转换成响应式来侦测变化,否则会出现修改数据时无法触发消息等问题。因此,我们必须侦测数组中新增元素的变化。

(2)实现:只要能获取新增的元素并使用Observer来侦测它们就行。

(3)获取新增元素

1、想要获取新增元素,我们需要在拦截器中对数组方法的类型进行判断。如果操作数组的方法是push、unshift和splice(可以新增数组元素的方法),则把参数中新增的元素拿过来,用Observer来侦测。

2、通过switch对method进行判断,如果method是push、unshift和splice这种新增元素的方法,那么从args中将新增元素取出来,暂存在inserted中。

3、接下来要使用Observer把inserted中的元素转换成响应式的。

(4)使用Observer侦测新增元素

1、Observer会将自身的实例附加到value的_ob_属性上。所有被侦测了变化的数据都有一个_ob_属性,数组元素也不例外。

2、因此,我们可以在拦截器中通过this访问到_ob_,然后调用_ob_上的observeArray方法就可以了

;[
	'push',
	'pop',
	'shift',
	'unshift',
	'splice',
	'sort',
	'reverse'
]
.forEach(function(method){
	<!-- 缓存原始方法 -->
	const original = arrayProto[method];
	def(arrayMethods,method,function mutator(...args){
		    const ob = this._ob_;
			const result = original.apply(this,args);
			let inserted;
			switch(method){
				case:'push':
				case:'unshift':
				    inserted = args
				    break;
				case:'splice':
				    inserted = args.slice(2);
				    break;
			}
			if(inserted) ob.observeArray(inserted);
			ob.dep.notify()
			return result;
	})
})

3、从this._ob_上拿到Observer实例后,如果有新增元素,则使用ob.observeArray来侦测这些新增元素的变化。

十二、关于Array的问题

(1)Array的变化侦测是通过拦截原型的方式实现的。正是因为这种实现方式,其实有些数组操作Vue.js是拦截不到的。例如

  • this.list[0] = 2;

修改数组第一个元素的值时,无法侦测到数组的变化,所以不会触发re-render或watch等。

  • this.list.length = 0;

这个清空数组操作也无法侦测到数组的变化,所以也不会触发re-render或watch等。

十三、总结

(1)Array追踪变化的方式和object不一样。因为它是通过方法来改变内容的,所以我们通过创建拦截器去覆盖数组原型的方法来追踪变化。

(2)为了不污染全局Array.prototype,我们在Observer中只针对那些需要侦测变化的数组使用_proto_来覆盖原型方法,但_proto_在ES6之前并不是标准属性,不是所有浏览器都支持它。因此,针对不支持_proto_属性的浏览器,我们直接循环拦截器,把拦截器中的方法直接设置到数组身上来拦截Array.prototype上的原生方法。

(3)Array收集依赖的方式和object一样,都是在getter中收集。但是由于使用依赖的位置不同,数组要在拦截器中向依赖发消息,所以依赖不能像Object那样保存在defineReactive中,而是把依赖保存在了Observer实例上。

(4)在Observer中,我们对每个侦测了变化的数据都标上了印记_ob_,并把this(Observer实例)保存在_ob_上。这主要有两个作用,一方面是为了标记是否被侦测了变化(保证同一个数据只被侦测一次),另一方面可以很方便得通过数据取到_ob_,从而拿到Observer实例上保存的依赖。当拦截到数组发生变化时,向依赖发送通知。

(5)除了侦测数组自身的变化外,数组中元素发生的变化也要侦测。我们在Observer中判断如果当前被侦测的数据是数组,则调用oberveArray方法将数组中每一个元素都转换成响应式的并侦测变化。

(6)除了侦测已有数据外,当用户使用push等方法向数组中新增数据时,新增的数据也要进行变化侦测,我们使用当前操作数组的方法来判断,如果是push、unshift和splice方法,则从参数中将新增数据提取出来,然后使用observeArray对新增数据进行变化侦测。

(7)由于在ES6之前,JavaScript并没有提供元编程的能力,所以对于数组类型的数据,一些语法无法追踪到变化,只能拦截原型上的方法,而无法拦截数组特有的语法。例如使用length清空数组的操作就无法拦截。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值