前言
- immer 的作者同时也是 mobx 的作者。mobx 又像是把 Vue 的一套东西融合进了 React
- 安装: npm install immer
例子
需要更新第二个 todo,并添加第三个。但是,我们不想改变原始的 currentState,我们也想避免深度克隆(以保留第一个 todo)
const currentState = [
{
title: 'Learn TypeScript',
done: true
},
{
title: 'Try Immer',
done: false
}
]
不使用 Immer
const nextState = currentState.slice() // 浅拷贝数组
// 更新第二个 todo
nextState[1] = {
...nextState[1], // 浅拷贝第一层元素
done: true // 期望的更新
}
// 添加第三个
// 因为 nextState 是新拷贝的, 所以使用 push 方法是安全的,
// 但是在未来的任意时间做相同的事情会违反不变性原则并且导致 bug!
nextState.push({title: 'Tweet about it'})
使用 Immer
- 利用 produce 函数,它将我们要更改的 state 作为第一个参数
- 对于第二个参数,我们传递一个名为 recipe 的函数,该函数传递一个 draftState 参数,我们可以对其应用直接的 mutations
- 一旦 recipe 执行完成,这些 mutations 被记录并用于产生下一个状态 nextState
- produce 将负责所有必要的复制,并通过冻结数据来防止未来的意外修改
import produce from 'immer'
const nextState = produce(currentState, draftState => {
draftState[1].done = true
draftState.push({title: 'Tweet about it'})
})
基本概念
- currentState: 被操作对象的最初状态
- draftState: 根据currentState生成的草稿、是currentState的代理、对draftState所有的修改都被记录并用于生成nextState。在此过程中,currentState不受影响
- nextState: 根据draftState生成的最终状态
- produce: 用于生成nextState或者producer的函数
- Producer: 通过produce生成,用于生产nextState,每次执行相同的操作
- recipe: 用于操作draftState的函数
结合react使用
useState + Immer
import React, { useCallback, useState } from 'react'
import produce from 'immer'
const TodoList = () => {
const [todos, setTodos] = useState([
{
id: 'React',
title: 'Learn React',
done: true
},
{
id: 'Immer',
title: 'Try Immer',
done: false
}
])
const handleToggle = useCallback((id) => {
setTodos(
produce(draft => {
const todo = draft.find((todo) => todo.id === id)
todo.done = !todo.done
})
)
}, [])
const handleAdd = useCallback(() => {
setTodos(
produce(draft => {
draft.push({
id: 'todo_' + Math.random(),
title: 'A new todo',
done: false
})
})
)
}, [])
return (<div>{*/ See CodeSandbox */}</div>)
}
useImmer
import React, { useCallback } from 'react'
import { useImmer } from 'use-immer'
const TodoList = () => {
const [todos, setTodos] = useImmer([
{
id: 'React',
title: 'Learn React',
done: true
},
{
id: 'Immer',
title: 'Try Immer',
done: false
}
])
const handleToggle = useCallback((id) => {
// 相对于useState + Immer,简化了produce
setTodos(draft => {
const todo = draft.find((todo) => todo.id === id)
todo.done = !todo.done
})
}, [])
const handleAdd = useCallback(() => {
// 相对于useState + Immer,简化了produce
setTodos(draft => {
draft.push({
id: 'todo_' + Math.random(),
title: 'A new todo',
done: false
})
})
}, [])
useReducer + Immer
import React, {useCallback, useReducer} from 'react'
import produce from 'immer'
const TodoList = () => {
const [todos, dispatch] = useReducer(
produce((draft, action) => {
switch (action.type) {
case 'toggle':
const todo = draft.find(todo => todo.id === action.id)
todo.done = !todo.done
break
case 'add':
draft.push({
id: action.id,
title: 'A new todo',
done: false
})
break
default:
break
}
}),
[
/* initial todos */
]
)
const handleToggle = useCallback(id => {
dispatch({
type: 'toggle',
id
})
}, [])
const handleAdd = useCallback(() => {
dispatch({
type: 'add',
id: 'todo_' + Math.random()
})
}, [])
}
useImmerReducer
import React, { useCallback } from 'react'
import { useImmerReducer } from 'use-immer'
const TodoList = () => {
const [todos, dispatch] = useImmerReducer(
// 相对于useReducer + Immer,简化了produce
(draft, action) => {
switch (action.type) {
case 'toggle':
const todo = draft.find((todo) => todo.id === action.id)
todo.done = !todo.done
break
case 'add':
draft.push({
id: action.id,
title: 'A new todo',
done: false
})
break
default:
break
}
},
[ /* initial todos */ ]
)
}
Redux + Immer
import produce from 'immer'
// 初始 state
const INITIAL_STATE = [
/* 一系列 todos */
]
const todosReducer = produce((draft, action) => {
switch (action.type) {
case 'toggle':
const todo = draft.find(todo => todo.id === action.id)
todo.done = !todo.done
break
case 'add':
draft.push({
id: action.id,
title: 'A new todo',
done: false
})
break
default:
break
}
})
Immer 和 Immutable 的比较
Immer
- 优点
- 上手容易: API 简单上手容易,同时项目迁移改造也比较容易
- 体积小: 仅有12KB
- 缺点
- 兼容性差一点: 需要环境支持 Proxy 和 defineProperty,否则无法使用
- 运行效率不稳定: 运行效率受到环境因素影响较大,对于不支持 proxy 的浏览器使用 defineProperty 实现,在性能上为 proxy 的两倍
Immutable
- 优点
- 兼容性好: 支持编译为 ES3 代码,适合所有 JS 环境
- 运行效率稳定: 效率整体来说比较平稳,但是在转化过程中要先执行 fromJS 和 toJS,所以需要一部分前期效率的成本
- 缺点
- 上手难一点: API 丰富,上手复杂的多,使用 immutable 做项目迁移或者改造起来会稍微复杂一些
- 体积大一点: 库的体积大约在 63KB
- JavaScript 的数据类型和 Immutable 需要相互转换,有入侵性,开发中需要时刻关注操作的是原生对象还是 Immutable对象 返回的结果