文章来源于我在组内给同事分享的 Immer 和 immutable 的使用和对比,总结一下。
两个库都是用来解决数据的Immutable 问题的。但是使用上有很大区别。
Shared mutable state is the root of all evil(共享的可变状态是万恶之源)
Immutable
优点
- Immutable 降低了 Mutable 带来的复杂度 可变(Mutable)数据耦合了 Time 和 Value 的概念,造成了数据很难被回溯。
- 节省内存 Immutable.js 使用了 Structure Sharing (结构共享) 会尽量复用内存。没有被引用的对象会被垃圾回收。
import { Map} from 'immutable'; let a = Map({ select: 'users', filter: Map({ name: 'Cam' }) }) let b = a.set('select', 'people'); a === b; // false a.get('filter') === b.get('filter'); // true
上面 a 和 b 共享了没有变化的
filter
节点。 - Undo/Redo(撤销重写),Copy/Paste,甚至时间回溯这些功能做起来小菜一碟
- 并发安全 因为js 单线程,所以没啥卵用
- 拥抱函数式编程 Immutable 本身就是函数式编程中的概念,纯函数式编程比面向对象更适用于前端开发。因为只要输入一致,输出必然一致,这样开发的组件更易于调试和组装。
函数式编程的本质:函数式编程中的函数这个术语不是指计算机中的函数(实际上是Subroutine),而是指数学中的函数,即自变量的映射。也就是说一个函数的值仅决定于函数参数的值,不依赖其他状态。比如sqrt(x)函数计算x的平方根,只要x不变,不论什么时候调用,调用几次,值都是不变的。
缺点
- 容易与原生对象混淆 虽然 Immutable.js 尽量尝试把 API 设计的原生对象类似,有的时候还是很难区别到底是 Immutable 对象还是原生对象,容易混淆操作。 Immutable 中的 Map 和 List 虽对应原生 Object 和 Array,但操作非常不同,比如你要用
map.get('key')
而不是map.key
,array.get(0)
而不是array[0]
。另外 Immutable 每次修改都会返回新对象,也很容易忘记赋值。 - 当使用外部库的时候,一般需要使用原生对象,也很容易忘记转换。
下面给出一些办法来避免类似问题发生:
- 使用 Flow 或 TypeScript 这类有静态类型检查的工具
- 约定变量命名规则:如所有 Immutable 类型对象以
$$
开头。 - 使用
Immutable.fromJS
而不是Immutable.Map
或Immutable.List
来创建对象,这样可以避免 Immutable 和原生对象间的混用
API
太多了,图上也不是很全
总结: 功能很好,就是太难用,侵入性太强。放弃!
Immer
常用API就一个produce
,掌握之后就可以应对绝大多数业务场景
produce
几个基本概念
- currentState 被操作对象的最初状态
- draftState 根据 currentState 生成的草稿状态,它是 currentState 的代理,对 draftState 所做的任何修改都将被记录并用于生成 nextState 。在此过程中,currentState 将不受影响
- nextState 根据 draftState 的修改生成的最终状态
- recipe 操作方法 用来操作 draftState 的函数
produce(currentState, recipe: (draftState) => void): nextState
produce的第一种使用姿势
let currentState = { x:1, y:1 } let nextState = produce(currentState, (draft) => { draft.x = 2; }) console.log(currentState); // {x:1,y:1} console.log(nextState); // { x: 2, y: 1 }
let currentState = { a: [], p: { x: 1 } } let nextState = produce(currentState, (draft) => { draft.a.push(2); }) currentState.a === nextState.a; // false currentState.p === nextState.p; // true
draftState 的修改都会反应到 nextState 上,而 Immer 使用的结构是共享的,nextState 在结构上又与 currentState 共享未修改的部分,类似于immutable gif的样子
自动冻结
Immer 还在内部做了一件很巧妙的事情,那就是通过 produce 生成的 nextState 是被冻结(freeze)的,(Immer 内部使用Object.freeze方法,只冻结 nextState 跟 currentState 相比修改的部分),这样,当直接修改 nextState 时,将会报错。 这使得 nextState 成为了真正的不可变数据。
let currentState = { a: [], p: { x: [1] } } let nextState = produce(currentState, (draft) => { draft.p.x.push(2); }) // nextState.p.x.push(3); 报错 nextState.a.push(1) nextState.c = 3; // 无效 console.log(nextState); // { a: [ 1 ], p: { x: [ 1, 2 ] } }
produce的第二种使用姿势
produce(recipe: (draftState) => void | draftState)(currentState): nextState
传入的第一个参数是函数
高阶函数的写法,提前生成一个producer方法,当producer 方法被调用的时候,它会把第一个参数用作你希望改变的 currentState
let producer = produce((draft) => { draft.x = 2 }); let nextState = producer(currentState);
正因为有这个操作,所以我们可以很方便使用到setState 中,因为 setState 有接收函数作为参数的能力。
// 想要更新state 中的name = 'BBB'; let producer = produce((draft)=>{ draft.name='BBB' }) this.setState(producer)
注意recipe的返回值
-
recipe 没有返回值时:nextState 是根据 recipe 函数内的 draftState 生成的;
-
recipe 有返回值时:nextState 是根据 recipe 函数的返回值生成的
但是不能同时修改了draftState 也 return 一个新的state,否则会报错,提示你只能修改draft 或者 return value;
let currentState = { name: 'aaa' } let nextState = produce(currentState,(draft)=>{ draft.name = 'bbb'; // return {v:'v'} }) console.log(nextState)
Patches 补丁
Patches 可以在recipe执行期间记录所有操作,方便去做一些数据回滚或者是数据跟踪调试啥的。
patch 对象长这个模样
interface Patch { op: "replace" | "remove" | "add" // 一次更改的动作类型 path: (string | number)[] // 此属性指从树根到被更改树杈的路径 value?: any // op为 replace、add 时,才有此属性,表示新的赋值 }
语法
produce( currentState, recipe, // 通过 patchListener 函数,暴露正向和反向的补丁数组 patchListener: (patches: Patch[], inversePatches: Patch[]) => void )
新API applyPatches
// Demo 1 let state1 = { x: 1, y: 1 } console.log(state1); let patches1 = []; let inversePatches1 = []; let state2 = produce(state1, (draft) => { draft.x = 2; draft.y = 2; }, (patches, inversePatches) => { patches1 = patches; inversePatches1 = inversePatches; }); console.log(state2); let patches2 = []; let inversePatches2 = []; let state3 = produce(state2, (draft) => { draft.x = 3; draft.y = 3; }, (patches, inversePatches) => { patches2 = patches; inversePatches2 = inversePatches; }); console.log(state3); let backState2 = applyPatches(state3,inversePatches2) console.log(backState2); let backState1 = applyPatches(state2,inversePatches1) console.log(backState1);
不允许跨级回滚
let state1 = { list:[ { name:'thor' }, { name:'Rogers' }, ] } console.log(state1); let patches1 = []; let inversePatches1 = []; let state2 = produce(state1, (draft) => { draft.list.push({name:'Natasha'}); draft.list.shift(); }, (patches, inversePatches) => { patches1 = patches; inversePatches1 = inversePatches; }); console.log(state2); let patches2 = []; let inversePatches2 = []; let state3 = produce(state2, (draft) => { delete draft.list; draft.world = [{name:'Marvel'}] }, (patches, inversePatches) => { patches2 = patches; inversePatches2 = inversePatches; }); console.log(state3); let backState2 = applyPatches(state3,inversePatches2) console.log(backState2); let backState1 = applyPatches(state2,inversePatches1) console.log(backState1);
createDraft、finishDraft
let currentState = { x: 1 } let draft = createDraft(currentState); draft.x=2; const nextState = finishDraft(draft); console.log(currentState); console.log(nextState);
Immer 完败 Immutable,用immer 得永生。
文章评论