immer.js详解

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 对象的所有属性做了监听。

所以整体思路就有了:draftobj 的代理,对 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 代码。所以现在入口变成了 gettersetter

getter 主要用来懒初始化代理对象,也就是当代理对象子属性被访问的时候,才会生成其代理对象。

这么说比较抽象,举个例子,下面是原始 obj:

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

那么初始情况下,draftobj 的代理,所以访问 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 等额外属性的代理),我们必须一层层递归,拿到真正的值。

所以在这个阶段,所有 draftfinalized 都是 falsecopy 内部可能还存在大量 draft 属性,因此递归 basecopy 的子属性,如果相同,就直接返回;如果不同,递归一次整个过程(从这小节第一行开始)。

最后返回的对象是由 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 前端数据流框架精讲》,分享这几年涌现的各套数据流技术方案研究心得,感兴趣的同学欢迎报名参加。

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.shuli.cc/?p=17888,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?