使用immer加快开发速度

5,959 阅读4分钟
本文参照源文档github.com/immerjs/imm…介绍使用immer v3

大家都知道,开发react项目时,推荐使用immutable的数据结构,这样react就能很高效并且正确地检测到数据变化用以确定是否更新UI。

市面上有几款帮助你实现immutable操作的库,immutable.js本身比较中规中矩,提供了一些方法,在必要时你可以调用他们,然而笔者觉得他增加了使用负担,要专门去记各个api,而且最重要的是,经常一不小心或者顺手或者手贱就直接修改obj了!其实我觉得这才是问题的关键,immer的出现就很好的解决了这一痛点。因为他的思路就是把你整个操作包裹起来,不管你是直接push数组还是改obj.field,最后输出的肯定是新的对象。

API

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

第一个参数为你准备要改的对象,第二个参数是个回调,这个回调函数的参数就是他给你复制的一个临时对象,所以你可以对这个做任何操作。最后produce返回新的对象(下一状态),而currentState任然保持不变。

基本例子

import produce from "immer"

const baseState = [
    {
        todo: "Learn typescript",
        done: true
    },
    {
        todo: "Try immer",
        done: false
    }
]

const nextState = produce(baseState, draftState => {
    draftState.push({todo: "Tweet about it"})
    draftState[1].done = true
})

所以你在开发时只需要专注于逻辑即可,不必纠结再怎么保证不可变性。在下面redux的reducer里体现的更明显。

reducer的例子

当收到新的products之后,把这些products按id加入总的state里面

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
    }
}

使用immer后:

import produce from "immer"

const byId = (state, action) =>
    produce(state, draft => {
        switch (action.type) {
            case RECEIVE_PRODUCTS:
                action.products.forEach(product => {
                    draft[product.id] = product
                })
        }
    })

你可以看到,一个加id和对象到另一个对象里面的操作是多简单。而且这里你不用处理默认的情况,因为producer啥都不做的话会返回原对象。

react的setState例子

/**
 * Classic React.setState with a deep merge
 */
onBirthDayClick1 = () => {
    this.setState(prevState => ({
        user: {
            ...prevState.user,
            age: prevState.user.age + 1
        }
    }))
}

/**
 * ...But, since setState accepts functions,
 * we can just create a curried producer and further simplify!
 */
onBirthDayClick2 = () => {
    this.setState(
        produce(draft => {
            draft.user.age += 1
        })
    )
}

对于依赖之前的值的setState,得按第一种方式写,而用produce就可以直接+=。(注意这里produce的用法是produce((draft)=>{}),你不需要再传baseState了,其实用的是下面的语法)

Currying(柯里化,部分求值)的produce

currying的意思是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术, 比如func(a,b,c)变成func(a)(b)(c),redux里的connect就是这个样子的,记得吗。另外Currying是个人名。

当你给produce传入的第一个参数是回调函数的话,produce返回的是一个预绑定回调函数的函数,这个函数接收一个baseState作为参数。

比如前面的例子,produce返回的就是(prevState)=>{}这个函数,所以它可以直接放在setState里面。另一个例子:

// mapper will be of signature (state, index) => state
const mapper = produce((draft, index) => {
    draft.index = index
})

// example usage
console.dir([{}, {}, {}].map(mapper))
//[{index: 0}, {index: 1}, {index: 2}])

这样我们就可以改前面的reducer例子代码更少:

import produce from "immer"

const byId = produce((draft, action) => {
    switch (action.type) {
        case RECEIVE_PRODUCTS:
            action.products.forEach(product => {
                draft[product.id] = product
            })
            return
    }
})

produce生成的函数是接受state作为传入值的,到produce里面就是draft了。另外你可以传第二个参数去初始化state:

import produce from "immer"

const byId = produce(
    (draft, action) => {
        switch (action.type) {
            case RECEIVE_PRODUCTS:
                action.products.forEach(product => {
                    draft[product.id] = product
                })
                return
        }
    },
    {
        1: {id: 1, name: "product-1"}
    }
)

返回undefined

前面说过,默认producer啥都不做的话会返回baseState,然而你显式地return undefined其实也会返回baseState。如果你真想返回undefined,需要返回一个预定义的token:nothing

import produce, {nothing} from "immer"

const state = {
    hello: "world"
}

produce(state, draft => {})
produce(state, draft => undefined)
// Both return the original state: { hello: "world"}

produce(state, draft => nothing)
// Produces a new state, 'undefined'

多种导入方式

import produce from "immer"
import {produce} from "immer"

const {produce} = require("immer")
const produce = require("immer").produce
const produce = require("immer").default

import unleashTheMagic from "immer"
import {produce as unleashTheMagic} from "immer"

异步producer

import produce from "immer"

const user = {
    name: "michel",
    todos: []
}

const loadedUser = await produce(user, async function(draft) {
    draft.todos = await (await window.fetch("http://host/" + draft.name)).json()
})

性能

包大小4.35k,速度跟immutablejs差不多