【vue设计与实现】非原始值的响应式方案 8-数组的查找方法

之前我们说到,数组的方法内部其实都依赖了对象的基本语义,因此多数情况下,不需要特殊处理就能让方法按预期执行。就比如数组的includes方法,看下面代码:

const arr = reactive([1,2])

effect(()=>{
	console.log(arr.includes(1)) // 初始打印 true
})

arr[0] = 3 // 副作用函数重新执行,并打印false

这是因为includes方法为了找到给定的值,内部会访问数组的length属性以及数组的索引,因此修改某个索引值后能够触发响应。
但是includes方法并不总是按照预期工作,如下:

const obj = {}
const arr = reactive([obj])
console.log(arr.includes(arr[0])) 	// false

这里预期的结果是true,但是返回的却是false

这里我们通过查阅语言规范,了解includes方法的执行流程是怎样的。
通过阅读语言规范得知,arr.includes(arr[0])这段代码中,arr[0]得到的是一个代理对象,而在includes方法内部也会通过arr访问数组元素,从而得到一个代理对象,而问题在于这两个代理对象是不同的,这是因为每次调用reactive函数时都会创建一个新的代理对象

function reactive(obj){
	// 即使obj相同,还是会创建新的代理对象
	return createReactive(obj)
}

这个问题的解决方案如下:

// 定义一个Map实例,存储原始对象到代理对象的映射
const reactiveMap = new Map()

function reactive(obj){
	// 先通过原始对象obj寻找之前创建的代理对象,如果找到了直接返回已有的代理对象
	const existionProxy = reactiveMap.get(obj)
	if(existionProxy) return existionProxy 
	
	// 如果没找到,则创建新的代理对象
	const proxy = createReactive(obj)
	// 存储到Map中,帮助重复创建
	reactiveMap.set(obj, proxy)
	return proxy 
}

这样就避免了为同一个原始对象多次创建代理对象的问题。接下来再运行开头的代码就能得到预期的结果了

const obj = {}
const arr = reactive([obj])
console.log(arr.includes(arr[0])) 	// true

但是看下面的代码

const obj = {}
const arr = reactive([obj])
console.log(arr.includes(obj)) 	// false

这样直接把原始对象作为参数传递给includes,可是在数组中仍然找不到obj对象。其实原因很简单,因为includes内部的this指向的是代理对象arr, 并且在获取数组元素时得到的值也是代理对象,所以拿原始对象obj去查找肯定找不到。因此需要重写数组的includes方法并实现自定义的行为,才能解决这个问题。首先看下如何重现includes方法,如下面代码:

const arrayInstrumentations = {
	includes: function(){/*...*/}
}

function createReactive(obj, isShallow = false, isReadonly = false){
	return new Proxy(obj, {
		get(target, key, receiver){
			if(key === 'raw'){
				return target
			}
			// 如果操作的目标对象时数组,并且key存在于arrayInstrumentations上
			// 那么返回定义在arrayInstrumentations上的值
			if(Array.isArray(target) && arrayInstrumentations.hasOwnProperty(key)){
				return Reflect.get(arrayInstrumentations, key, receiver)
			}
			
			if(!isReadonly && typeof key !== 'symbol'){
				track(target, key)
			}
		
			const res = Reflect.get(target, key, receiver)
		
			if(!isShallow){
				return res
			}

			if(typeof res === 'object' && res !== null){
				return isReadonly?readonly(res):reactice(res)
			}
			
			return res
		}
	})
}

当执行arr.includes时,实际执行的是定义在arrayInstrumentations上的includes函数,这样就实现了重写

接下来来自定义includes函数:

const originMethod = Array.prototype.includes
const arrayInstrumentations = {
	includes: function(...args){
		// this是代理对象,现在代理对象中查找,将结果存储到res中
		let res = originMethod.apply(this, args)
		if(res === false){
			// res为false说明没有找到,通过this.raw拿到原始数组,再去其中查找并更新res值
			res = originMethod.apply(this.raw, args)
			
		}
		return res
	}
}

这样再运行下面的代码就能按预期执行了

const obj = {}
const arr = reactive([obj])
console.log(arr.includes(obj)) 	// true

除了includes方法外,还需要做类似处理的数组方法有indexOf和lastIndexOf,因为这些方法都是属于根据指定的值返回查找结果的方法,完整代码如下:

const arrayInstrumentations = {}
;['includes','indexOf','lastIndexOf'].forEach(method => {
	// this是代理对象,现在代理对象中查找,将结果存储到res中
	let res = originMethod.apply(this, args)
	if(res === false){
		// res为false说明没有找到,通过this.raw拿到原始数组,再去其中查找并更新res值
		res = originMethod.apply(this.raw, args)
		
	}
	return res
})

知识扩展
apply方法的作用
其作用有两个,跟它的入参有关。

  1. 改变this指向。
  2. 将数组入参变为一般入参。

详细可参考:https://blog.csdn.net/weixin_43877799/article/details/120282509

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值