immer.js实现不可变数据结构

2020-09-27124次阅读javascript

Immer是mobx的作者写的一个immutable库,核心实现是利用ES6的proxy,几乎以最小的成本实现了js的不可变数据结构,简单易用、体量小巧、设计巧妙,满足了我们对JS不可变数据结构的需求。

 

数据处理存在的问题

JS里面的变量类型可以分为基本类型和引用类型 。

在使用过程中,引用类型经常会产生一些无法意识到的副作用,所以在现代JS开发过程中,有经验的开发者都会在特定位置有意识的写下断开引用的不可变数据类型。

// 因为引用所带来的副作用:
var a = [{ val: 1 }]
var b = a.map(item => item.val = 2)

// 期望:b 的每一个元素的 val 值变为 2,但最终 a 里面每个元素的 val 也变为了 2
console.log(a[0].val) // 2


我们可以使用 Object.assign 或者 ... 对对象进行解构,成功断掉一层的引用。 例如上面的问题我们可以改用下面的这种写法:

var a = [{ val: 1 }]
var b = a.map(item => ({ ...item, val: 2 }))

console.log(a[0].val) // 1
console.log(b[0].val) // 2

但是这样做会有另外一个问题,无论是 Object.assign 还是 ... 的解构操作,断掉的引用也只是一层,如果对象嵌套超过一层,这样做还是有一定的风险。再说深度拷贝成本较高,会影响性能。

 

immer.js实现

先定义一个初始对象,供后面例子使用: 首先定义一个currentState对象,后面的例子使用到变量currentState时,如无特殊声明,都是指这个

currentState对象
let currentState = {
  p: {
    x: [2]
  },
}

不小心修改原始对象:

// Q1
let o1 = currentState;
o1.p = 1; // currentState 被修改了
o1.p.x = 1; // currentState 被修改了

// Q2
fn(currentState); // currentState 被修改了
function fn(o) {
  o.p1 = 1;
  return o;
};

// Q3
let o3 = {
  ...currentState
};
o3.p.x = 1; // currentState 被修改了

// Q4
let o4 = currentState;
o4.p.x.push(1); // currentState 被修改了

immer实现不可变数据结构:

//immer修复Q1、Q3
import produce from 'immer';
let o1 = produce(currentState, draftState => {
  draftState.p.x = 1;
})

//immer修复Q2
import produce from 'immer';
fn(currentState);
function fn(o) {
  return produce(o, draftState => {
    draftState.p1 = 1;
  })
};

//immer修复Q4
import produce from 'immer';
let o4 = produce(currentState, draftState => {
  draftState.p.x.push(1);
})

immer利用高阶函数的特点还可以提前生成一个生产者producer

import produce from 'immer'
let producer = produce((draftState) => {
  draftState.p.x.push(1);
});
let o4 = producer(currentState);

 

immer优化react项目的探索

首先定义一个state对象,后面的例子使用到变量state或访问this.state时,如无特殊声明,都是指这个state对象

state = {
  members: [
    {
      name: 'ronffy',
      age: 30
    }
  ]
}

需求让members成员中的第1个成员,年龄增加1岁。

setState的第1种实现方法

const { members } = this.state;
this.setState({
  members: [
    {
      ...members[0],
      age: members[0].age + 1,
    },
    ...members.slice(1),
  ]
})

setState的第2种实现方法

this.setState(state => {
  const { members } = state;
  return {
    members: [
      {
        ...members[0],
        age: members[0].age + 1,
      },
      ...members.slice(1)
    ]
  }
})

如果用Immer解决?

 

用immer更新state

this.setState(produce(draft => {
  draft.members[0].age++;
}))

是不是瞬间代码量就少了很多,阅读起来舒服了很多,而且更易于阅读了。

 

优化reducer

  • reducer形式为(state, action) => state的纯函数,描述了action如何把state转变成下一个state。
  • state的形式取决于你,可以是基本类型、数组、对象、甚至是 Immutable.js 生成的数据结构。
  • 惟一的要点是当state变化时需要返回全新的对象,而不是修改传入的参数。

普通reducer怎样解决上面抛出的需求

const reducer = (state, action) => {
  switch (action.type) {
    case 'ADD_AGE':
      const { members } = state;
      return {
        ...state,
        members: [
          {
            ...members[0],
            age: members[0].age + 1,
          },
          ...members.slice(1),
        ]
      }
    default:
      return state
  }
}

集合immer,reducer可以怎样写

const reducer = (state, action) => produce(state, draft => {
  switch (action.type) {
    case 'ADD_AGE':
      draft.members[0].age++;
  }
})

可以看到通过 produce ,我们的代码量已经精简了很多;

不过仔细观察不难发现,利用高阶函数的特点,通过produce提前生成一个生产者producer,代码还能更优雅:

const reducer = produce((draft, action) => {
  switch (action.type) {
    case 'ADD_AGE':
      draft.members[0].age++;
  }
})

相关推荐:

https://immerjs.github.io/immer/docs/introduction

https://juejin.im/post/6844904024693555213

https://juejin.im/post/6844904111402385422

https://github.com/ronffy/immer-tutorial/blob/master/README.md

上一篇: TypeScript中高级类型简介  下一篇: React-Router简介  

immer.js实现不可变数据结构相关文章