十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MobX deepEnhancer 原始值快速路径:可观察数据写入性能优化原理与验证

MobX deepEnhancer 原始值快速路径:可观察数据写入性能优化原理与验证 MobX deepEnhancer 原始值快速路径可观察数据写入性能优化原理与验证【免费下载链接】mobxSimple, scalable state management.项目地址: https://gitcode.com/gh_mirrors/mo/mobx本文基于 MobX 仓库中的 changeset 记录.changeset/deep-enhancer-primitive-fast-path.md深入剖析 MobX 核心增强器deepEnhancer针对原始值primitive实现的快速路径优化为何向深层可观察结构写入数字、字符串、布尔值等原始值时不再需要执行任何类型检查以及这项优化如何带来创建原始值可观察数组约快 4 倍、可观察 Set/Map 写入快 20-25%的实测收益。读完本文你将理解 MobX enhancer 机制的工作原理、快速路径的源码实现细节以及如何通过仓库自带的性能测试套件自行复现和验证这些数据。一、这条 changeset 记录了什么.changeset/deep-enhancer-primitive-fast-path.md是仓库中由changesets/cli维护的变更记录changeset文件声明对mobx包打一个patch级别的版本补丁其核心内容如下perf: fast-path primitives indeepEnhancer. Writing a primitive into a deep observable no longer runs the observable/array/plain-object/Map/Set/function type checks; primitives can never be made observable, so they are returned immediately. Creating an observable array of primitives is ~4x faster, and observable Set/Map writes are ~20-25% faster in the perf suite.翻译并拆解这条记录的技术信息可以提炼出四个要点优化位置deepEnhancer——MobX 中负责深度转换数据结构的默认增强器优化手段为原始值建立快速路径fast-path写入时直接返回跳过后续全部类型检查分支理论依据原始值primitive永远不可能被转换/增强为可观察对象因此提前返回是安全且无损的性能收益在仓库自带的性能测试套件中创建由原始值组成的可观察数组约提速 4 倍可观察Set/Map的写入约提速 20-25%。changeset 的版本管理配置位于 .changeset/config.json可以看到该仓库使用changesets/changelog-github生成变更日志baseBranch为main。这类文件是理解 MobX 每个版本中性能改动与行为变化的第一手资料。二、理解前提MobX 的 Enhancer 机制要理解这条优化必须先弄清deepEnhancer在 MobX 中的角色。2.1 什么是 Enhancer在 MobX 中enhancer增强器是决定一个值在写入可观察结构时如何处理的函数。它定义了类型为IEnhancerT的接口签名如下见 packages/mobx/src/types/modifiers.tsexport interface IEnhancerT { (newValue: T, oldValue: T | undefined, name: string): T }enhancer 接收三个参数新值newValue、旧值oldValue和属性名name返回最终存入可观察结构的值。MobX 内置了多个 enhancer在同一个文件中全部定义Enhancer行为使用场景deepEnhancer递归地把普通对象、数组、Map、Set、函数转换为对应可观察结构默认的深度可观察转换shallowEnhancer只转换顶层容器不递归内部值observable({...}, { deep: false })等浅层观察referenceEnhancer原样返回绝不转换为可观察observable.ref、observable.box默认等refStructEnhancer用deepEqual比较新旧值相等则保留旧引用observable.struct其中deepEnhancer是 MobX 的默认增强器。从 packages/mobx/src/api/observable.ts 的源码可以看出只要没有显式指定最终都会回落到deepEnhancerexport function getEnhancerFromOptions(options: CreateObservableOptions): IEnhancerany { return options.deep true ? deepEnhancer : options.deep false ? referenceEnhancer : getEnhancerFromAnnotation(options.defaultDecorator) } export function getEnhancerFromAnnotation(annotation?: Annotation): IEnhancerany { return !annotation ? deepEnhancer : annotation.options_?.enhancer_ ?? deepEnhancer }也就是说observable({...})、observable.array([...])、observable.map()、observable.set()以及各种observable装饰器凡是走深度路线最终都会调用deepEnhancer。这就是为什么优化它的热路径hot path能带来全局性的写入性能提升。2.2 deepEnhancer 的完整逻辑优化后的deepEnhancer完整实现在 packages/mobx/src/types/modifiers.tsexport function deepEnhancer(v, _, name) { // primitives can never be made observable; skip the type checks below if (v null || (typeof v ! object typeof v ! function)) { return v } // it is an observable already, done if (isObservable(v)) { return v } // something that can be converted and mutated? if (Array.isArray(v)) { return observable.array(v, { name }) } if (isPlainObject(v)) { return observable.object(v, undefined, { name }) } if (isES6Map(v)) { return observable.map(v, { name }) } if (isES6Set(v)) { return observable.set(v, { name }) } if (typeof v function !isAction(v) !isFlow(v)) { if (isGenerator(v)) { return flow(v) } else { return autoAction(name, v) } } return v }可以看到函数体从一行带注释的原始值检查开始这正是本次 changeset 引入的快速路径。三、快速路径的实现原理3.1 为什么原始值可以安全地提前返回快速路径的判断条件只有一行if (v null || (typeof v ! object typeof v ! function)) { return v }即当值为null、或者类型既不是object也不是function时也就是number、string、boolean、symbol、bigint、undefined等原始值直接原样返回。这条优化的正确性建立在 MobX 的一个核心不变式invariant之上原始值永远不可能被转换为可观察对象。MobX 的响应式能力需要附着在可变的容器上对象、数组、Map、Set或者通过observable.box这样的盒子间接持有裸的原始值本身没有可观察的载体deepEnhancer对它们做不了任何转换。因此在进入昂贵的类型检查之前就返回结果与走完所有分支完全一致——这正是 changeset 中 primitives can never be made observable, so they are returned immediately 这句话的源码含义。3.2 优化前跳过了什么在引入快速路径之前一个原始值写入会依次执行isObservable(v)—— 检查是否已是可观察对象Array.isArray(v)—— 数组检查isPlainObject(v)—— 普通对象检查isES6Map(v)—— 原生 Map 检查isES6Set(v)—— 原生 Set 检查函数分支 ——isAction/isFlow/isGenerator等检查。这些检查大多涉及原型链遍历、标志位读取乃至Symbol属性探测如isObservable需要读取对象上的$mobx标记对一个注定要原样返回的原始值而言全部是无效开销。而原始值写入在真实业务中恰恰是最常见的场景——数组里存数字、Map/Set 里存字符串、对象属性存布尔值——因此这个分支命中率极高是典型的高收益热路径优化。值得一提的是优化后isObservable等检查仍然保留服务于真正的引用类型如已可观察对象、普通对象、数组等功能语义没有任何变化只是把不需要处理的输入挡在了门口。从代码注释 primitives can never be made observable; skip the type checks below 也可以确认这是有意为之的显式优化而非副作用。四、性能收益与复现方法4.1 changeset 声明的收益按 changeset 的记载在仓库的性能测试套件perf suite中实测创建由原始值组成的可观察数组observable.array接收大量数字/字符串约4 倍提速。原因很直观数组初始化时每个元素都要过一遍 enhancer快速路径让每个元素只需一次类型判断可观察 Set / Map 的写入set.add/map.set约20-25%提速。同理每次写入的 enhancer 调用从一串类型检查缩减为一次原始值判断。这些是 changeset 中声明的仓库内测数据引用时应标注其来源为.changeset/deep-enhancer-primitive-fast-path.md的 perf suite 记录。具体的收益幅度会随运行平台CPU、Node 版本浮动但优化方向与量级可以通过下面的方法自行复现。4.2 性能测试套件的位置与运行方式MobX 仓库自带一套完整的性能测试位于 packages/mobx/tests/perf/perf.js覆盖可观察数组创建Map/Set 读写computed 记忆化观察者调度等场景。例如create arrayperf.js循环 1000 次用含 1000 个元素的数组创建observable.array测量耗时Set: setting and deleting propertiesperf.js对可观察 Set 反复add/delete测量写入耗时Map: setting and deleting propertiesperf.js对可观察 Map 反复set/delete。套件入口是 packages/mobx/tests/perf/index.js它通过tape执行各测试并支持通过环境变量PERSIST把测量结果追加写入perf_report/perf.txt。运行方式由 packages/mobx/scripts/perf.sh 给出time node --expose-gc ./__tests__/perf/index.js注意两点前提该脚本在packages/mobx目录下执行且性能测试直接require(../../dist/mobx.cjs.production.min.js)见 perf.js因此先要构建出生产版产物如执行npm run build生成dist目录否则会因找不到产物而失败--expose-gc暴露global.gc()配合脚本里各测试开头的gc()调用如 perf.js在每组测量前强制垃圾回收尽量消除 GC 干扰、保证测量可比性。五、快速路径在调用链中的覆盖面这项优化之所以值得记录为 patch是因为deepEnhancer作为默认 enhancer 遍布 MobX 的多个核心数据结构。从源码检索deepEnhancer的引用可以看到它的覆盖面packages/mobx/src/api/observable.tsgetEnhancerFromOptions/getEnhancerFromAnnotation的默认回落值影响所有observable系列 APIpackages/mobx/src/types/observablemap.tsObservableMap构造函数的默认enhancer_ deepEnhancer影响observable.map的所有写入packages/mobx/src/types/observableset.tsObservableSet构造函数的默认enhancer_ deepEnhancer并且包装为(newV, oldV) enhancer(newV, oldV, name_)供每次add调用影响observable.set的所有写入packages/mobx/src/types/observableannotation.tsobservable注解在defineObservableProperty_中默认使用deepEnhancer影响makeObservable/makeAutoObservable装饰属性的写入同文件第 74-85 行的lazyObservableKeys_懒加载分支同样回落到deepEnhancer。以ObservableSet为例observableset.ts构造时的 enhancer 被固定为带 name 的包装函数后续每次add都会经过它——因此向 Set 写入字符串这类操作在快速路径下每次写入省掉的都是完整的一套容器类型检查这正是 20-25% 提速的来源。同理可观察数组在初始化时会为每个元素调用 enhancer元素越多、原始值占比越高4 倍量级的创建提速就越明显。从测试代码 packages/mobx/tests/base/make-observable.ts 还可以看到deepEnhancer也作为公开的 internal API 被测试直接引用import { deepEnhancer } from ../../src/internal用于构造ObservableMap/ObservableSet的子类测试印证了它是 MobX 内部被广泛依赖的基础构件。六、实践意义与注意事项对使用 MobX 的开发者而言这条优化是透明且无侵入的无需改代码快速路径只改变deepEnhancer的内部执行顺序不改变任何 API 语义。凡是之前用observable、observable.array、observable.map、observable.set或observable写原始值的地方升级后自动受益保持预期原始值写入后依然是普通值可通过toJS得到原样数据嵌套对象、数组、Map、Set 依然会被递归转换为可观察结构函数依然会被转换为flow/autoAction行为完全不变性能敏感场景受益明显批量构建大数组如表单字段列表、ID 集合、高频向 Map/Set 写入标量键值的场景是本次优化收益最直观的落点changeset 声明为patch级别也表明这是纯性能改进、无行为破坏的修复性变更。如果希望在自己的机器上复核收益可以按第四节的方式先构建生产产物再运行 perf 套件并通过git log/ changeset 记录对比优化前后PERSIST模式下两次测量的perf_report/perf.txt的Create array、Set/Map setting and deleting等指标。七、小结.changeset/deep-enhancer-primitive-fast-path.md虽然只是一条简短的变更记录但它完整概括了 MobX 一次小而精的性能优化利用原始值永不可观察这一核心不变式在deepEnhancer入口处为原始值建立快速路径跳过所有容器类型检查直接返回。结合 packages/mobx/src/types/modifiers.ts 的源码实现与 packages/mobx/tests/perf/perf.js 的性能套件可以完整还原这条优化从原理到验证的闭环——这也正是阅读 MobX changeset 并追溯源码的价值所在每一条 patch 背后都藏着对热路径的精雕细琢。【免费下载链接】mobxSimple, scalable state management.项目地址: https://gitcode.com/gh_mirrors/mo/mobx创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表