前言
Immutable思想
数据就是一旦创建,就不能更改的数据。每当对 Immutable 对象进行修改的时候,就会返回一个新的 Immutable 对象,以此来保证数据的不可变。
如果不用 Immutable,直接对源数据操作有什么不好?
1.因为通常修改一个引用类型的数据,为了降低对源数据的副作用,一般要进行深拷贝,数据量大的话对性能有影响,效率低
2.数据操作过于灵活,不好追踪在哪修改的数据
关于 Immutable 思想的实现有Immutable.js、immer.js等
而Immutable.js用法相对复杂,immer.js用法简单效率高,所以这篇文章是对 immer.js 源码的分析解读
一、immer 思想及意义
1.不再需要数据复制的防御机制(因为源数据是不变的)
2.优化对数据变化的检测
3.有助于简化开发过程,因为开发者不再需要在代码中追踪数据,寻找数据变更的位置(因为修改后的数据不可变,只能在 produce 理修改)
4.降低修改数据过程中的复杂度(不用深拷贝的,采用原生代理,哪里修改代理哪里,实现数据共享)
5.性能优化,数据共享,只改动被修改的部分(同上)
6.提供草稿功能,保存了每次的修改,相当于提供了数据历史快照的功能
二、熟悉 Immer
来个 demo 先熟悉下 Immer 用法
1 | import produce, { enablePatches, applyPatches } from "./immer"; |
这篇文章讲了啥?
- Immer 的调度流程
- immer 的插件模式
- 文件组织结构
- 实现按需代理的原理
- 将 proxy 转为普通对象的原理
三、Immer 目录结构
1 | | |____immer |
- 核心功能以文件的形式放在了
core
下 - 不同插件以文件的形式放在了
plugins
下 - 剩下一些工具放在了
utils
下 - internal.ts 作为一个入口将上面提到的功能同一暴露出去
四、Immer 功能拆分
组织方式:immer 将主要功能拆分为不同文件,并将这些文件集中放在core
目录下管理
immerClass
scope
proxy
processResult
maybeFreeze
patchListener
pluign系统
下面挨个对这些功能进行分析
1、immerClass
用这个 immer 库的时候,主要用的是produce
这个函数 ,这个函数作为一个 Immer 类的实例方法暴露出来
源码:produce
1 | const immer = new Immer(); |
其中produce
有两种用法,一种是普通传两个参数的调用
1 | nextState = produce(state,draft=>{ ... }) |
一种是高阶函数
1 | producer = produce(draft=>{ ... }) |
produce 方法里统筹调度了整个数据流转的过程,如下
1.1、主流程
produce 主要干了下面的几件事
-
对数据类型判断,只处理基本类型 对象、数组
const scope = enterScope(this);
创建 scope,scope 相当是一个 immer 实例、proxy 的管理器,具体的下面有说
const proxy = createProxy(this, base, undefined)
对 base(源数据)创建代理,并返回给 produce 的第二个函数,就是 draft
-
执行 produce 的第二个参数函数,这里的 draft 就是上面创建的 proxy
1
produce(state,draft=>{ ... })
const m = processResult(result, scope);
解析结果,对于 proxy 的修改都反映到了的代理数据(下面有讲)的 copy_字段,这里是将这些修改组装,反映到 result 上并返回。
主要依赖这俩函数finalize finalizeProperty,将 proxy 类型的值进行递归取值,赋值给 resultmaybeFreeze 数据自动冻结
经过 produce 返回的数据都是冻结状态,这个函数就是冻结数据用的
2、scope github
scope 这个概念在这里相当于一个 proxy 管理器,进行 proxy 历史操作的保存和 proxy 的 revoke
1 | interface ImmerScope { |
patches、inversePatches
历史操作信息并不是在每次操作的时候 push 的,而是在 processResult 阶段通过调用generatePatches_ push 的
当 recipe(produce 第二个参数)执行时,如果出现错误则直接调用 scope 的revokeScope 方法,目的是将所有 proxy 撤销,终止流程
canAutoFreeze_默认就是 true,冻结结果
- drafts_
每次创建的 proxy 都会推入这里存储
- patchListener_
这个字段是个引用,指向的是 procude 第三个参数
- immer_
immerClass 的实例
- unfinalizedDrafts_
这里存储的是未标记为 finallized 的 proxy 的数量,当流程结束时会调用finalize 将 proxy 标记为finalized,同时 unfinalizedDrafts_减 1
如果执行recipe
顺利则执行leaveScope方法标记当前 scope 为完成状态
否则执行revokeScope 方法,将所有 proxy 撤销,终止流程
3、proxy github
这个功能就是 immer 的精髓了,对应这个图
3.1、注解 ❶ ❷
❶ ❷
这个 state 数据结构
1 |
|
创建代理的时候是使用Proxy.revocable创建的,因为用这个创建可以有revoke方法销毁 proxy
关于这个代理后的结构举个例子
1 | let demo = { name: { age: 333 }, fan: { zz: 333 } }; |
提出问题:那这里的 draft 是啥结构呢?
这个 draft 就是上面代理返回的 proxy,它保存的所有的修改信息,数据结构如下
1 | interface Ibase { |
那上面的draft
因为修改了 name 下的 age,那这个 draft 就是这样的
1 | let proxy = (base) => |
draft.name.age = 1000
因为修改的深度是两层,而且只修改了name
下的age
字段,所以只代理的这两个值
可以看到base_
只是存源数据,所有的修改都反映到了 copy_字段上,这个代理有下面的特点
- 每次的代理只代理对象的一层
- 并不是对象所有值都会代理
- 修改只会反映到 copy_字段上
按需代理
3.2、注解 ❸ 源码
1 | // peek直接取得是原始值,这里做的判断,如果相等那就proxy一下,否则不重复代理 |
3.3、注解 ❹ github
3.4、注解 ❺ github
3.5、注解 ❼ github
4、processResult github
这个函数主要干了下面三件事
1、计算出最后结果
2、冻结结果
3、计算补丁patches_
、inversePatches_
后触发补丁函数 patchListener_
这三个功能主要依赖的是
finalize、finalizeProperty 计算结果 result
generatePatches_ 计算 patches、inversePatches
1 | // 这个scope就是produce进来的时候enterScope创建的scope |
现在来简化一下 finalize 流程,看看 immer 是怎么解析出最后结果的
1 | function finalize(rootScope,value){ |
5、maybeFreeze
这个是依赖Object.freeze()
实现
6、patchListener
补丁监听函数,”补丁“指的是操作数据的历史,主要有三个类型REPLACE
、ADD
、REMOVE
patchListener 也是 produce 的第三个参数,它接收两个参数分别是inversePatches
和patches
patches 是操作数据的历史动作
inversePatches 是 patches 反向,这个反向比如 patches 其中一向是 Add,那么在 inversePatches 就是 Remove,也就是说通过 inversePatches 可以反解数据
produce 第三个参数
1 | // 通过 patchListener 函数,暴露正向和反向的补丁数组 |
数组 patch 参数格式
1 | interface Patch { |
通过补丁功能实现数据回溯
1、历史回溯 demo
1 | let stash = []; |
7、plugin 系统
为啥要说 immer 的插件呢,因为用 patchListener 功能的时候必须引入enablePatches()
调用一下才能用,达到了按需使用的效果
immer 设计了一个 plugin 管理器存放在 plugins.ts 文件
主要功能有
1、plugins={}
作为插件表,存放这所有插件
2、loadPlugin
注册插件,其实就是往插件表上存
3、getPlugin
调用插件,去插件表检索返回
所有的插件以文件的形式保存在 plugins 目录下
1 | |____plugins |
这种可插拔的插件模式在自己写功能的时候也可以借鉴,这样能让小功能通过插件的方式引入,即插即用,提高了代码的灵活性,也便于不同功能的维护管理