不可变数据的现状
(吾爱)React18+TS通用后台管理系统解决方案落地实战
download:https://www.51xuebc.com/thread-599-1-1.html
不可变数据由于具有构造共享的特性,让一些严重依赖浅比拟的框架快速取得性能收益(如react),同时也让一些需求运用严厉不可变数据的场景防止了深克隆带来的冗余性能开支,而当下除了immutablejs 和 immer 这两款十分盛行的工具库之外,有没有一款比它们的性能和易用性都更好的不可变数据工具库呢?在答复此问题之前,我们先看下immutablejs和immer堕入的窘境。
immutablejs作为一个先驱者,最早的git提交记载能够追溯到2014年4月,随同着react的不可变状态编程理念在2015年之后开端越来越走红,现已到达30K+ star数量,它在js言语世界里具有为不可变数据指引方向般的重要位置,率领大家认识到了不可变数据在某些特定编程范畴的重要性。
不过它的问题也比拟突出,主要归结为2点
1 api 复杂,与原始js操作处置隔离的状态,有很重的学习本钱和记忆担负
2 内建了一套本人的数据构造,需求经过fromJs和toJs做普通json和不可变数据直接的互相转换,带来了额外的开支。
// 额外的学习本钱和记忆担负
immutableA = Immutable.fromJS([0, 0, [1, 2]]);
immutableB = immutableA.set 1, 1;
immutableC = immutableB.update 1, (x) -> x + 1;
immutableC = immutableB.updateIn [2, 1], (x) -> x + 1;
而 2018降生的 immer 则圆满的处理了以上两点问题,它巧妙的运用Proxy代理了原始数据,让用户能够像原始js一样完成一切不可变数据的操作(不支持的环境自动降级为 defineProperty),这样一来用户没有了任何学习本钱和记忆担负.
const { produce } = limu;
const baseState = {
a: 1,
b: [ 1, 2, 3 ],
c: {
c1: { n: 1 },
c2: { m: 2 },
}
};
// 像原始js一样丝滑的操作不可变数据
const nextState = produce(baseState, (draft)=>{
draft.a = 2;
draft.b['2'] = 100;
});
console.log(nextState === baseState); // false
console.log(nextState.a === baseState.a); // false
console.log(nextState.b === baseState.b); // false
console.log(nextState.c === baseState.c); // true
但immer真的就是终极答案了么,在大数组和深层次对象场景immer的性能问题较为突出,经我实测发现的确如它们所说的快过immer较多倍,但仍然未能处理既要速度快又要开发体验好的问题,这两个问题我将在下面逐个详细意义分析。
limu降生
在2021年底我开端为状态库构思v3版本,其中一个重点是支持深度依赖搜集(v2只支持搜集状态的第一层读依赖),那么就需求深度运用Proxy来完成此动作,在深度运用immer是发现调试形式下查看草稿十分糟心,需求借助JSON.parse(JSON.stringify(draft))来完成,虽然后来发现current接口能够导出草稿副本并查看数据构造,但漫天插入额外的current然后在编译时擦除真的让我比拟懊恼,且current自身也有不小的开支,再加上经过issue发现immer的如下相似的性能问题后
const demo = { info: Array.from(Array(10000).keys()) };
produce(demo, (draft) => {
draft.info[2000] = 0; // take long time
});
开端尝试设计并完成limu,希冀坚持像immer一样的api,但可以更快且更好用,于是在阅历经过无数个小迭代后,探索出了一些提速关键技巧(下面将会引见到),处理了内存泄露问题,并达成了保证质量的两个关键点:
跑通了 370+ 测试用例
测试掩盖率抵达了97%
同时也让性能和易用性均到达我的理想后,终于能够正式宣布稳定版发布,且已开端作为根底组件效劳于,接下来将重点引见limu的3大优势。
更快
区别于immer的写时复制机制,limu采用读时浅克隆写时标志修正机制,详细操作流程我们将以下图为例来解说,运用produce接口生成草稿数据后,limu只会对草稿数据读取途径上经过的相关数据节点做浅克隆
修正了目的节点下的值的时分,则会回溯该节点到跟节点的一切途径节点并标志这些节点为已修正
最后完毕草稿生成final对象时,limu只需求从根节点把一切标志修正的节点的副本交换到对应位置即可,没有标志修正的节点则不运用副本(注:生成副本不代表已被修正)
这样的机制在对象的原始层级关系较为复杂且修正途径不广的场景下,且不需求冻结原始对象时,性能表现异常优良,可到达比 immer 快 5 倍或更多,只要在修正数据逐步普及整个对象一切节点时,limu的性能才会呈线性下载趋向,逐渐接近immer,但也要比immer快很多。
测实验证
为考证上述结论,用户可依照以下流程取得针对limu与immer性能测试比照数据
git clone https://github.com/tnfe/limu
cd limu
npm i
cd benchmark
npm i
node opBigData.js // 触发测试执行,控制台回显结果
# or
node caseReadWrite.js
我们准备两个用例,一个改编自 immer 官方的性能测(注:跳转后见头部标注的链接)
执行 node opBigData.js 得到如下结果 (柱条越短代表越快)
注:以上是v9版本,immer 23年4月发布了v10版本,经测试发现结果变化不大,性能提升不明显
一个是我们本人准备的深层次 json 读写案例,结果如下 (柱条越短代表越快)
可经过注入ST值调整不同的测试战略,例如 ST=1 node caseReadWrite.js,不注入时默许为 1
ST=1,关闭冻结,不操作数组
ST=2,关闭冻结,操作数组
ST=3,开启冻结,不操作数组
ST=4,开启冻结,操作数组
更强
limu应用Symbol和原型链躲藏代理元数据,让元数据一直跟随草稿节点,在草稿完毕后才擦除,让用户不只能够像操作原生js一样操作不可变数据,还能像查看原生json一样查看草稿数据(仅需展开一层代理即可),且一直让用用户对草稿的修正数据实时同步到可查看节点上,极大的进步了调试体验。
这里我们将分别罗列limu、immer、mutative、structura在调试状态下对草稿展开的图示:
limu 可恣意查看草稿一切节点,且数据一直同步为修正后的数据
structura 可查看草稿的原始构造,但草稿数据是过时的(注:但log的数据是正确的)
mutative 坚持了和immer相似的构造,无法快速查看
immer 应用Proxy层层代理,无法快速查看
轻量
imu设计为面向现代阅读器的不可变数据js库,只运转于支持proxy特性的js环境,原生支持根对象为Map,Set,Array,Object,相比immer 6.3kb大小容量接近减少1/3。
同时提供了更多适用的api
immut
生成一个不可修正的对象im,但原始对象的修正将同步会影响到im
import { immut } from 'limu';
const base = { a: 1, b: 2, c: [1, 2, 3], d: { d1: 1, d2: 2 } };
const im = immut(base);
im.a = 100; // 修正无效
base.a = 100; // 修正会影响 im
兼并后仍然能够读到最新值
const base = { a: 1, b: 2, c: [1, 2, 3], d: { d1: 1, d2: 2 } };
const im = immut(base);
const draft = createDraft(base);
draft.d.d1 = 100;
console.log(im.d.d1); // 1,坚持不变
const next = finishDraft(draft);
Object.assign(base, next);
console.log(im.d.d1); // 100,im和base一直坚持数据同步
immut 采用了读时浅代理的机制,相比deepFreeze会具有更好性能,适用于不暴露原始对象进来,只暴露生成的不可变对象进来的场景,并应用onOperate搜集读依赖
onOperate
支持对createDraft、produce、immt 配置 onOperate回调监听一切读写变化(注:immut只能监听到读变化)
例如以下代码:
const { createDraft, finishDraft } = limu;
const base = new Map([
['nick', { list: [1,2,3], info: { age: 1, grade: 4, money: 1000 } }],
['fancy', { list: [1,2,3,4,5], info: { age: 2, grade: 6, money: 100000000 } }],
['anonymous', { list: [1,2], info: { age: 0, grade: 0, money: 0 } }],
]);
const draft = createDraft(base, { onOperate: console.log });
draft.delete('anonymous');
draft.get('fancy').info.money = 200000000;
const final = finishDraft(draft);
将产生以下监听结果,十分有利于上层框架做读写依赖的搜集
行将发布的基于limu驱动后完成了十分多有意义的功用,尽请等待。
结语
2年磨砺,让一个最初有点玩具性质的作品最终落地(融入concent、helux)是我预料之外的结果,分离最近爆火的室温超导的韩国团队做类比,他们的LK-99一烧就是20多年,不论结果能否如意,至少具有一颗挚爱科学的心才干够坚持下来,想起在无数个深夜一遍遍npm run test并优化代码,何尝又不是由于坚持一颗挚爱的心而沉溺进去炼代码丹呢?
领取专属 10元无门槛券
私享最新 技术干货