本周精读的仓库是 immer。
[h1]1 引言[/h1]Immer 是最近火起来的一个项目,由 Mobx 作者 Mweststrate 研发。
了解 mobx 的同学可能会发现,Immer 就是更底层的 Mobx,它将 Mobx 特性发扬光大,得以结合到任何数据流框架,使用起来非常优雅。
[h1]2 概述[/h1][h2]麻烦的 Immutable[/h2]Immer 想解决的问题,是利用元编程简化 Immutable 使用的复杂度。举个例子,我们写一个纯函数:
const addProducts = products => { const cloneProducts = products.slice() cloneProducts.push({ text: "shoes" }) return cloneProducts}虽然代码并不复杂,但写起来内心仍隐隐作痛。我们必须将拷贝一份,再调用函数修改新的,再返回它。
如果 js 原生支持 Immutable,就可以直接使用了!对,Immer 让 js 现在就支持:
const addProducts = produce(products => { products.push({ text: "shoes" })})很有趣吧,这两个函数功能一模一样,而且都是纯函数。
[h2]别扭的 setState[/h2]我们都知道,react 框架中,支持函数式写法:
this.setState(state => ({ ...state, isShow: true}))配合解构语法,写起来仍是如此优雅。那数据稍微复杂些呢?我们就要默默忍受 “糟糕的 Immutable” 了:
this.setState(state => { const cloneProducts = state.products.slice() cloneProducts.push({ text: "shoes" }) return { ...state, cloneProducts }})然而有了 Immer,一切都不一样了:
this.setState(produce(state => (state.isShow = true)))this.setState(produce(state => state.products.push({ text: "shoes" })))[h2]方便的柯里化[/h2]上面讲述了 Immer 支持柯里化带来的好处。所以我们也可以直接把两个参数一次性消费:
const oldObj = { value: 1 }const newObj = produce(oldObj, draft => (draft.value = 2))这就是 Immer:Create the next immutable state by mutating the current one.
[h1]3 精读[/h1]虽然笔者之前在这方面已经有所研究,比如做出了 Mutable 转 Immutable 的库:dob-redux,但 Immer 实在是太惊艳了,Immer 是更底层的拼图,它可以插入到任何数据流框架作为功能增强,不得不赞叹 Mweststrate 真的是非常高瞻远瞩。
所以笔者认真阅读了它的源代码,带大家从原理角度认识 Immer。
Immer 是一个支持柯里化,仅支持同步计算的工具,所以非常适合作为 redux 的 reducer 使用。Immer 也支持直接 return value,这个功能比较简单,所以本篇会跳过所有对 return value 的处理。PS: mutable 与 return 不能同时返回不同对象,否则弄不清楚到哪种修改是有效的。 柯里化这里不做拓展介绍,详情查看 curry。我们看函数 callback 部分:
produce(obj, draft => { draft.count++})是个普通对象,那黑魔法一定出现在对象上,Immer 给对象的所有属性做了监听。
所以整体思路就有了:是的代理,对mutable 的修改都会流入到自定义函数,它并不修改原始对象的值,而是递归父级不断浅拷贝,最终返回新的顶层对象,作为函数的返回值。
[h2]生成代理[/h2]第一步,也就是将转为这一步,为了提高 Immutable 运行效率,我们需要一些额外信息,因此将封装成一个包含额外信息的代理对象:
{ modified, // 是否被修改过 finalized, // 是否已经完成(所有 setter 执行完,并且已经生成了 copy) parent, // 父级对象 base, // 原始对象(也就是 obj) copy, // base(也就是 obj)的浅拷贝,使用 Object.assign(Object.create(null), obj) 实现 proxies, // 存储每个 propertyKey 的代理对象,采用懒初始化策略}在这个代理对象上,绑定了自定义的,然后直接将其扔给执行。
[h2]getter[/h2]回调函数中包含了用户的代码。所以现在入口变成了与。主要用来懒初始化代理对象,也就是当代理对象子属性被访问的时候,才会生成其代理对象。
这么说比较抽象,举个例子,下面是原始 obj:
{ a: {}, b: {}, c: {}}那么初始情况下,是的代理,所以访问时,都能触发,进入自定义处理逻辑。可是对就无法监听了,因为代理只能监听一层。
代理懒初始化就是要解决这个问题,当访问到时,自定义已经悄悄生成了新的针对对象的代理,因此相当于访问了,所以能递归监听一个对象的所有属性。
同时,如果代码中只访问了,那么只会在内存生成代理,属性因为没有访问,因此不需要浪费资源生成代理。
当然 Immer 做了一些性能优化,以及在对象被修改过()获取其对象,为了保证是不可变的,这里不做展开。
[h2]setter[/h2]当对修改时,会对也就是原始值进行浅拷贝,保存到属性,同时将属性设置为。这样就完成了最重要的 Immutable 过程,而且浅拷贝并不是很消耗性能,加上是按需浅拷贝,因此 Immer 的性能还可以。
同时为了保证整条链路的对象都是新对象,会根据属性递归父级,不断浅拷贝,直到这个叶子结点到根结点整条链路对象都换新为止。
完成了对象再有属性被修改时,会将这个新值保存在对象上。
[h2]生成 Immutable 对象[/h2]当执行完后,用户的所有修改已经完成(所以 Immer 没有支持异步),如果属性为,说明用户根本没有改这个对象,那直接返回原始属性即可。
如果属性为,说明对象发生了修改,返回属性即可。但是过程是递归的,的子对象也是(包含了等额外属性的代理),我们必须一层层递归,拿到真正的值。
所以在这个阶段,所有的都是,内部可能还存在大量属性,因此递归与的子属性,如果相同,就直接返回;如果不同,递归一次整个过程(从这小节第一行开始)。
最后返回的对象是由的一些属性(没有修改的部分)和的一些属性(修改的部分)最终拼接而成的。最后使用冻结属性,将属性设置为。
至此,返回值生成完毕,我们将最终值保存在属性上,并将其冻结,返回了 Immutable 的值。
Immer 因此完成了不可思议的操作:Create the next immutable state by mutating the current one。源码读到这里,发现 Immer 其实可以支持异步,只要支持 produce 函数返回 Promise 即可。最大的问题是,最后对代理的清洗,需要借助全局变量,这一点阻碍了 Immer 对异步的支持。 [h1]4 总结[/h1]读到这,如果觉得不过瘾,可以看看 redux-box 这个库,利用 immer + redux 解决了 reducer 冗余的问题。同样我们也开始思考并设计新的数据流框架,笔者在 2018.3.24 的携程技术沙龙将会分享 《mvvm 前端数据流框架精讲》,分享这几年涌现的各套数据流技术方案研究心得,感兴趣的同学欢迎报名参加。 [h1]5 更多讨论[/h1]讨论地址是:精读《Immer.js》源码》 · Issue #68 · dt-fe/weekly 如果你想参与讨论,请点击这里,每周都有新的主题,每周五发布。
|
|