架构的思考(3)

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

分析分析

  • 下标
js 复制代码
function fn() {
  state[1]
}
fn()//【get】 1

读下标没问题。

  • length
js 复制代码
function fn() {
  state.lenght
}
fn()//【get】 length

读length没问题

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

遍历读到了length下标,没问题。依赖重复收集的事后面再说。

  • for of
js 复制代码
 for (const item of state) {
  }

到现在也没毛病。

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

  • includes
js 复制代码
function fn() {
  state.includes(1);
}

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

  • lastIndexOf
js 复制代码
function fn() {
  state.lastIndexOf(1);
}

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

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

js 复制代码
const obj = {};
const arr = [1, {}, 3];
const state = reactive(arr);

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

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

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

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

js 复制代码
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那里处理。

js 复制代码
//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里面的判断了,让它去执行我们修改过的数组方法。现在就是来修改数组的方法了,不过这些数组每一个的处理逻辑都一样。

js 复制代码
const arrayInstrumentations = {};

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

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

  1. 正常找

原型上有数组的方法。

js 复制代码
["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陷阱里是不是有原始对象?那就好办了啊。例如:
js 复制代码
 if (res < 0 || res === false) {
      Array.prototype[key].apply(this.fff, args); //读属性 触发`get`
    }
    
 //读取
function get(target, key, receiver) {
  console.log("key", key); //fff
  }

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

js 复制代码
["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;
  }
}

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

分析分析写

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

js 复制代码
function fn() {
  state[0] = 4; // set 0
}

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

js 复制代码
function fn() {
  state[5] = 4; // add 0
}

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

得满足几个条件:

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

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

js 复制代码
//修改
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啊。所以还是得手动触发。

js 复制代码
 //手动触发更新 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使用的是第二种,因为第一种重写是完全的重写,太麻烦了哈。

js 复制代码
["pop", "push", "shift", "unshift", "splice"].forEach((key) => {
  arrayInstrumentations[key] = function (...args) {
    pauseTracking(); //暂停依赖收集
    let res = Array.prototype[key].apply(this, args);
    resumeTracking(); //回复依赖收集
    return res;
  };
});
js 复制代码
//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);
}

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

相关推荐
m0_7482567824 分钟前
【Django自学】Django入门:如何使用django开发一个web项目(非常详细)
前端·django·sqlite
林小白的日常35 分钟前
uniapp中wx.getFuzzyLocation报错如何解决
前端·javascript·uni-app
傻小胖1 小时前
React 脚手架配置代理完整指南
前端·react.js·前端框架
EterNity_TiMe_1 小时前
【论文复现】农作物病害分类(Web端实现)
前端·人工智能·python·机器学习·分类·数据挖掘
余生H1 小时前
深入理解HTML页面加载解析和渲染过程(一)
前端·html·渲染
吴敬悦2 小时前
领导:按规范提交代码conventionalcommit
前端·程序员·前端工程化
ganlanA2 小时前
uniapp+vue 前端防多次点击表单,防误触多次请求方法。
前端·vue.js·uni-app
卓大胖_2 小时前
Next.js 新手容易犯的错误 _ 性能优化与安全实践(6)
前端·javascript·安全
m0_748246352 小时前
Spring Web MVC:功能端点(Functional Endpoints)
前端·spring·mvc
CodeClimb2 小时前
【华为OD-E卷 - 猜字谜100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od