架构的思考(2)
上面留下了两个知识点,只是大概的实现了那个过程。心急吃不了热豆腐,追求卓越的人都有一个共性,那就是聚焦
。
卓越的人善于把所有的能量聚集在一点,伤其十指不如断其一指。当面对复杂问题的时候,
人很容易乱,要解决的问题有很多,问题点之间还有关联,这就不可避免的造成我们的精力分散。本来每一天的精力都是有限的,随着精力的分散,那作用在每个问题点上的精力就微乎其微了,这也是我们大部分人的通病。结果就是写着写着就烦躁,然后放弃。这就是不善于聚焦,聚焦的关键点在于发现,发现问题的弱点,有哪些点需要一个一个地去攻克,当攻克其中某个点的时候,可能会收到其他的干扰,分散我们的精力。如何来抵抗这种干扰,这是需要非常强的能力的。当解决其中某个点的时候,就死死地抓住,然后集中所有的力量发起一轮又一轮的猛攻,这与我们高考不会就跳过
方案是不同的。接下来,就让我们来感受一下这聚焦的力量。
现在集中精力搞定这个问题:监听数据的读取和修改
。
监听
整个Proxy
代码就是监听, 先不去考虑set
和get
,这是监听的细节。先处理一下边界调节,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
的指向呢,那就得深入对象的内部方法,看看读取属性的基本操作。
接收了两个参数,第一个属性,第二个是指定
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);
}
到这里操作类型就写好了,但至于set
与get
的关联,触发哪个读,触发哪个写,这时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);
}
那读取信息就暂时先这样,应该是其他了吧。
写
写肯定不是写一个属性值,写是更改对象的信息。首先ADD
和SET
都会触发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;
}
到这里,写
的部分也就完事了。
转载自:https://juejin.cn/post/7346580626319130636