48. 精读《Immer.js》源码

论坛 期权论坛 期权     
前端精读周刊   2019-7-29 01:09   3637   0
本周精读的仓库是 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}虽然代码并不复杂,但写起来内心仍隐隐作痛。我们必须将
  1. products
复制代码
拷贝一份,再调用
  1. push
复制代码
函数修改新的
  1. cloneProducts
复制代码
,再返回它。
如果 js 原生支持 Immutable,就可以直接使用
  1. push
复制代码
了!对,Immer 让 js 现在就支持:
const addProducts = produce(products => {  products.push({ text: "shoes" })})很有趣吧,这两个
  1. addProducts
复制代码
函数功能一模一样,而且都是纯函数。
[h2]别扭的 setState[/h2]我们都知道,react 框架中,
  1. setState
复制代码
支持函数式写法:
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。我们看
  1. produce
复制代码
函数 callback 部分:
produce(obj, draft => {  draft.count++})
  1. obj
复制代码
是个普通对象,那黑魔法一定出现在
  1. draft
复制代码
对象上,Immer 给
  1. draft
复制代码
对象的所有属性做了监听。
所以整体思路就有了:
  1. draft
复制代码
  1. obj
复制代码
的代理,对
  1. draft
复制代码
mutable 的修改都会流入到自定义
  1. setter
复制代码
函数,它并不修改原始对象的值,而是递归父级不断浅拷贝,最终返回新的顶层对象,作为
  1. produce
复制代码
函数的返回值。
[h2]生成代理[/h2]第一步,也就是将
  1. obj
复制代码
转为
  1. draft
复制代码
这一步,为了提高 Immutable 运行效率,我们需要一些额外信息,因此将
  1. obj
复制代码
封装成一个包含额外信息的代理对象:
{  modified, // 是否被修改过  finalized, // 是否已经完成(所有 setter 执行完,并且已经生成了 copy)  parent, // 父级对象  base, // 原始对象(也就是 obj)  copy, // base(也就是 obj)的浅拷贝,使用 Object.assign(Object.create(null), obj) 实现  proxies, // 存储每个 propertyKey 的代理对象,采用懒初始化策略}在这个代理对象上,绑定了自定义的
  1. getter
复制代码
  1. setter
复制代码
,然后直接将其扔给
  1. produce
复制代码
执行。
[h2]getter[/h2]
  1. produce
复制代码
回调函数中包含了用户的
  1. mutable
复制代码
代码。所以现在入口变成了
  1. getter
复制代码
  1. setter
复制代码
  1. getter
复制代码
主要用来懒初始化代理对象,也就是当代理对象子属性被访问的时候,才会生成其代理对象。
这么说比较抽象,举个例子,下面是原始 obj:
{  a: {},  b: {},  c: {}}那么初始情况下,
  1. draft
复制代码
  1. obj
复制代码
的代理,所以访问
  1. draft.a
复制代码
  1. draft.b
复制代码
  1. draft.c
复制代码
时,都能触发
  1. getter
复制代码
  1. setter
复制代码
,进入自定义处理逻辑。可是对
  1. draft.a.x
复制代码
就无法监听了,因为代理只能监听一层。
代理懒初始化就是要解决这个问题,当访问到
  1. draft.a
复制代码
时,自定义
  1. getter
复制代码
已经悄悄生成了新的针对
  1. draft.a
复制代码
对象的代理
  1. draftA
复制代码
,因此
  1. draft.a.x
复制代码
相当于访问了
  1. draftA.x
复制代码
,所以能递归监听一个对象的所有属性。
同时,如果代码中只访问了
  1. draft.a
复制代码
,那么只会在内存生成
  1. draftA
复制代码
代理,
  1. b
复制代码
  1. c
复制代码
属性因为没有访问,因此不需要浪费资源生成代理
  1. draftB
复制代码
  1. draftC
复制代码

当然 Immer 做了一些性能优化,以及在对象被修改过(
  1. modified
复制代码
)获取其
  1. copy
复制代码
对象,为了保证
  1. base
复制代码
是不可变的,这里不做展开。
[h2]setter[/h2]当对
  1. draft
复制代码
修改时,会对
  1. base
复制代码
也就是原始值进行浅拷贝,保存到
  1. copy
复制代码
属性,同时将
  1. modified
复制代码
属性设置为
  1. true
复制代码
。这样就完成了最重要的 Immutable 过程,而且浅拷贝并不是很消耗性能,加上是按需浅拷贝,因此 Immer 的性能还可以。
同时为了保证整条链路的对象都是新对象,会根据
  1. parent
复制代码
属性递归父级,不断浅拷贝,直到这个叶子结点到根结点整条链路对象都换新为止。
完成了
  1. modified
复制代码
对象再有属性被修改时,会将这个新值保存在
  1. copy
复制代码
对象上。
[h2]生成 Immutable 对象[/h2]当执行完
  1. produce
复制代码
后,用户的所有修改已经完成(所以 Immer 没有支持异步),如果
  1. modified
复制代码
属性为
  1. false
复制代码
,说明用户根本没有改这个对象,那直接返回原始
  1. base
复制代码
属性即可。
如果
  1. modified
复制代码
属性为
  1. true
复制代码
,说明对象发生了修改,返回
  1. copy
复制代码
属性即可。但是
  1. setter
复制代码
过程是递归的,
  1. draft
复制代码
的子对象也是
  1. draft
复制代码
(包含了
  1. base
复制代码
  1. copy
复制代码
  1. modified
复制代码
等额外属性的代理),我们必须一层层递归,拿到真正的值。
所以在这个阶段,所有
  1. draft
复制代码
  1. finalized
复制代码
都是
  1. false
复制代码
  1. copy
复制代码
内部可能还存在大量
  1. draft
复制代码
属性,因此递归
  1. base
复制代码
  1. copy
复制代码
的子属性,如果相同,就直接返回;如果不同,递归一次整个过程(从这小节第一行开始)。
最后返回的对象是由
  1. base
复制代码
的一些属性(没有修改的部分)和
  1. copy
复制代码
的一些属性(修改的部分)最终拼接而成的。最后使用
  1. freeze
复制代码
冻结
  1. copy
复制代码
属性,将
  1. finalized
复制代码
属性设置为
  1. true
复制代码

至此,返回值生成完毕,我们将最终值保存在
  1. copy
复制代码
属性上,并将其冻结,返回了 Immutable 的值。
Immer 因此完成了不可思议的操作:Create the next immutable state by mutating the current one。
源码读到这里,发现 Immer 其实可以支持异步,只要支持 produce 函数返回 Promise 即可。最大的问题是,最后对代理的
  1. revoke
复制代码
清洗,需要借助全局变量,这一点阻碍了 Immer 对异步的支持。
[h1]4 总结[/h1]读到这,如果觉得不过瘾,可以看看 redux-box 这个库,利用 immer + redux 解决了 reducer 冗余
  1. return
复制代码
的问题。
同样我们也开始思考并设计新的数据流框架,笔者在 2018.3.24 的携程技术沙龙将会分享 《mvvm 前端数据流框架精讲》,分享这几年涌现的各套数据流技术方案研究心得,感兴趣的同学欢迎报名参加。
[h1]5 更多讨论[/h1]
讨论地址是:精读《Immer.js》源码》 · Issue #68 · dt-fe/weekly
如果你想参与讨论,请点击这里,每周都有新的主题,每周五发布。

分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:10
帖子:2
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP