实现响应式系统
笔记记录重要内容。
核心要素
要实现一个响应式系统,最为核心的有两个部分:
- 监听数据的读写
- 关联数据和函数
只要把这两个部分完成了,那么整个响应式系统也就基本成型了。
监听数据读写
- 数据:在 JS 中,能够拦截读写的方式,要么 Object.defineProperty,要么就是 Proxy,这两个方法针对的目标是对象,因此我们这里考虑对对象类型进行监听
- 读写:虽然说是监听读写,但是细分下来要监听的行为如下:
- 获取属性:读取
- 设置属性:写入
- 新增属性:写入
- 删除属性:写入
- 是否存在某个属性:读取
- 遍历属性:读取
拦截后对应的处理
不同的行为,拦截下来后要做的事情是不一样的。整体来讲分为两大类:
- 收集器:针对读取的行为,会触发收集器去收集依赖,所谓收集依赖,其实就是建立数据和函数之间的依赖关系
- 触发器:针对写入行为,触发器会工作,触发器所做的事情就是触发数据所关联的所有函数,让这些函数重新执行
下面是不同行为对应的事情:
- 获取属性:收集器
- 设置属性:触发器
- 新增属性:触发器
- 删除属性:触发器
- 是否存在某个属性:收集器
- 遍历属性:收集器
总结起来也很简单,只要涉及到属性的访问,那就是收集器,只要涉及到属性的设置(新增、删除都算设置),那就是触发器。
完善 reactive 方法
reactive 需要加上一些边界的判断条件:
- 如果传入的值本身就是一个代理对象,返回这个本身的代理对象(采用 weakMap 的映射缓存代理对象和原对象)
- 如果传入的不是对象,直接返回这个数据
数组中查找对象
因为在进行代理的时候,是进行了递归代理的,也就是说对象里面成员包含对象的话,也会被代理,这就会导致数组中成员有对象的话,是找不到的。原因很简答,比较的是原始对象和代理对象,自然就找不到。
问题:
js
import { reactive } from "./reactive/index.js";
const obj = { name: "Yan" };
const arr = reactive([1, obj, 3]);
console.log(arr.indexOf(obj)); //-1 出现问题了,因为obj(原)和数组中的obj(代理对象)不是一个对象。
console.log(arr.lastIndexOf(obj)); //-1
console.log(arr.includes(obj)); //false
解决方案:先正常找,找不到就在原始对象中重新找一遍
- 重写数组的读取方法:indexOf,includes,lastIndexOf 方法。[采用 symbol 来做一个标识,标识这个方法需要重写]
此处的代码实现有点难,需要好好的理解,看代码
getterHandler.js
js
//+++
const arrayInstrumentations = {};
["indexOf", "lastIndexOf", "includes"].forEach((key, index) => {
arrayInstrumentations[key] = function (...args) {
//先从数组中正常找
const res = Array.prototype[key].apply(this, args);
//如果找不到,就从原对象中去找
if (res === -1 || res === false) {
//注意:这里访问了代理对象的一个属性,那么就会被该代理对象的拦截函数拦截。即会走到下main的getter中
return Array.prototype[key].apply(this[SYMBOL_RAW], args);
}
return res;
};
});
//getter拦截函数
export default function (target, key) {
//+++如果读取的是特殊的属性,我们就需要将原对象返回,这是供重写数组方法用的
if (key === SYMBOL_RAW) return target;
//依赖收集
track(target, options.GET, key);
//递归深层次代理对象
const result = Reflect.get(target, key);
if (isObject(result)) {
return reactive(result);
}
//+++ 判断是否是数组,是的话就返回重写后的方法
if (Array.isArray(target) && arrayInstrumentations.hasOwnProperty(key)) {
return arrayInstrumentations[key];
}
return result;
}
数组改动长度
关于数组长度的改变,也会有一些问题,如果是隐式的改变长度,不会触发 length 的派发更新。
js
const proxyArr = reactive([1, 2, 3]);
//只进行了5下标的set拦截。实际上这里隐式的改变length,也期望length进行派发更新
proxyArr[5] = 100;
//显示的更新length会触发两次
proxyArr.length = 10;
//如果显示改变length操作,新length<原length,是不会触发删除操作的派发更新的
proxyArr.length = 1;
另外即便是显式的设置 length,这里会涉及到新增和删除,新增情况下的拦截是正常的,但是在删除的情况下,不会触发 DELETE 拦截,因此也需要手动处理。
解决方案:
- 在 set 拦截器中,缓存原 length,当隐士改变 length 的时候,手动调用派发更新 trigger
- 在显示改变 length 同时小于原 length 长度,手动派发更新 delete 的 trigger
js
import trigger from "../../effect/trigger.js";
import { hasChanged, options } from "../../utils/index.js";
export default function (target, key, value) {
//判断操作是新增还是修改
const type = target.hasOwnProperty(key) ? options.SET : options.ADD;
//缓存旧值,没修改的话不派发更新
const oldVal = target[key];
//+++缓存数组原长度
const oldLen = Array.isArray(target) ? target.length : undefined;
//+++先进行设置
const result = Reflect.set(target, key, value);
// 设置值不一样,派发更新
if (hasChanged(oldVal, value)) {
trigger(target, type, key);
//+++数组情况:隐士和显示length处理
if (Array.isArray(target) && oldLen !== target.length) {
if (key !== "length") {
// 隐式设置length
trigger(target, options.SET, "length");
} else {
// 显示设置length要考虑length长度小于旧长度的情况,执行删除的派发更新
for (let i = target.length; i < oldLen; i++) {
trigger(target, options.DELETE, i.toString());
}
}
}
}
return result;
}
自定义是否要收集依赖
当调用 push、pop、shift 等方法的时候,因为涉及到了 length 属性的变化,会触发依赖收集,这是我们不期望的。
最好的方式,就是由我们来控制是否要依赖收集。
解决方案
track.js中依赖追踪收集函数,加一个变量来控制是否搜集依赖。
重写数组的
"push", "pop", "shift", "unshift", "splice"
方法,关闭和打开依赖
track,js
js
import { options } from "../utils/index.js";
// 控制依赖收集的开关
let shouldTrack = true;
export function openTrack() {
shouldTrack = true;
}
export function stopTrack() {
shouldTrack = false;
}
// 依赖收集
export default function track(target, type, key) {
// 先判断收集开关是否打开
if (!shouldTrack) return;
if (type === options.ITERATE) {
// console.log("收集器:原对象", target);
console.log(`收集器:代理函数的${type}操作`);
return;
}
// console.log("收集器:原对象", target);
console.log(`收集器:代理函数的${key}的${type}操作`);
}
getterHandler,js
js
// 重写数组:手动控制push等方法收集依赖
["push", "pop", "shift", "unshift", "splice"].forEach((key) => {
arrayInstrumentations[key] = function (...args) {
stopTrack();
const res = Array.prototype[key].apply(this, args);
openTrack();
return res;
};
});
//getter拦截函数
//xxxx.......