精读《Immer.js》源码

迭代根系
• 阅读 118

本周精读的仓库是 immer。
1 引言 Immer 是最近火起来的一个项目,由 Mobx 作者 Mweststrate 研发。
了解 mobx 的同学可能会发现,Immer 就是更底层的 Mobx,它将 Mobx 特性发扬光大,得以结合到任何数据流框架,使用起来非常优雅。
2 概述 麻烦的 Immutable Immer 想解决的问题,是利用元编程简化 Immutable 使用的复杂度。举个例子,我们写一个纯函数:

 const addProducts = products => {
  const cloneProducts = products.slice()
  cloneProducts.push({ text: "shoes" })
  return cloneProducts
}

虽然代码并不复杂,但写起来内心仍隐隐作痛。我们必须将

 products

拷贝一份,再调用

 push

函数修改新的

 cloneProducts

,再返回它。
如果 js 原生支持 Immutable,就可以直接使用

 push

了!对,Immer 让 js 现在就支持:

 const addProducts = produce(products => {
  products.push({ text: "shoes" })
})

很有趣吧,这两个

 addProducts

函数功能一摸一样,而且都是纯函数。
别扭的 setState 我们都知道,react 框架中,

 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" })))

方便的柯里化 上面讲述了 Immer 支持柯里化带来的好处。所以我们也可以直接把两个参数一次性消费:

 const oldObj = { value: 1 }
const newObj = produce(oldObj, draft => (draft.value = 2))

这就是 Immer:Create the next immutable state by mutating the current one.
3 精读 虽然笔者之前在这方面已经有所研究,比如做出了 Mutable 转 Immutable 的库:dob-redux,但 Immer 实在是太惊艳了,Immer 是更底层的拼图,它可以插入到任何数据流框架作为功能增强,不得不赞叹 Mweststrate 真的是非常高瞻远瞩。
所以笔者认真阅读了它的源代码,带大家从原理角度认识 Immer。
Immer 是一个支持柯里化,仅支持同步计算的工具,所以非常适合作为 redux 的 reducer 使用。
Immer 也支持直接 return value,这个功能比较简单,所以本篇会跳过所有对 return value 的处理。PS: mutable 与 return 不能同时返回不同对象,否则弄不清楚到哪种修改是有效的。
柯里化这里不做拓展介绍,详情查看 curry。我们看

 produce

函数 callback 部分:

 produce(obj, draft => {
  draft.count++
})
 obj

是个普通对象,那黑魔法一定出现在

 draft

对象上,Immer 给

 draft

对象的所有属性做了监听。
所以整体思路就有了:

 draft

 obj

的代理,对

 draft

mutable 的修改都会流入到自定义

 setter

函数,它并不修改原始对象的值,而是递归父级不断浅拷贝,最终返回新的顶层对象,作为

 produce

函数的返回值。
生成代理 第一步,也就是将

 obj

转为

 draft

这一步,为了提高 Immutable 运行效率,我们需要一些额外信息,因此将

 obj

封装成一个包含额外信息的代理对象:

 {
  modified, // 是否被修改过
  finalized, // 是否已经完成(所有 setter 执行完,并且已经生成了 copy)
  parent, // 父级对象
  base, // 原始对象(也就是 obj)
  copy, // base(也就是 obj)的浅拷贝,使用 Object.assign(Object.create(null), obj) 实现
  proxies, // 存储每个 propertyKey 的代理对象,采用懒初始化策略
}

在这个代理对象上,绑定了自定义的

 getter
 setter

,然后直接将其扔给

 produce

执行。
getter

 produce

回调函数中包含了用户的

 mutable

代码。所以现在入口变成了

 getter

 setter

 getter

主要用来懒初始化代理对象,也就是当代理对象子属性被访问的时候,才会生成其代理对象。
这么说比较抽象,举个例子,下面是原始 obj:

 {
  a: {},
  b: {},
  c: {}
}

那么初始情况下,

 draft

 obj

的代理,所以访问

 draft.a
 draft.b
 draft.c

时,都能触发

 getter
 setter

,进入自定义处理逻辑。可是对

 draft.a.x

就无法监听了,因为代理只能监听一层。
代理懒初始化就是要解决这个问题,当访问到

 draft.a

时,自定义

 getter

已经悄悄生成了新的针对

 draft.a

对象的代理

 draftA

,因此

 draft.a.x

相当于访问了

 draftA.x

,所以能递归监听一个对象的所有属性。
同时,如果代码中只访问了

 draft.a

,那么只会在内存生成

 draftA

代理,

 b
 c

属性因为没有访问,因此不需要浪费资源生成代理

 draftB
 draftC


当然 Immer 做了一些性能优化,以及在对象被修改过(

 modified

)获取其

 copy

对象,为了保证

 base

是不可变的,这里不做展开。
setter 当对

 draft

修改时,会对

 base

也就是原始值进行浅拷贝,保存到

 copy

属性,同时将

 modified

属性设置为

 true

。这样就完成了最重要的 Immutable 过程,而且浅拷贝并不是很消耗性能,加上是按需浅拷贝,因此 Immer 的性能还可以。
同时为了保证整条链路的对象都是新对象,会根据

 parent

属性递归父级,不断浅拷贝,直到这个叶子结点到根结点整条链路对象都换新为止。
完成了

 modified

对象再有属性被修改时,会将这个新值保存在

 copy

对象上。
生成 Immutable 对象 当执行完

 produce

后,用户的所有修改已经完成(所以 Immer 没有支持异步),如果

 modified

属性为

 false

,说明用户根本没有改这个对象,那直接返回原始

 base

属性即可。
如果

 modified

属性为

 true

,说明对象发生了修改,返回

 copy

属性即可。但是

 setter

过程是递归的,

 draft

的子对象也是

 draft

(包含了

 base
 copy
 modified

等额外属性的代理),我们必须一层层递归,拿到真正的值。
所以在这个阶段,所有

 draft

 finalized

都是

 false

 copy

内部可能还存在大量

 draft

属性,因此递归

 base

 copy

的子属性,如果相同,就直接返回;如果不同,递归一次整个过程(从这小节第一行开始)。
最后返回的对象是由

 base

的一些属性(没有修改的部分)和

 copy

的一些属性(修改的部分)最终拼接而成的。最后使用

 freeze

冻结

 copy

属性,将

 finalized

属性设置为

 true


至此,返回值生成完毕,我们将最终值保存在

 copy

属性上,并将其冻结,返回了 Immutable 的值。
Immer 因此完成了不可思议的操作:Create the next immutable state by mutating the current one。
源码读到这里,发现 Immer 其实可以支持异步,只要支持 produce 函数返回 Promise 即可。最大的问题是,最后对代理的

 revoke

清洗,需要借助全局变量,这一点阻碍了 Immer 对异步的支持。
4 总结 读到这,如果觉得不过瘾,可以看看 redux-box 这个库,利用 immer + redux 解决了 reducer 冗余

 return

的问题。
同样我们也开始思考并设计新的数据流框架,笔者在 2018.3.24 的携程技术沙龙将会分享 《mvvm 前端数据流框架精讲》,分享这几年涌现的各套数据流技术方案研究心得,感兴趣的同学欢迎报名参加。
5 更多讨论 讨论地址是:精读《Immer.js》源码》 · Issue #68 · dt-fe/weekly
如果你想参与讨论,请点击这里,每周都有新的主题,每周五发布。
转载于猿2048:➻《精读《Immer.js》源码》

点赞
收藏
评论区
推荐文章
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
GoCoding GoCoding
4年前
React MobX 开始
用于状态管理,简单高效。本文将于React上介绍如何开始,包括了:了解MobX概念从零准备React应用MobXReact.FC写法MobXReact.Component写法可以在线体验:https://ikuokuo.github.io/startreact,代码见:https://github.com/ikuokuo/s
巴拉米 巴拉米
4年前
MobX 上手指南
之前用Redux比较多,一直听说Mobx能让你体验到在React里面写Vue的感觉,今天打算尝试下Mobx是不是真的有写Vue的感觉。题外话在介绍MobX的用法之前,先说点题外话,我们可以看一下MobX的中文简介。在MobX的中文网站上写着:MobX是一个经过战火洗礼的库,它通过透明的函数响应式编程
Kevin501 Kevin501
4年前
Mobx-React:当前最适合React的状态管理工具
MobX_简单、可扩展的状态管理_MobX是由Mendix、Coinbase、Facebook开源和众多个人赞助商(https://cn.mobx.js.org/backers)所赞助的。安装安装: np
Peter20 Peter20
4年前
mysql中like用法
like的通配符有两种%(百分号):代表零个、一个或者多个字符。\(下划线):代表一个数字或者字符。1\.name以"李"开头wherenamelike'李%'2\.name中包含"云",“云”可以在任何位置wherenamelike'%云%'3\.第二个和第三个字符是0的值wheresalarylike'\00%'4\
Wesley13 Wesley13
4年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Stella981 Stella981
4年前
Git远程推送时记住用户名和密码
当使用HTTPS协议推送代码到Git仓库时,发现每次都需要输入密码,操作起来非常麻烦。下面介绍几种免去输入密码的方法。HTTPS协议推送使用HTTPS协议,有一种简单粗暴的方式是在远程地址中带上密码。gitremoteseturloriginhttp://yourname:password@bitbucket.org
Stella981 Stella981
4年前
Hooks结合Mobx简单使用
定义和导出Storeimport{action,observable}from"mobx";//定义一个Store:UserclassUser{name'LL';sex'man';@actiongetName
Stella981 Stella981
4年前
C语言Linix服务器网络爬虫项目(一)项目初衷和网络爬虫概述
一、项目初衷和爬虫概述1.项目初衷  本人的大学毕设就是linux上用c写的一个爬虫,现在我想把它完善起来,让他像一个企业级别的项目。为了重复发明轮子来学习轮子的原理,我们不使用第三方框架(这里是说的是指通常的linux系统编程教材以外的库和接口0,当初我也是在虚拟机上跑的ubuntuputtyvim来开发)来实现。  然而我们造轮
迭代根系
迭代根系
Lv1
鞭个马儿归去也,心急马行迟。
文章
4
粉丝
0
获赞
0