架构的思考(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);
}

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

相关推荐
kyriewen10 分钟前
折腾了半年 AI 编程工作流,最后发现效率瓶颈是桌上那块屏幕
前端·javascript·ai编程
蜗牛前端37 分钟前
codex 全流程开发上线的高颜值礼簿小程序
前端·微信小程序
大龄秃头程序员1 小时前
我在图文流 App 里落地双层缓存、弱网降级与 OOM 治理
前端
老王以为1 小时前
React Renderer 分离的多平台架构
前端·react native·react.js
hunterandroid1 小时前
Kotlin Coroutines 与 Flow:让异步任务更清晰
前端
Bigger2 小时前
从零搭建 AI 代码审查服务:一份前端也能看懂的 Python 学习笔记
前端·ci/cd·ai编程
lichenyang4532 小时前
JSAPI、NAPI、Biz、Imp:ASCF Demo 如何真正调用系统能力和 C++ 能力
前端
lichenyang4533 小时前
IPC、JSVM、UIThread、libuv:ASCF 架构图里最容易混的几个词
前端
用户059540174463 小时前
Redis记忆存储故障恢复测试踩坑实录:手动测试让我漏掉了2个一致性Bug
前端·css
用户2136610035723 小时前
Vue2脚手架工程化与Axios集成
前端·vue.js