likes
comments
collection
share

架构的思考(3)

作者站长头像
站长
· 阅读数 4

之前的流程已经是对对象进行了监听、读、写,那数组也是数据,也需要被监听,那数组的监听和普通的监听有什么区别呢?那就慢慢试吧!

分析分析

  • 下标
function fn() {
  state[1]
}
fn()//【get】 1

读下标没问题。

  • length
function fn() {
  state.lenght
}
fn()//【get】 length

读length没问题

  • for 循环
function fn() {
  for(let i =0;i<state.length;i++){
    state[i]
  }
}

架构的思考(3) 遍历读到了length下标,没问题。依赖重复收集的事后面再说。

  • for of
 for (const item of state) {
  }

架构的思考(3) 到现在也没毛病。

还有什么呢? 数组的方法,那一大堆算不算读呢?

  • includes
function fn() {
  state.includes(1);
}

架构的思考(3)

现在分析一下这些收集对不对。includes是一个方法,如果将来给这个方法改成新的东西,会不会影响函数的运行结果?肯定会,所以应该被收集。length不用说了,现在判断1在不在数组了,数组长度变为0,那就直接影响函数了,所以应该被收集。0下标也应该收集。

  • lastIndexOf
function fn() {
  state.lastIndexOf(1);
}

架构的思考(3) 前面两个不用看了,那has应该不应该收集呢?它在内部做了一个判断,判断某个下标在这个数组里存不存在,那这个收集是有意义的,因为当稀疏数组的时候,下标是不存在的。

现在看起来数组的好像没啥了,但如果数组里面有对象呢?

const obj = {};
const arr = [1, {}, 3];
const state = reactive(arr);

function fn() {
  var i = state.indexOf(obj);
  
}

架构的思考(3) 结果是 -1,没找到?这是为什么呢?在调用方法的时候,是在源对象里查找还是代理对象里查找?很明显是代理对象啊,也就意味着这个方法里面的this指向的是代理对象。先输出来看看。

function fn() {
  var i = state.indexOf(obj);
  console.log('state[1]',state[1]);
  console.log('arr[1]',arr[1]);
}

架构的思考(3) 我们发现代理对象的那一项是个代理对象,因为我们之前有这么一个处理,当得到的结果是对象,那又进行一次响应式处理。

function get(target, key, receiver) {
  track(target, TrackOpTypes.GET, key); //依赖收集
  const result = Reflect.get(target, key, receiver); //返回对象属性值
  if (isObject(result)) {
    return reactive(result);
  }
  return result;
}

所以,当在代理对象里去查找的时候,找不到,因此我们可以有两个方案。

  • 把传入的对象转化为代理对象。
  • 当在代理对象里找不到时,再去原始数组里找一次。

这里vue官方使用了第二种。那我就来看看如何修改数组的方法

分析一下,通过依赖收集也发现了,在使用这个方法的时候,会掉进get陷阱,那就可以在get那里处理。

//handlers.js

const arrayInstrumentations = {
  includes: () => {},
  indexOf: () => {},
  lastIndexOf: () => {},
};

//读取
function get(target, key, receiver) {
  track(target, TrackOpTypes.GET, key); //依赖收集

  //如果是数组,且调用了数组方法
  if (arrayInstrumentations.hasOwnProperty(key) && Array.isArray(target)) {
   return arrayInstrumentations[key]
  }

  const result = Reflect.get(target, key, receiver); //返回对象属性值
  if (isObject(result)) {
    return reactive(result);
  }
  return result;
}

这就get里面的判断了,让它去执行我们修改过的数组方法。现在就是来修改数组的方法了,不过这些数组每一个的处理逻辑都一样。

const arrayInstrumentations = {};

["includes", "indexOf", "lastIndexOf"].forEach((key) => {
  arrayInstrumentations[key] = function (...args) {
    //1.正常查找 在原型上找
    //2.找不到 在原始对象上找
  };
});

共两个步骤,先正常找,找不到再在原始对象上找。

  1. 正常找

架构的思考(3) 原型上有数组的方法。

["includes", "indexOf", "lastIndexOf"].forEach((key) => {
  arrayInstrumentations[key] = function (...args) {
    //1.正常查找 在原型上找
    const res = Array.prototype[key].apply(this, args);
  };
});

现在是includes方法,所以res = false

  1. 在原始对象上找 其实还是执行上面的代码,不过需要修改this的指向,让它指向原始对象,那这里如何拿到原始对象呢?读属性是不是会进入get陷阱,而get陷阱里是不是有原始对象?那就好办了啊。例如:
 if (res < 0 || res === false) {
      Array.prototype[key].apply(this.fff, args); //读属性 触发`get`
    }
    
 //读取
function get(target, key, receiver) {
  console.log("key", key); //fff
  }

所以,先有一个特殊的属性名,让原始对象的方法去读。

["includes", "indexOf", "lastIndexOf"].forEach((key) => {
  arrayInstrumentations[key] = function (...args) {
    console.log("args", args);
    //1.正常查找 在原型上找
    const res = Array.prototype[key].apply(this, args);

    //找不到 在原始对象上找
    if (res < 0 || res === false) {
      return Array.prototype[key].apply(this[sy], args); //读属性 触发`get`
    }
    return res;
  };
});


//读取
function get(target, key, receiver) {
  if (key === sy) {
    return target;
  }
}

到这里就差不多了,下面来看看

分析分析写

在数组里有哪些会改动数组?最直接的就是改动下标了。

function fn() {
  state[0] = 4; // set 0
}

没毛病,那如果是超过数组的长度呢?

function fn() {
  state[5] = 4; // add 0
}

合理吧,但是不完整,整个数组的长度变了,其中有些是稀疏项。那长度变了怎么没触发 set length呢?官方文档说了,当设置的的下标大于数组的长度,那就会执行一个Object.defineProperty(obj,'length',value),这并没触发length属性,而是隐式修改,所以不会触发set的执行,所以我们得自己处理了。

得满足几个条件:

  • 设置的对象是一个数组。
  • 设置前后数组的length有变化。
  • 设置的不是length属性。

当三个条件都满足了,手动触发length属性的变化。

//修改
function set(target, key, value, receiver) {
  const type = target.hasOwnProperty(key)
    ? TriggerOpTypes.SET
    : TriggerOpTypes.ADD;

  const oldValue = target[key]; 
  const oldLen = Array.isArray(target) ? target.length : undefined; //获取旧数组长度

  const result = Reflect.set(target, key, value, receiver); 

  //赋值失败
  if (!result) {
    return result;
  }

  const newLen = Array.isArray(target) ? target.length : undefined;

  //当属性值发生变化 或 新增属性 时
  if (hasChange(oldValue, value) || type === TriggerOpTypes.ADD) {
    trigger(target, type, key); //派发更新

    //手动触发更新 set
    if (Array.isArray(target) && oldLen !== newLen) {
      if (key !== "length") {
        trigger(target, TriggerOpTypes.SET, "length");
      }
    }
  }
  return result;
}

修改数组下标是没问题,那看看直接修改数组的length。 当把length放大,得到的是一个稀疏数组,并且触发了set,数组原来值不变,没毛病。当把length缩小呢?触发了set,同时把数组后几项给干掉了,属性发生了改变,但没有触发delete啊。所以还是得手动触发。

 //手动触发更新 set
    if (Array.isArray(target) && oldLen !== newLen) {
      if (key !== "length") {
        trigger(target, TriggerOpTypes.SET, "length");
      } else {
        //找到哪些被删除的下标,依次触发配发更新
        for (let i = newLen; i < oldLen; i++) {
          trigger(target, TriggerOpTypes.DELETE, i.toString());
        }
      }
    }

在调用push方法时,派发更新是合理的,触发了add 3set length。但进行了两个依赖收集get pushget length。我的目的就是为了改动这个数组,去派发更新。我不需要知道内部是怎么实现的,就是我添加了,就要派发更新,但现在却进行了依赖收集,这超出了开发者的预期。

这就难搞了,数组变动的话我只想派发更新。那就有两种方法:

  • 把会对数组产生改动的方法全部重写
  • 调用这些会改动数组的方法期间,停止依赖收集。

vue使用的是第二种,因为第一种重写是完全的重写,太麻烦了哈。

["pop", "push", "shift", "unshift", "splice"].forEach((key) => {
  arrayInstrumentations[key] = function (...args) {
    pauseTracking(); //暂停依赖收集
    let res = Array.prototype[key].apply(this, args);
    resumeTracking(); //回复依赖收集
    return res;
  };
});
//effect.js

let shouldTrack = true;

export function pauseTracking() {
  shouldTrack = false;
}

export function resumeTracking() {
  shouldTrack = true;
}

export function track(target, type, key) {
  //停止依赖收集
  if (!shouldTrack) {
    return;
  }

  if (type === TrackOpTypes.INTERATE) {
    console.log(`【${type}】`);
    return;
  }
  console.log(`【${type}】`, key);
}

到现在为止,包括对象、数组的监听以及读和写,也就是差不多了。