Immer 的使用以及在react中的应用

2021-12-01  本文已影响0人  黎明的叶子

Immer是mobx的作者写的一个immutable库,核心实现是利用ES6的proxy,几乎以最小的成本实现了JS的不可变数据结构,简单易用、体量小巧、设计巧妙,满足了我们对JS不可变数据结构的需求。原文来源于https://www.cnblogs.com/ronffy/p/immer.html(记录一遍加深理解。)

数据存在的问题

引用类型对象存储地址,一旦被修改,原始的部分也被修改。
如下:

let currentState = {
  p: {
    x: [2],
  },
  a: [],
};
let copyData = currentState
copyData.p = 1 // currentState也被修改了

解决引用类型对象被修改的办法

1.深度拷贝,但是深拷贝的成本较高,会影响性能
2.ImmutableJS,非常棒的一个不可变数据结构的库,可以解决上面的问题,但是跟Immer比起来,ImmutableJS有两个不足:
需要使用者学习它的数据结构操作方式,没有Immer提供的使用原生对象的操作方式简单、易用
它的操作结果需要通过toJS方法才能得到原生对象,这使得在操作一个对象的时候,时刻要注意操作的是原生对象还是ImmutableJS的返回结果。

immer基本使用

语法:

produce(currentState, recipe: (draftState) => void | draftState, ?PatchListener): nextState

常用api介绍

1.produce

//页面引入 
import { produce} from "immer"; 
let currentState = {
  p: {
    x: [2],
  },
  a: [],
};
let nextState = produce(currentState, (draft) => {
  // console.log(draft);// 依据currentState 封装的 Proxy对象
  draft.a.push(1);
});
console.log(nextState === currentState);  //false
console.log(nextState.p === currentState.p); // true
console.log(nextState.a === currentState.a); // false 

由此可见,对 draftState 的修改都会反应到 nextState 上,而 Immer 使用的结构是共享的,nextState 在结构上又与 currentState 共享未修改的部分

自动冻结功能
Immer 还在内部做了一件很巧妙的事情,那就是通过 produce 生成的 nextState 是被冻结(freeze)的,(Immer 内部使用Object.freeze方法,只冻结 nextState 跟 currentState 相比修改的部分),这样,当直接修改 nextState 时,将会报错。
这使得 nextState 成为了真正的不可变数据。

//基于上面代码查看描述
console.log(Object.getOwnPropertyDescriptors(nextState)); //writable: false 
console.log(Object.getOwnPropertyDescriptors(currentState)); //writable: true

对于冻结的对象仍然可以通过immer做处理,因为是基于原来对象做一层代理。

// nextState里面的属性是writeable:false
//这里说明可以对 属性为writeable:false的同样做处理
let nextState2 = produce(nextState,(draft)=>{
  draft.a.push('fd')
})
console.log(nextState2 === nextState);
console.log(nextState.a === nextState2.a);
console.log(nextState.p=== nextState2.p);
console.log(Object.getOwnPropertyDescriptors(nextState)); //writable: false
console.log(Object.getOwnPropertyDescriptors(nextState2));  //writable: false

利用高阶函数的特点,提前生成一个生产者 producer,看起来更简洁

let producer = produce((draft)=>{
  draft.x = 2
})
let nextState = producer(currentState)

2. recipe
返回值
recipe 没有返回值时:nextState 是根据 recipe 函数内的 draftState 生成的;
recipe 有返回值时:nextState 是根据 recipe 函数的返回值生成的;

let nextState = produce(currentState,(draft)=>{
  return {
    x:2
  }
})

** recipe中的this**
recipe 函数内部的this指向 draftState ,也就是修改this与修改 recipe 的参数 draftState ,效果是一样的。
注意:此处的 recipe 函数不能是箭头函数,如果是箭头函数,this就无法指向 draftState 了

let nextState = produce(currentState,(draft)=>{
  // 如果这里不是箭头函数  this 指向 draft   这里是箭头函数 就不是draft了
  return {
    x:2
  }
})

3.patch补丁功能
通过此功能,可以方便进行详细的代码调试和跟踪,可以知道 recipe 内的做的每次修改,还可以实现时间旅行。

import {produce, applyPatches } from "immer"
let state={
  x:1
}

let replaces = []
let inverseReplaces = []

state = produce(state,draft=>{
  draft.x = 2
  draft.y= 2 
},(patches,inversePatches)=>{
  // console.log(patches) // 存储了正向操作记录
  console.log(inversePatches)//  存储了反向操作记录
  replaces = patches.filter(patch=>patch.op ==='replace')
  inverseReplaces =inversePatches.filter(patch=>patch.op ==='replace')
})

state = produce(state,draft=>{
  draft.x = 3
})
// console.log(state) 

state = applyPatches(state, replaces);
console.log('state2', state); // { x: 2, y: 2 }

state = produce(state,draft=>{
  draft.x = 4
})

console.log(state) 

state = applyPatches(state,inverseReplaces)
console.log(state)
// 以上实现了时间旅行  

inversePatches数据结构如下:


inversePatches数据结构-反向操作记录

用immer优化react项目的探索

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

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

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

优化setState方法

错误示例

this.state.members[0].age++;

只所以有的新手同学会犯这样的错误,很大原因是这样操作实在是太方便了,以至于忘记了操作 State 的规则。(就是要通过setState改变值)

下面看下正确的实现方法

const { members } = this.state;
// 可以直接为对象 也可以接受函数
this.setState({
  members: [
    {
      ...members[0],
      age: members[0].age + 1,
    },
    ...members.slice(1),
  ]
})

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

用immer更新state

// 一般写法
this.setState(state=>{
  return produce(state,draft=>{
    draft.members[0].age++;
  })
})
// 高阶写法
this.setState(state=>{
  return produce(draft=>{
    draft.members[0].age++;
  })(state) 
})
// 简洁写法
this.setState(produce(draft => {
  draft.members[0].age++;
}))

普通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 => {
  switch (action.type) {
    case 'ADD_AGE':
      draft.members[0].age++;
  }
})(state, action) 

// immer内部的produce方法 会把以上代码中 除了state后面的参数都给回掉函数,所以可以简写为以下
const reducer = produce((draft, action) => {
  switch (action.type) {
    case 'ADD_AGE':
      draft.members[0].age++;
  }
})
上一篇 下一篇

猜你喜欢

热点阅读