本文参照源文档 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差不多