Skip to content

【技术干货第13期】介绍Immer-现代化的React状态管理工具 #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
fanchangyong opened this issue Oct 12, 2019 · 0 comments

Comments

@fanchangyong
Copy link
Owner

前言

上篇文章中我们介绍了ES6 Proxy的机制,其中提到了Immer这个库,我当初想了解Proxy也就是因为Immer。Immer是基于Proxy实现的,我们今天就来聊一下这个库。

Immer是什么

Immer是一个让对象的非破坏性操作变得更方便的库。我们前面有两篇文章在讲如何对一个对象做非破坏性的操作,其实你可以看到,像之前介绍的那样使用JavaScript语言原生的写法还是比较复杂且容易出错的,尤其是在对象结构嵌套较深的情况下。而Immer就是用来解决这一问题的,它允许我们使用一般的破坏性语法来达到非破坏性的效果,并且和其他类似的库不同,一切都是使用原生JS的语法,几乎没有任何的学习成本。
接下来我们就来看一下Immer的使用方式。

开始使用

第一步,使用npmyarn来安装。

> npm install immer
# 或者
> yarn add immer

接下来我们看一个代码示例,这几乎就是我们使用Immer之前需要学习的所有东西了,你可以看到使用是多么的简单:

// Immer默认导出一个函数,这也是整个Immer的核心函数,一般命名为produce
import produce from "immer"

// 这个对象和我们前面讲解嵌套对象的那篇文章中使用的一样
// 你可以对比一下使用immer有多方便

var baseState = {
  id: 1,
  name: "Mike",
  personalInfo: {
    account: {
      phone: '18612345678',
      email: '[email protected]',
    },
    address: {
      province: 'beijing',
      district: 'haidian',
    },
  },
  hobby: ['reading', 'sports'],
}

const nextState = produce(baseState, draftState => {
  // 在这里,我们可以对draftState做任意的操作
  // 我们对draftState所做的修改,会被返回,但原始的baseState对象并不会被修改
  // 比如往数组中添加元素,现在我们就可以直接用array.push方法
  draftState.personalInfo.hobby.push('movie');
  // 在数组中红删除元素,也可以直接使用splice这种破坏性的函数
  draftState.personalInfo.hobby.splice(0, 1)
  // 更改数组元素,可以直接使用下标来赋值
  draftState.personalInfo.hobby[0] = 'running'
  // 对象中增加字段:
  draftState.personalInfo.address.city = 'beijing'
  // 删除字段:
  delete draftState.personalInfo.address.district
  // 替换字段:
  draftState.personalInfo.address.district = 'chaoyang'
  
  // 我们不需要return任何东西
})

核心API

从上面的示例代码中,我们可以看到,Immer默认导出一个函数(通常命名为produce)。这个函数是Immer的核心API函数,我们来看一下它的函数签名:

produce(currentState, producer: (draftState) => void): nextState

它接受两个参数,分别是:currentState和一个producer函数。我们想要对状态做的修改都写在producer函数中。这里最重要的点就在于,我们对draftState所做的修改都会作为produce的返回值返回到nextState中,而currentState不会被修改。

另外,produce还有一个curried版本。这里我们简单介绍一下Currying,中文叫”柯里化“,大概意思就是将一个接受多个参数的函数转换成接受一个参数的函数,然后这个新函数会返回一个函数,来接受剩余的参数,举个例子:

// 假设我们有一个函数接受两个参数
function foo(x, y) {
  return x + y
}

// 如果我们将foo柯里化,那么就实现另一个函数,它只接受一个参数
function bar(x) {
  // 返回另一个函数(这个函数接受剩余的参数),然后它的返回值和foo一致
  return function (y) {
    return x + y
  }
}

如果这里你还是对Currying的概念不太理解,建议去网上找一下资料,我们这里不再做过多解释。

接下来还是看一下Curried produce版本:
如果我们给produce的第一个参数传一个函数,那么它将变为Curried版本,也就是produce的返回值不再是nextState,而是返回另一个函数。然后我们调用它返回的这个函数,来得到nextState。还是来看代码:

// 和普通版本的produce一样引入,因为他们是同一个函数的不同`重载`版本
import produce from 'immer'

// 我们后面传递给`producer`的参数会被传给这里的回调函数中
// 这里我们只传递了一个参数,但其实参数数量是不受限制的
const producer = produce(function(draft) {
  // 这里对draft所做的修改会被返回给调用`producer`的地方
  draft.x = 'x'
})

const currentState = {}
// 这个`currentState`会被传到上面的`draft`
const nextState = producer(currentState)
console.log(nextState) // 输出:{ x: 'x' }

在使用Immer的时候,我觉得大部分情况下只需要了解produce的用法就足够了。当然Immer提供的API不止这一个,但我们这篇文章不会介绍其他的非核心API,如果你感兴趣还是建议去读一遍官方文档。

在React setState中的应用

React整个框架都是构建在Immutable State的思想之上,这一思想也被Redux等框架所继承。我们使用Immer的最终目的基本还是应用在React的state管理之中,所以我们接下来就看一下Immer如何与React结合使用。

我们还是用一段示例代码来解释(代码来自官网):

// 原始的使用JavaScript原生语法的方式
onBirthDayClick1 = () => {
    this.setState(prevState => ({
        user: {
            ...prevState.user,
            age: prevState.user.age + 1
        }
    }))
}

// 使用Immer的方式
onBirthDayClick2 = () => {
    // 使用Immer,produce的返回值就是nextState
    // 我们可以直接将produce的返回值传递给setState
    // produce内部还是和上面一样,可以直接对draftState进行修改,所进行的修改会返回到nextState
    this.setState(
        produce(draft => {
            draft.user.age += 1
        })
    )
}

在Redux reducer中的使用

除了直接应用在setState中,Immer当然也非常适合应用在Redux reducer中,因为本质上redux也是React的state。在Redux reducer中使用Immer,就会用到我们之前提到的Curried produce版本的函数。
我们接下来看一下在reducer中如何应用Immer,还是使用示例代码的方式(代码同样来自Immer官网):

// 1. 这是使用原生JS的写法
const byId = (state, action) => {
    switch (action.type) {
        case RECEIVE_PRODUCTS:
            return {
                ...state,
                ...action.products.reduce((obj, product) => {
                    obj[product.id] = product
                    return obj
                }, {})
            }
        default:
            return state
    }
}

// 2. 使用Immer的写法
import produce from "immer"
// 这里使用`Curried produce`,我们传递一个回调函数给produce
// 回调函数的调用时机就是当这个reducer被执行的时候(也就是通过dispatch触发一个action的时候)
// 它接受的参数就是普通的reducer所接受的`state`和`action`两个参数
// 只是这里按照管理将`state`命名为`draft`
const byId = produce((draft, action) => {
    switch (action.type) {
        case RECEIVE_PRODUCTS:
          // 然后我们在这里直接对draft进行修改即可
            action.products.forEach(product => {
                draft[product.id] = product
            })
    }
})

优点

上面介绍了Immer的基本使用,接下来我们看一下使用Immer会带来哪些好处:

  1. 使用简单,这一点从上面的介绍中你基本可以感受到了。Immer并没有带来什么奇怪的语法,一切都是最基本的JavaScript原生语法的写法,基本没有学习和使用成本。

  2. 全部都基于JavaScript的原生数据类型,而不像ImmutableJS那样引入自己的一套类型系统,那样的话要和系统其他部分互操作是比较麻烦的,Immer完全没有这方面的问题。

  3. 可以渐进式的引入,Immer并不要求我们整个程序统一使用它,我们可以只将它应用在一个reducer中,甚至一个action或者setState中,其他地方还是用原来的写法都可以。

  4. 性能,虽然和经过手动优化的代码慢一些(这是肯定的),但它的性能和ImmutableJS是差不多的,所以一般使用完全不是问题。而且根据第3点,我们完全可以在一些需要手动优化的地方使用原生写法,这是非常自然的。(但需要注意的是,在ES6之前的环境中由于无法使用Proxy,所以性能要更慢一些,大概会比Proxy版本慢一倍)。

总结

本篇文章我们简单介绍了Immer是什么,通过代码示例展示了它的主要使用方式,介绍了Immer的核心API,以及在项目中引入Immer的好处。不知道你读完之后是什么感觉,反正我第一次扫了一眼它的文档就已经被吸引了,下次项目中一定会用上它。但这篇文章的定位是将它简单的介绍给你,如果你决定使用,还是要读一遍官方文档的~

欢迎扫码关注公众号<前端时光机>:

qrcode

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant