《深入浅出Vue.js》阅读笔记(二)Array的变化侦测

不看源码还不知道Vue的变化侦测居然还分Object和Array。很多地方还是不太理解,通过自己写一遍梳理下思路。

Array由于可以通过其原型上的方法来改变数组的内容,因此与Object不同,不会触发getter/setter。

1 如何追踪变化

既然数组可以用原型上的方法改变内容,那我们就可以对原型方法进行一些改造,来实现和Object一样的效果。

用一个拦截器覆盖原生的原型方法,之后,看似使用的是原型上的方法在操作数组,实质却是拦截器中的方法。

2 实现拦截器

既然要覆盖原型方法,则必然要先继承上面的方法,再对它进行改造。

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
	})
})

当使用push方法时,实际调用的是arrayMethods.push,而arrayMethods.push是函数mutator,最后在mutator中执行original来进行push

3 使用拦截器覆盖Array原型

拦截器实现完了,就要用来覆盖Array原型,但不能直接覆盖,会污染全局的Array。

从上一章知道数据是通过Observer转换成响应式的,所以只需要在Observer中使用拦截器覆盖那些即将被转换成响应式Array类型数据的原型

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

4 将拦截器方法挂载到数据属性上

不是所有浏览器都支持使用_proto_来访问原型,因此需要处理不能使用的情况,粗暴的做法------将arrayMethods设置到被侦测的数组上

// 判断_proto_是否可用
const hasProto = '_proto_' in {}
const arraykeys = Object.getOwnPropertyNames(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])
		}
	}
}

如果浏览器支持_proto_,则使用protoAugment函数来覆盖原型,如果不支持,则调用copyAugment。

5 如何收集依赖

在getter中收集依赖,在拦截器中触发依赖

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();
        })
}

6 依赖存放在哪里

为了满足在getter和拦截器中都可以访问到依赖,因此把依赖保存在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)
		}
	}
	.......
}

7 收集依赖

新增一个函数observe,它尝试创建一个Observer实例。

function defineReactive(data,key,val){
	let childOb = observer(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;
			}
                    dep.notify();
                    val = newVal;	
                }
	})
}
<!-- 尝试为value创建一个Observer实例,
如果创建成功,直接返回新创建的Observer实例。
如果value已经存在一个Observer实例,则直接返回它 -->
export function observer(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;
}

8 在拦截器中获取Observer实例

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)
	}
	....
}

_ob_属性的值即当前Observer的实例

9 向数组的依赖发送通知

在Observer实例中拿到dep属性,调用ob.dep.notify()去通知依赖(Watcher)数据发生了改变

[
	'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)
	}
}

总结:

Vue无法侦测到数组的以下变化:

  • 直接通过索引设置某个值  this.list[0] = 2
  • 修改数组的长度  this.list.length = 0

这是因为数组的变化侦测是通过其原型上的方法来实现的,包括push、pop、shift、unshift、splice、sort、reverse,只能侦测通过这七种方法而造成的数据变化。

解决方法:

  • 使用vue.$set(),手动通知vue完成观察(推荐这种方法)
  • 将这两种变动换成原型上的七种方法实现

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值