第五章-非原始值地响应式方案(数组)

85 阅读7分钟

一、数组的索引与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