前言

欢送理解和关注limu,拜访文档并点击右键调出控制台后可实时体验 limu api 和 immer api做比照(全局已绑定limu和immer对象)。

接下来让咱们一起深刻理解limu的诞生历程吧 ^\_^

不可变数据的现状

不可变数据因为领有构造共享的个性,让一些重大依赖浅比拟的框架疾速取得性能收益(如react),同时也让一些须要应用严格不可变数据的场景防止了深克隆带来的冗余性能开销,而当下除了immutablejs 和 immer 这两款十分风行的工具库之外,有没有一款比它们的性能和易用性都更好的不可变数据工具库呢?在答复此问题之前,咱们先看下immutablejsimmer陷入的窘境。

immutablejs作为一个先驱者,最早的git提交记录能够追溯到2014年4月,随同着react的不可变状态编程理念在2015年之后开始越来越走红,现已达到30K+ star数量,它在js语言世界里领有为不可变数据指引方向般的重要位置,率领大家意识到了不可变数据在某些特定编程畛域的重要性。

不过它的问题也比较突出,次要归结为2点

  • 1 api 简单,与原始js操作解决隔离的状态,有很重的学习老本和记忆负担
  • 2 内建了一套本人的数据结构,须要通过fromJstoJs做一般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); // falseconsole.log(nextState.a === baseState.a); // falseconsole.log(nextState.b === baseState.b); // falseconsole.log(nextState.c === baseState.c); // true

immer真的就是终极答案了么,在大数组和深层次对象场景immer的性能问题较为突出,见此问题形容,社区开始有不少作者重整旗鼓尝试冲破,留意到这外面较为突出的有structura和mutative,经我实测发现的确如它们所说的快过immer较多倍,但仍然未能解决既要速度快又要开发体验好的问题,这两个问题我将在上面一一具体意义分析。

limu诞生

在2021年底我开始为状态库concent构思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快很多。

测试验证

为验证上述论断,用户可依照以下流程取得针对limuimmer性能测试比照数据

git clone https://github.com/tnfe/limucd limunpm icd benchmarknpm inode opBigData.js // 触发测试执行,控制台回显后果# ornode 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一样查看草稿数据(仅需开展一层代理即可),且始终让用用户对草稿的批改数据实时同步到可查看节点上,极大的进步了调试体验。

这里咱们将别离列举limuimmermutativestructura在调试状态下对草稿开展的图示:

  • limu 可任意查看草稿所有节点,且数据始终同步为批改后的数据

  • structura 可查看草稿的原始构造,但草稿数据是过期的(注:但log的数据是正确的)

  • mutative 放弃了和immer相似的构造,无奈疾速查看

  • immer 利用Proxy层层代理,无奈疾速查看

轻量

imu设计为面向古代浏览器的不可变数据js库,只运行于反对proxy个性的js环境,原生反对根对象为MapSetArrayObject,相比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

反对对createDraftproduceimmt 配置 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);

将产生以下监听后果,十分有利于下层框架做读写依赖的收集

行将公布的helux v3基于limu驱动后实现了十分多有意思的性能,尽请期待。

结语

2年磨砺,让一个最后有点玩具性质的作品最终落地(融入concent、helux)是我意料之外的后果,联合最近爆火的室温超导的韩国团队做类比,他们的LK-99一烧就是20多年,不论后果是否如意,至多领有一颗挚爱迷信的心才可能保持下来,想起在无数个深夜一遍遍npm run test并优化代码,何尝又不是因为放弃一颗挚爱的心而沉浸进去炼代码丹呢?

不论 limu 是否会被吞没在历史的星辰大海里,稳定版的公布算是给本人一个交代了,愿各位码客也放弃源源不断的求知欲炼出心中的丹药。

友链

欢送关注这些乏味的我的项目

  • 一个基于node.js的高速视频制作库 ffcreator
  • 工具链无关sdk化模块联邦 hel-micro
  • 行将公布的具备深浅依赖收集双策略和有向图架构的全新状态库 helux v3
  • vscode插件联合chatgpt实现的工程化工具 smart-ide
  • 一个让webpack我的项目反对vite的前端我的项目的转换工具 wp2vite