乐趣区

关于前端:剑指immer更快更强的limu

前言

欢送理解和关注 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); // false
console.log(nextState.a === baseState.a); // false
console.log(nextState.b === baseState.b); // false
console.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/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 一样查看草稿数据(仅需开展一层代理即可),且始终让用用户对草稿的批改数据实时同步到可查看节点上,极大的进步了调试体验。

这里咱们将别离列举 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
退出移动版