likes
comments
collection
share

架构的思考(2)

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

上面留下了两个知识点,只是大概的实现了那个过程。心急吃不了热豆腐,追求卓越的人都有一个共性,那就是聚焦

卓越的人善于把所有的能量聚集在一点,伤其十指不如断其一指。当面对复杂问题的时候, 人很容易乱,要解决的问题有很多,问题点之间还有关联,这就不可避免的造成我们的精力分散。本来每一天的精力都是有限的,随着精力的分散,那作用在每个问题点上的精力就微乎其微了,这也是我们大部分人的通病。结果就是写着写着就烦躁,然后放弃。这就是不善于聚焦,聚焦的关键点在于发现,发现问题的弱点,有哪些点需要一个一个地去攻克,当攻克其中某个点的时候,可能会收到其他的干扰,分散我们的精力。如何来抵抗这种干扰,这是需要非常强的能力的。当解决其中某个点的时候,就死死地抓住,然后集中所有的力量发起一轮又一轮的猛攻,这与我们高考不会就跳过方案是不同的。接下来,就让我们来感受一下这聚焦的力量。

现在集中精力搞定这个问题:监听数据的读取和修改

监听

整个Proxy代码就是监听, 先不去考虑setget,这是监听的细节。先处理一下边界调节,Proxy要求接收一个对象,所以得先处理这个。

// reactive.js

export function reactive(target) {
  if (!isObject(target)) {
    return target; // 如果不是对象, 返回原始数据
  }
  return new Proxy(target, {
    get(target, key) {
      track(target, key); //依赖收集
      return target[key]; //返回对象属性值
    },
    set(target, key, value) {
      trigger(target, key); //派发更新
      return Reflect.set(target, key, value); //设置对象的响应属性
    },
  });
}

另外,如果监听的都是同一个对象,那怎么办?因为是 new调用,所以reactive返回的都是新的对象,也就是说监听同个对象,拿到的是不同的对象。监听的目的就是给对象打标记,同一个对象有一个标记才是合理的。

那就要将被监听的对象与代理之间形成一种关联, 一个对象对应一个代理,如果将来再传入同一个对象,那直接把代理返回。这样就可以用 map来做,当然有个小细节,如果使用map,那当这个对象不再使用时,对象的引用在map里还存在,就造成内存泄漏,回收不掉。而weakmap的key值是弱引用,也就是说外边不再使用这个对象时,可以把它整个键值对回收掉。

// reactive.js

const targetMap = new WeakMap(); //创建 map

export function reactive(target) {
  if (!isObject(target)) {
    return target; // 如果不是对象, 返回原始数据
  }

  if (targetMap.has(target)) {
    return targetMap.get(target); //如果代理过 直接返回
  }
  const proxy = new Proxy(target, {
    get(target, key) {
      track(target, key); //依赖收集
      return target[key]; //返回对象属性值
    },
    set(target, key, value) {
      trigger(target, key); //派发更新
      return Reflect.set(target, key, value); //设置对象的响应属性
    },
  });
  target.set(target, proxy); //存储 proxy
  return proxy;
}

这样边界条件就OK了,但是看起来逻辑有点混乱,我们可以把proxy的处理抽出来。

// handlers.js

import { track, trigger } from "./effect.js";

export const handlers = {
  get(target, key) {
    track(target, key); //依赖收集
    return target[key]; //返回对象属性值
  },
  set(target, key, value) {
    trigger(target, key); //派发更新
    return Reflect.set(target, key, value); //设置对象的响应属性
  },
};


//reactive.js

export function reactive(target) {
  if (!isObject(target)) {
    return target; // 如果不是对象, 返回原始数据
  }

  if (targetMap.has(target)) {
    return targetMap.get(target); //如果代理过 直接返回
  }
  const proxy = new Proxy(target, handlers); //处理
  targetMap.set(target, proxy); //存储 proxy
  return proxy;
}

到这里,监听部分就差不多了,当然还有点细节,如果如果传入的就是代理怎么办,这就慢慢处理。

读取

对于读取,里面的依赖收集仅仅是打印了点东西,还有很多细节等待处理,现在重点不在这里。通过聚焦,重点是要分析什么时候在读,读了什么东西的问题。

Reflect这个应该认识哈,可以用来读取属性。那以下两种方式的读取有什么区别呢?

  get(target, key) {
    track(target, key); //依赖收集

    Reflect.get(target,key)
    return target[key]; //返回对象属性值
  },

有这么一种场景,如下

import { reactive } from "./reactive.js";

const obj = {
  a: 1,
  b: 2,
  //相当于以下写法
  get c() {
    return this.a + this.b;
  },
};

// Object.defineProperty(obj, "c", {
//   get() {
//     return this.a + this.b;
//   },
// });

const state = reactive(obj);

function fn() {
  state.c;
}
fn(); // 依赖收集 c

这时肯定是收集到了属性c的依赖,而属性c又用到了属性a和属性b,那也会收集到属性a和属性b,这时我们所期望的。但执行结果是 只收集了属性c的依赖。

收集不到属性a和属性c的关键是this,这个this指向的是obj源对象,并不是代理对象。只有当this指向代理对象的时候,才能拿到属性a和属性b。

那如果改变this的指向呢,那就得深入对象的内部方法,看看读取属性的基本操作。

架构的思考(2) 接收了两个参数,第一个属性,第二个是指定this的指向。回到代码语言层面上来,是在执行target[key],但是基本操作时可以指定第二个参数的,但这个语言层面的代码无法实现。它就默认了第二个参数是target,这也就是为什么this指向了obj

Reflect则可以直接调用对象的内部方法,对,直接调。所以读取时就可以写成这样

  get(target, key, receiver) {
    track(target, key); //依赖收集
    return Reflect.get(target, key, receiver); //返回对象属性值
  },

这样就可以读到 属性c、属性a和属性b了。

说到返回,上面是直接把属性值返回,但如果属性值是一个对象呢?

const obj = {
  a: 1,
  b: 2,
  c: {
    d: 5,
  },
};
const state = reactive(obj);

function fn() {
  state.c.d;
}
fn();

当读取属性d的时候,我们希望能收集到属性c和属性d的依赖。但事与愿违,目前是能收集到属性c的依赖,因为当读取c的时候,返回的是对象c,而对象c不是代理,所以没有被标记,因此无法监听到对属性d的读取。

 get(target, key, receiver) {
    track(target, key); //依赖收集
    console.log(Reflect.get(target, key, receiver)); //{d:5}
    return Reflect.get(target, key, receiver); //返回对象属性值
  },

所以当返回的是一个对象的时候,还得进行代理。

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

现在读的方法,哦不能说读,应该是obj.的捕获器的基本功能已经完成了,那是不是说就结束了呢?那for...in呢?又比如判断一个属性在不在一个对象里,那如果将来这个属性存在对象里里了,那是不是要重新运行那个函数,这不就是依赖收集然后派发更新吗?所以这就加深了对的认知,不仅仅是读取属性,读取属性是一种信息,而判断属性在不在对象里,这也是一种信息,所以应该理解为读取信息。

Proxy的处理函数里,get是拦截读取的,并不能拦截到 判断属性存在不存在。那判断属性存在与否是运行什么?打开262文档,就是这个[[HasProperty]]这个内部方法。打开Proxy文档发现对应的捕获器就是has

 has(target, key) {
    track(target, key); 
    return Reflect.has(target, key);
  },

那这就ok了。

那技术不管是什么职位,都对涉及两个层面:

  • 知识层面 在写某个东西的时候,得懂它涉及的方方面面的知识,比如上面的,得懂代理,懂反射,懂绑定this,不然根本不会想到会发生这样的问题,连会发生问题都不知道,那有何谈解决问题呢,这就说明知识的广度非常重要。
  • 能力层面 知识都懂,但没办法去组织这些知识,无法使用它们来解决问题。

好的,这么一说紧张起来,那面的has还存在问题吗?说个抽象的事,之前是属性不存在,然后去判断,后面属性存在了,那是应该触发响应的函数,毕竟这是和目标(判断存不存在,然后做别的事)相契合的。但是,如果对象的属性本来就在,后面修改属性值了,那还应该触发has进行依赖收集吗?想想,修改属性,为什么要去触发has,这里我根本不关心属性的值是啥,我只关心存在不存在,那存在本来就没发生变化,那就不应该触发has

这说明了我们在依赖收集和派发更新的时候,只关心某个对象是否被读或者是否被改了,还少了读和改的动作。想想是不是,没有这个动作时,只要读或者改,都会进入函数,所以我们应该限制的更细。比如依赖收集,应该是我正在读取对象的属性或者我正在判断对象属性存在不存在,所以依赖收集和派发更新还少了个操作类型。

  • 依赖收集
//operation.js

export const TrackOpTypes = {
  GET: "get", //读取属性值
  HAS: "has", //潘丹属性是否存在
};
// handlers.js

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

/**
 * @description: 依赖收集(建立对应关系)
 * @param {* object} target 代理的源对象
 * @param {* string} key 属性
 * @param {* string} type 读的操作类型
 * @return {*}
 */
export function track(target, type,key) {
  console.log(type, key);
}
  • 派发更新
//operation.js

export const TriggerOpTypes = {
  SET: "set", //设置属性
  ADD: "add", //添加属性
  DELETE: "delete", //删除属性
};
// handlers.js

 set(target, key, value) {
    trigger(target, TriggerOpTypes.SET, key); //派发更新
    return Reflect.set(target, key, value); //设置对象的响应属性
  },

//effect.js

/**
 * @description: 派发更新
 * @param {* object} target 代理的源对象
 * @param {* stirng} key 属性
 * @param {* stirng} type 写的操作类型
 * @return {*}
 */
export function trigger(target, type, key) {
  console.log(`【${type}】`, key);
}

到这里操作类型就写好了,但至于setget的关联,触发哪个读,触发哪个写,这时effect要处理的是,现在还没到那个阶段。

上面已经引出了一个问题判断属性存在与否,并做了例子。那for...in遍历同样,当某个属性变化或新增属性,都会影响到函数的运行结果。

//operation.js

export const TrackOpTypes = {
  GET: "get", //读取属性值
  HAS: "has", //判断属性是否存在
  INTERATE: "interate", //迭代对象
};
// handlers.js

function ownKeys(target) {
  track(target, TrackOpTypes.INTERATE);
  return Reflect.ownKeys(target);
}

那读取信息就暂时先这样,应该是其他了吧。

写肯定不是写一个属性值,写是更改对象的信息。首先ADDSET都会触发set捕获器,那就得先做个类型判断。

//effect.js

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

  trigger(target, type, key); //派发更新
  return Reflect.set(target, key, value, receiver); //设置对象的响应属性
}

写还有个delete属性。

//effect.js

function deleteProperty(target, key) {
  trigger(target, TriggerOpTypes.DELETE, key);
  return Reflect.deleteProperty(target, key);
}

发现一个问题,当删除一个不存在的属性的时候也派发更新了🤣,所以这里还得操作一下,当原来有属性且删除成功的时候才派发更新。

function deleteProperty(target, key) {
  //原来有属性
  const hasKey = target.hasOwnProperty(key);
  const result = Reflect.deleteProperty(target, key); // true or false
  if (hasKey && result) {
    trigger(target, TriggerOpTypes.DELETE, key);
  }
  return result;
}

😂😂😂😂那修改属性是不是也是这样,原来属性值为1,重新修改为1,这个值没变,不能进行派发哈,所以再优化下,当值有变化或者新增属性的时候,才派发更新。 头疼,什么是变化?===判断吗?可NaN !== NaN啊,我们关心它的变化,那是因为变化过后可能会影响到函数的执行结果,才要派发更新。重点是会影响结果的变化,所以用Object.js()来判断会更好。

/**
 * @description: 判断两个值是否相等
 * @param {* string | number} oldValue
 * @param {* string | number} newValue
 * @return {* boolean}
 */
export function hasChange(oldValue, newValue) {
  return !Object.is(oldValue, newValue);
}
//effect.js

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

  const oldValue = target[key]; //仅获取值 不用Reflect,因为会收集依赖

  const result = Reflect.set(target, key, value, receiver); //设置对象的响应属性

  if (!result) {
    return result;
  }

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

到这里,的部分也就完事了。