一、数组的索引与length
1、问题一
1.1、问题
当前对数组进行读取和设置已经能满足要求
// 测试示例
let obj = reactive([1,2,3])
effect(() => {
console.log(obj[0])
})
obj[0] = 2;
//测试结果
1
2
2、 当数组进行设置,设置的索引值大于数组当前长度, 要命更新数组的length属性, 下面代码需要建立关联关系
// 测试示例
let obj = reactive([1,2,3])
effect(() => {
console.log(obj.length)
})
obj[4] = 1;
// 测试示例
3
1.2、解决方法(set和trigger都需要添加判断和处理)
set(target, key, newVal, receiver) {
if(isReadOnly) {
console.warn(`属性${key}是只读的`);
return true;
}
let oldValue = target[key];
// 当目标对象时数组并且检测被设置的索引值小于数组, 则视为ADD操作, 否则视为SET操作
let type = Array.isArray(target) ? Number(key) < target.length ? "SET" : "ADD" : Object.prototype.hasOwnProperty.call(target, key) ? "SET": "ADD";
let res = Reflect.set(target, key, newVal, receiver)
if(target === receiver.raw){
if(oldValue !== newVal && (oldValue === oldValue || newVal === newVal)){
trigger(target, key, type);
}
}
return res
},
function trigger(target, key, type) {
let depsMap = bucket.get(target);
if(!depsMap) return;
let effectsToRun = new Set();
let effects = depsMap.get(key);
effects && effects.forEach(effectFn => {
if(effectFn !== activeEffect){
effectsToRun.add(effectFn);
}
})
// 当操作类型为ADD 并且目标对象时数组的时候, 应该取出并执行与length属性相关的副作用函数
if(type === "ADD" && Array.isArray(target)) {
let lengthEffects = depsMap.get("length");
lengthEffects && lengthEffects.forEach(effectFn => {
if(effectFn !== activeEffect){
effectsToRun.add(effectFn);
}
})
}
let iterateEffects = depsMap.get(ITERATE_KEY);
if(type === "ADD" || type === "DELETE") {
iterateEffects && iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
})
}
effectsToRun.forEach(effectFn => {
if(effectFn.options.scheduler){
effectFn.options.scheduler(effectFn);
} else {
effectFn();
}
});
}
2、问题二
2.1问题
修改数组的length值也会影响数组元素,下面示例需要建立示例
// 测试示例
let obj = reactive([1,2,3])
effect(() => {
console.log(obj[2])
})
obj.length = 1;
// 测试结果
3
2.2、解决方法(set和trigger都需要添加判断和处理)
set(target, key, newVal, receiver) {
if(isReadOnly) {
console.warn(`属性${key}是只读的`);
return true;
}
let oldValue = target[key];
let type = Array.isArray(target) ? Number(key) < target.length ? "SET" : "ADD" : Object.prototype.hasOwnProperty.call(target, key) ? "SET": "ADD";
let res = Reflect.set(target, key, newVal, receiver)
if(target === receiver.raw){
if(oldValue !== newVal && (oldValue === oldValue || newVal === newVal)){
// 给trigger函数添加第四个参数, 即触发响应的新值
trigger(target, key, type, newVal);
}
}
return res
},
// 触发函数
function trigger(target, key, type, newVal) {
let depsMap = bucket.get(target);
if(!depsMap) return;
let effectsToRun = new Set();
let effects = depsMap.get(key);
effects && effects.forEach(effectFn => {
if(effectFn !== activeEffect){
effectsToRun.add(effectFn);
}
})
// 如果操作目标时数组, 并且修改了数组的length
// 对应索引大于或等于新的length值得元素
// 需要把所有相关联的副作用函数取出并添加到effectsToRun中执行
if(Array.isArray(target) && key === "length") {
depsMap.forEach((effects, key) => {
if(key >= newVal) {
effects && effects.forEach(effectFn => {
if(effectFn !== activeEffect){
effectsToRun.add(effectFn);
}
})
}
})
}
if(type === "ADD" && Array.isArray(target)) {
let lengthEffects = depsMap.get("length");
lengthEffects && lengthEffects.forEach(effectFn => {
if(effectFn !== activeEffect){
effectsToRun.add(effectFn);
}
})
}
let iterateEffects = depsMap.get(ITERATE_KEY);
if(type === "ADD" || type === "DELETE") {
iterateEffects && iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
})
}
effectsToRun.forEach(effectFn => {
if(effectFn.options.scheduler){
effectFn.options.scheduler(effectFn);
} else {
effectFn();
}
});
}
// 测试示例
let obj = reactive([1,2,3])
effect(() => {
console.log(obj[0])
})
// 测试结果
obj.length = 2;
// 1
-
这里修改的length值, 所有对应触发length响应的响应函数
-
为什么上面关联了索引值为0的响应函数, 但是不会触发, 因为这里对应触发length的响应函数,索引相关的方法是在判断条件
if(Array.isArray(target) && key === "length")符合后才额外添加的 -
depsMap.forEach((effects, key) => {...}中的key值时包含length,let effects = depsMap.get(key)中也包含的length, 为啥不怕effectsToRun重复添加的响应函数, 一方面,key >= newVal用于为false, 因为length为转成了NaN, 所以key >= newVal的结果永远为false, 另一方面, effectsToRun是个set方法, 本身就含有去重功能
二、遍历数组
1、for...in
数组对象和常规对象的不同仅体现在[[DefineOwnProperty]] 上, 使用for..in循环遍历数组和遍历常规对象并无差异, 之前的代理方法同样可用
// 对应for(ley key in obj)
ownKeys(target) {
// const ITERATE_KEY = new Symbol()
// 因为for...in没有对应的key, 所以通过Symbol来创建唯一的key建立对应的响应关系
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
}
影响对象for ... in 的原因: 添加属性值, 删除属性值"ADD", "DELETE", 对应的trigger中代码如下如下
let iterateEffects = depsMap.get(ITERATE_KEY);
if(type === "ADD" || type === "DELETE") {
iterateEffects && iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
})
}
对于数组,影响for..in的原本的原因
- 添加新元素: arr[100] = “bar”;
- 修改元素长度: arr.length = 0;
总结其本质都是修改了length属性
// 对应for(ley key in obj)
ownKeys(target) {
track(target, Array.isArray(target) ? "length" : ITERATE_KEY)
return Reflect.ownKeys(target)
}
2、for... of
for...of是用来遍历可迭代对象的
**可迭代对象:**如果一个对象实现了Symbol.iterator方法, 那么这个对象就是可迭代的
let obj1 = {
val : 0,
[Symbol.iterator](){
return {
next() {
return {
value: obj1.val ++,
done : obj1.val > 2
}
}
}
}
}
// 执行方法
for(let item of obj1) {
console.log(item)
}
数组可以用下面方法进行迭代
let arr = [1, 2]
let itr = arr[Symbol.iterator]()
console.log(itr.next()) // {value: 1, done: false}
console.log(itr.next()) // {value: 2, done: false}
console.log(itr.next()) // {value: undefine, done: true}
根据javaScript规范可以知道, 数组的迭代器执行会读取数组的length属性, 下面时数组for .. of的伪实现
let arr = [1,2]
arr[Symbol.iterator] = function() {
return {
let target = this;
let length = target.length;
let idx = 0;
next() {
return {
value: idx < len ? target[length] : undefine
done: idx++ >= len
}
}
}
}
所以在for...of中, 只需要在副作用函数与与数组的长度与索引之间建立响应关系, 当前reactive就能满足要求
// 测试示例
let obj = reactive([1,2,3, 7, 1, 10, 23])
effect(() => {
for(let key of obj) {
console.log(key)
}
})
obj.length = 2;
// 测试结果
--start--
1
2
3
--start--
1
需要注意的问题: 副作用函数与Symbol.iterator这类的symbol值之间不需要建立响应关系, 为了性能考虑和避免发生意外错误
get(target, key, receiver) {
if(key === "raw") return target;
// 这里添加判断
if(!isReadOnly && typeof key !== "symbol") {
track(target, key);
}
let res = Reflect.get(target, key, receiver);
if(isShallow) return res;
if(typeof res === "object" && res !== null) {
return isReadOnly ? readonly(res) : reactive(res);
}
return res;
},
三、数组的查找方法
数组的方法内部基本都依赖了对象的基本语义, 多数情况下不需要做特殊处理即可让这些方法按照预期工作, 像includes, 内部会访问数组的length和索引, 因此修改某个索引只想的元素值后能够触发响应
// 测试示例
let arr = reactive([1,2])
effect(() => {
console.log(arr.includes(1))
})
arr[0] = 2;
// 测试结果
true
false
但是对于查找对象, 可能就会存在问题
1、查找对象代理的值(隐藏的代理)
// 测试示例
let arr = reactive([{}])
console.log(arr.includes(arr[0]))
// 测试结果
false
原因
if(typeof res === "object" && res !== null) {
return isReadOnly ? readonly(res) : reactive(res);
}
- includes为了查找, 会通过索引读取数组元素的值, 当查找结果是个对象的时候, 会被封装成代理对象返回
- arr[0],通过索引访问数组的值, arr[0]的结果是个对象, 同样会被被封装成代理对象返回
- 每次封装的代理对象, createReactive返回的结果时不相等的
解决方法
let reactiveMap = new Map();
function reactive(data) {
let existionProxy = reactiveMap.get(data);
if(existionProxy) return existionProxy
let proxy = createReactive(data)
reactiveMap.set(data, proxy)
return proxy;
}
2、查找对象没有被代理的值
// 测试示例
let obj = {}
let arr = reactive([obj])
console.log(arr.includes(obj))
// 测试结果
false
原因
includes为了查找, 会通过索引读取数组元素的值, 当查找结果是个对象的时候, 会被封装成代理对象返回,
这里includes中查找的是一个原始的对象, 代理对象和原始对象对象并不相等
解决方法: 重写include事件, 设计proxy中get方法的修改
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);
}
let res = Reflect.get(target, key, receiver);
if(isShallow) return res;
if(typeof res === "object" && res !== null) {
return isReadOnly ? readonly(res) : reactive(res);
}
return res;
},
其中arrayInstrumentations对象如下
const arrayInstrumentations = {
includes: function (...args) {
let originMethod = Array.prototype.includes;
let res = originMethod.apply(this, args) // 这里的this是虚拟对象, 因为方法是通过Reflect.get(.., receiver)调用的
if(res === false) {
res = originMethod.apply(this.raw, args) // 如果没找到, 则通过this.raw拿到原始数组再去比对
}
return res;
}
}
除了includes外, indexof和lastIndexOf都需要同样处理
const arrayInstrumentations = {};
["includes", "indexOf", "lastIndexOf"].forEach(method => {
arrayInstrumentations[method] = function(...args) {
let originMethod = Array.prototype[method];
let res = originMethod.apply(this, args) // 这里的this是虚拟对象, 因为方法是通过Reflect.get(.., receiver)调用的
if(res === false || res === -1) {
res = originMethod.apply(this.raw, args) // 如果没找到, 则通过this.raw拿到原始数组再去比对
}
return res;
}
})
四、隐式修改数组长度的原型方法
push、pop、shift、unshift、splice都会隐形修改数组长度, 下面入push方法为例, 下面会造成无限循环, 栈溢出的情况
const arr = reactive([])
// 第一个副作用函数
effect(() => {
arr.push(1);
})
// 第二个副作用函数
effect(() => {
arr.push(1);
})
原因:
- 执行函数一, 读取length, 修改length, 由于trigger中
effectFn !== activeEffect,不会有影响 - 执行函数二 读取length, 修改length, 其实length中关联的副作用函数一和函数二, 执行函数一,
- 函数一再次执行, length中关联的副作用函数一和函数二, 执行函数二
- 循环往复, 栈溢出
解决方法:
- 在arrayInstrumentations添加对象方法
- 添加标记shouldTrack
let shouldTrack = true;
["push", "pop", "push", "shift", "unshift"].forEach(method => {
let originMethod = Array.prototype[method];
arrayInstrumentations[method] = function(...args) {
shouldTrack = false;
let res = originMethod.apply(this, args)
shouldTrack = true;
return res;
}
})
- 修改track方法
function track(target, key) {
if(!activeEffect || !shouldTrack) return;
let depsMap = bucket.get(target);
if(!depsMap) bucket.set(target, depsMap = new Map());
let deps = depsMap.get(key);
if(!deps) depsMap.set(key, deps = new Set())
deps.add(activeEffect);
activeEffect.deps.push(deps);
}
当标记变量 shouldTrack 的值为 false 时,即禁止追踪时,track 函数会直接返回。这样,当 push 方法间接读取length 属性值时,由于此时是禁止追踪的状态,所以 length 属性与副作用函数之间不会建立响应联系, 但是push方法会设置length属性, 所以当执行push方法, 会触发与length相关的响应函数的执行
// 测试示例
const arr = reactive([1, 2])
effect(() => {
console.log(arr.length)
})
effect(() => {
arr.push(4);
})
// 测试结果
2
3