一、引言
immer.js是mobx作者写的一个不可变数据库,核心是使用了ES6的Proxy和ES5的defineProperty,它的基本思想是,将所有更改都应用于临时的draftState,它是currentState的代理。一旦完成所有变更,Immer将基于到draftState的变更产生nextState。这意味着可以通过简单地修改数据而与数据进行交互,同时保留不可变数据的所有优点(避免一处变更处处变更、节省内存空间)。
二、概述
1. 安装immer
// yarn
yarn add immer
// npm
npm install immer
为确保Immer尽可能小,需选择加入每个项目不需要的功能,并且必须明确启用这些功能。这样可以确保未使用的功能不会占用任何空间。(请确认immer版本大于6)
支持 ES5 | 如果需要能够在较旧的JavaScript环境(例如Internet Explorer或React Native)上运行,请启用此功能 | enableES5() |
---|---|---|
支持 ES6的Map和Set | 如果要使Immer可以在原生 Map和Set上运行,请启用此功能 | enableMapSet() |
支持JSON Patch | 在producer运行期间,Immer可以记录所有做更改的patches。如果要更改重朔到原始状态,请启用此功能。 | enablePatches() |
支持所有功能 | enableAllPlugins() |
// 在项目的入口文件index.js
import {enableES5, enableMapSet} from "immer"
enableES5()
enableMapSet()
2.基础api介绍
1.produce: (currentState: T, producer: (draftState: T) => void): nextState
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
})
console.log(nextState[1].done) // true
console.log(baseState[1].done) // false
console.log(baseState[0] === nextState[0]) // true
注:由produce产生的nextState会被自动冻结,不可直接修改,想关闭这个功能可以使用setAutoFreeze(false)
producer的返回值
producer 是否有返回值,nextState 的生成过程是不同的:
producer 没有返回值时:nextState 是根据函数内的 draftState 生成的;
producer 有返回值时:nextState 是根据函数的返回值生成的;
import produce from "immer"
const baseState = [
{
todo: "Learn typescript",
done: true
},
{
todo: "Try immer",
done: false
}
]
const nextState = produce(baseState, draftState => {
return [{
todo: "Try immer",
}]
})
console.log(nextState) // [{todo: "Try immer"}]
如果想要返回undefined,可以使用nothing
import produce, {nothing} from "immer"
const state = {
hello: "world"
} // Produces a new state, 'undefined'
produce(state, draft => nothing)
2.柯里化produce
// 将函数作为第一个参数传递给produce,这样就会得到一个预绑定的producer : produce((state, index) => state),producer接收的参数会被传递给该函数
const producer = produce((draft, index) => {
draft.index = index
}) // example usage
console.dir([{}, {}, {}].map(producer)) //[{index: 0}, {index: 1}, {index: 2}])
// 可以给producer传递第二个参数作为初始状态值
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"}
}
)
优化reducer
// 没用Immer
const reducer = (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
const reducer = (state, action) => produce((state, action) => {
switch (action.type) {
case RECEIVE_PRODUCTS:
action.products.forEach(product => {
draft[product.id] = product
})
}
default:
return
})
优化setState
// 没用Immer
this.setState(state => ({
x: {
...x,
y: 1,
}
}))
// 用immer
this.setState(produce(state => {
state.x.y = 1
}))
3.original和current
original 和 current会拷贝draft的原始状态和当前状态并缓存下来,由original和current生成的状态不会被自动冻结
import {current, original} from 'immer'
const base = {
x: 0
}
const next = produce(base, draft => {
draft.x++
const orig = original(draft)
const copy = current(draft)
console.log(orig.x)
console.log(copy.x)
setTimeout(() => {
// this will execute after the produce has finised!
console.log(orig.x)
console.log(copy.x)
}, 100)
draft.x++
console.log(draft.x)
})
console.log(next.x)
// This will print
// 0 (orig.x)
// 1 (copy.x)
// 2 (draft.x)
// 2 (next.x)
// 0 (after timeout, orig.x)
// 1 (after timeout, copy.x)
4.patches 补丁功能
通过此功能,可以方便进行详细的代码调试和跟踪,可以知道 producer 内的做的每次修改。注:需要在入口文件开启enablePatches()
// patch对象
interface Patch {
op: "replace" | "remove" | "add" // 一次更改的动作类型
path: (string | number)[] // 此属性指从树根到被更改树杈的路径
value?: any // op为 replace、add 时,才有此属性,表示新的赋值
}
// 通过 patchListener 函数,暴露正向和反向的补丁数组
produce(
currentState,
producer,
patchListener: (patches: Patch[], inversePatches: Patch[]) => void
)
// 应用补丁功能
applyPatches(currentState, changes: (patches | inversePatches)[]): nextState
例子:
import produce, { applyPatches, Patch } from 'immer'
interface State {
readonly x: number
y?: number
}
const state: State = {
x: 0,
}
let replace: Patch[] = []
let inverseReplaces: Patch[] = []
let newState = produce(
state,
draft => {
// `x` can be modified here
draft.x += 1
draft.y = 2
},
(patches, inversePatches) => {
replace = patches
inverseReplaces = inversePatches
}
)
console.log('state1', newState)
// { x: 1, y: 2 }
newState = applyPatches(newState, inverseReplaces)
console.log('state2', newState)
// {x: 0}
console.log(replace, inverseReplaces)
// patches
[
{
op: "replace",
path: ["x"],
value: 1
},
{
op: "add",
path: ["y"],
value: 2
},
]
// inversePatches
[
{
op: "replace",
path: ["x"],
value: 0
},
{
op: "remove",
path: ["y"]
},
]
3.use-immer: Immer 同时提供了一个 React hook 库 use-immer
用于以 hook 方式使用 immer
// 更新时将函数作为参数
function App() {
const [person, updatePerson] = useImmer({
name: "Michel",
age: 33
});
function updateName(name) {
updatePerson(draft => {
draft.name = name;
});
}
}
// 接受值作为参数,类似useState
function App(){
const [age, setAge] = useImmer(20);
function birthDay(event){
setAge(age + 1);
}
}
三、对比immutable.js
immer.js相较于immutable.js的优点
1: 体积更小
2: 语法更简单,不需要重新学习新的语法,这很容易和原生Map的语法混淆
3: 不需要再使用toJS()将数据转换为js格式,有时候忘记这一步会引入bug