不可变数据实现-Immer.js
一、 Immer.js是什么?
Immer.js
是
mobx
的作者写的一个
Immutable
(不可变数据) 库,同时
Immer
在2019年获得 JavaScript Open Source Award 大奖。核心实现是利用 ES6 的
proxy
,几乎以最小的成本实现了
JavaScript
的不可变数据结构,简单易用、体量小巧、设计巧妙,满足了我们对 JS 不可变数据结构的需求。
二、 什么是不可变数据?
首先需要先理解什么是可变数据:举个例子
let objA = { name: '小明' };
let objB = objA;
objB.name = '小红';
console.log(objA.name); // objA 的name也变成了小红
像这样我们明明只修改代码
objB
的
name
,发现
objA
也发生了改变,这个就是可变数据。
那不可变数据是什么怩?
不可变数据概念来源于函数式编程。函数式编程中,对已初始化的“变量”是不可以更改的,每次更改都要创建一个新的“变量”。新的数据进行有副作用的操作都不会影响之前的数据。这也就是
Immutable
的本质。
JavaScript
在语言层没有实现不可变数据,需要借助第三方库来实现。
Immer.js
就是其中一种实现(类似的还有
Immutable.js
)。
三、 为什么要追求不可变数据?
- 数据拷贝处理中存在的问题
var testA = [{value:1}]
var testB = testA.map(item => item.value =2)
//问题:本意只是让testB的每个元素变为2、却无意改掉了testA每个元素的结果
//解决:当需要传递一个引用类型的变量进一个函数时,可以使用Object.assign或者...解构,断引用
var testB = testA.map(item =>({...item, item.value =2}))
//问题:Object.assign或者...只会断开一层引用,但如果对象嵌套超过一层
// 深层次的对象嵌套
var testA = [{
value: 1,
desc: { text: 'a' }
var testB = testA.map(item => ({ ...item, value: 2 }))
console.log(testA === testB) // false
console.log(testA.desc === testB.desc) // true
// testA.desc和testB.desc指向同一个引用
//解决:深拷贝,递归去遍历
//只考虑对象的场景
function deepClone(obj) {
const keys = Object.keys(obj)
return keys.reduce((memo, current) => {
const value = obj[current]
if (typeof value === 'object') {
return {
...memo,
[current]: deepClone(value),
return {
...memo,
[current]: value,
}, {})
// deepClone可以满足简单的需求,但在实际开发中,需要考虑其他因素:如原型链上的处理,value出现循环引用的场景、value是个Symbol
// 所以一般会使用大型的工具函数:lodash.cloneDeep
- 不可变数据在 React 中的重要性
为了加速了diff 算法中
reconcile
(调和)的过程,React 只需要检查
object
的索引有没有变即可确定数据有没有变
举个
在 React 的生命周期中每次调用 ComponentShouldUpdate() 会将
state
现有的数据跟将要改变的数据进行比较(只会对
state
进行浅对比,也就是更新某个复杂类型数据时只要它的引用地址没变,那就不会重新渲染组件)。
const [todos, setTodos] = useState([{study:'open',,work:'down'}]);
const onClick = () => {
todos[0].study = 'down';
setTodos(todos);
// 不会触发渲染
// 正确的做法
const onClick = () => {
let list =[...todos]
list[0].study='down'
setTodos(list);
//引入immer
setState(produce((state) => (state.isShow = true)))
四、 常见的实现方法
4.1 深拷贝
深拷贝的成本比较高,需要考虑其他如原型链、
value
为
symbol
或者出现循环引用的处理且没有地址共享的数据,影响性能。
4.2 Immutable.js
Immutable.js 源自 Facebook ,一个非常棒的不可变数据结构的库。使用另一套数据结构的 API,将所有的原生数据类型转化成 Immutable.js 的内部对象,并且任何操作最终都会返回一个新的
Immutable
值
// 举个
const { fromJS } = require('immutable')
const data = {
val: 1,
desc: {
text: 'a',
const a = fromJS(data)
const b = a.set('val', 2)
console.log(a.get('val')) // 1
console.log(b.get('val')) // 2
const pathToText = ['desc', 'text']
const c = a.setIn([...pathToText], 'c')
console.log(a.getIn([...pathToText])) // 'a'
console.log(c.getIn([...pathToText])) // 'c'
console.log(b.get('val') === a.get('val')) // false
console.log(b.get('desc') === a.get('desc')) // true
const d = b.toJS()
const e = a.toJS()
console.log(e.desc === d.desc) // false
console.log(e.val === d.val) // false
这个例子也可以看出:深层次的对象在没有修改的情况仍然能保证严格相等。这也是它另外一个特点:深层嵌套对象的结构共享
相比与 Immer.js,Immutable.js 的不足:
- 自己维护一套数据结构、JavaScript 的数据类型和 Immutable.js 需要相互转换,有入侵性
- 他的操作结果需要通过 toJS 方法才能得到原生对象,这样导致在开发中需要时刻关注操作的是原生对象还是 Immutable.js 返回的结果
- 库的体积大约在 63KB、而 Immer.js 仅有12KB
- API 丰富、学习成本较高
五、 Immer.js
基本概念
-
currentState
:被操作对象的最初状态 -
draftState
: 根据currentState
生成的草稿、是currentState
的代理、对draftState
所有的修改都被记录并用于生成nextState
。在此过程中,currentState
不受影响 -
nextState
: 根据draftState
生成的最终状态 -
produce
: 用于生成nextState
或者producer
的函数 -
Producer
: 通过produce
生成,用于生产nextState
,每次执行相同的操作 -
recipe
:用于操作draftState
的函数
API简介
produce
- 第一种用法:
produce(currentState, recipe: (draftState) => void | draftState, ?PatchListener): nextState
import produce from 'immer';
const baseState= [
title:'study javascript',
status:true
title:'study immer'.
status:false
const nextState = produce(baseState, draftState=>{
draftState.push({title:'study react'})
draftState[1].status = true
// 新增的只会体现在在nextState上,baseState没被修改
expect(baseState.length).toBe(2)
expect(nextState.length).toBe(3)
expect(baseState[1].status).toBe(false)
expect(nextState[1].status).toBe(true)
// 没有改变的数据共享
expect(nextState[0]).toBe(baseState[0])
// 改变的数据不再共享
expect(nextState[1]).not.toBe(baseState[1])
在上面的例子,对
draftState
的修改最终都会体现在
nextState
,但并不会修改
baseState
,需要注意的是
nextState
和
baseState
共享未修改的部分。需要注意的是通过
produce
生成的
nextState
是被冻结的(使用
Object.freeze
实现,仅冻结
nextState
和
currentState
相比更改的部分),直接修改
nextstate
会报错
- 第二种用法:柯里化
produce(recipe: (draftState) => void | draftState, ?PatchListener)(currentState): nextState
利用高阶函数特点,提前生成一个生产者
producer
recipe
没有返回值,
nextState
是根据
recipe
函数中的
draftState
生成的;有返回值是根据返回值生成的。
const currentState = {
x: [5],
let producer = produce((draft) => {
draft.x = 2
let nextState = producer(currentState);
怎么工作的
produce(obj, draft => {
draft.count++
通过以上的例子可以看出,
obj
是我们传入的简单对象,所以 Immer.js 的神奇一定在
draft
对象上。
核心实现是利用ES6的
proxy
实现
JavaScript
的不可变结构。几乎以最小成本实现了不可变数据结构,简单易用、体量小巧。其基本思想在于所有的更改都应用在临时的
draftState
。一旦完成所有的变更,将草稿状态的变更生成
nextState
。这就通过简单的修改数据同时保留不可变数据的优点。

源码解析
补充知识:Proxy 对象
用于创建一个对象的代理,实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)
Proxy 对象接受两个参数,第一个参数是需要操作的对象,第二个参数是设置对应拦截的属性,这里的属性同样也支持
get
,
set
等等,也就是劫持了对应元素的读和写,能够在其中进行一些操作,最终返回一个 Proxy 对象实例。
const handle = {
get(target, key) {
// 这里的 target 就是 Proxy 的第一个参数对象
console.log('proxy get key', key)
return '返回1'
set(target, key, value) {
console.log('proxy set key', value)
const target = {a:{b:1}}
const p = new Proxy(target,handle)
p.a = 2 // 所有设置操作都被转发到了 set 方法内部
p.a.b= 1 // 触发的是get而非set
注意⚠️⚠️⚠️:如果一个对象的层级比较深,而且内部会有引用类型的属性值时。如果给当前对象生成代理并修改内层属性值时,如果修改的是最外层属性的值时,是会触发
set
方法,但是如果修改最外层某个属性值为对象的属性的值时,并不会触发
set
方法
这也就是为什么在 Immer.js 的实现里 需要递归给某个对象内部所有的属性(属性值为对象类型的属性)做代理的原因
Produce的实现
核心源码
produce: IProduce = (base: any, recipe?: any, patchListener?: any) => {
// base判断 是否能生成draft
if (typeof base === "function" && typeof recipe !== "function") {
const defaultBase = recipe
recipe = base
const self = this
return function curriedProduce(
this: any,
base = defaultBase,
...args: any[]
return self.produce(base, (draft: Drafted) => recipe.call(this, draft, ...args)) // prettier-ignore
// recipe、patchListener异常处理
if (typeof recipe !== "function") die(6)
if (patchListener !== undefined && typeof patchListener !== "function")
die(7)
let result
// Only plain objects, arrays, and "immerable classes" are drafted.
if (isDraftable(base)) {
// 生成ImmerScope对象,和当前的produce绑定 主要是做复杂嵌套的追踪
const scope = enterScope(this)
// 创建 proxy(draft),并执行scope.drafts.push(proxy)将 proxy 保存到 scope 里
const proxy = createProxy(this, base, undefined)
let hasError = true
try {
// 执行用户的修改逻辑 也就是draftState
result = recipe(proxy)
hasError = false
} finally {
// finally instead of catch + rethrow better preserves original stack
if (hasError) revokeScope(scope)
else leaveScope(scope)
if (typeof Promise !== "undefined" && result instanceof Promise) {
return result.then(
result => {
usePatchesInScope(scope, patchListener)
return processResult(result, scope)
error => {
revokeScope(scope)
throw error
usePatchesInScope(scope, patchListener)
return processResult(result, scope)
} else if (!base || typeof base !== "object") {
result = recipe(base)
if (result === NOTHING) return undefined
if (result === undefined) result = base
if (this.autoFreeze_) freeze(result, true)
return result
} else die(21, base)
Produce简单的流程图

总结 produce
produce 接受三个参数:
base
初始值、
recipe
用户执行修改逻辑、
patchlistener
用户接受
patch
数据做自定义操作
去除一些特殊的判断兼容处理代码,可以看出其主流程主要根据
base
创建
draft
对象、执行用户传入的
recipe
拦截读写操作,走到自定义的
getter
和
setter
最后再解析组装结果返回给用户。
step1、调用
createProxy
创建
draftState
export function createProxy<T extends Objectish>(
immer: Immer,
value: T,
parent?: ImmerState
): Drafted<T, ImmerState> {
const draft: Drafted = isMap(value)
? getPlugin("MapSet").proxyMap_(value, parent)
: isSet(value)
? getPlugin("MapSet").proxySet_(value, parent)
: immer.useProxies_
? createProxyProxy(value, parent)
: getPlugin("ES5").createES5Proxy_(value, parent)

这里兼容了不支持
proxy
的ES5处理,其核心根据
base
构建一个
state
对象,如果
base
为数组,是则基于
arrayTraps
创建
state
的Proxy,否则基于
objectTraps
创建
state
Proxy
step2、拦截读写操作
export const objectTraps: ProxyHandler<ProxyState> = {
get(state, prop) {
if (prop === DRAFT_STATE) return state
const source = latest(state)
if (!has(source, prop)) {
return readPropFromProto(state, source, prop)
const value = source[prop]
if (state.finalized_ || !isDraftable(value)) {
return value
if (value === peek(state.base_, prop)) {
prepareCopy(state)
return (state.copy_![prop as any] = createProxy(
state.scope_.immer_,
value,
state
return value
state: ProxyObjectState,
prop: string /* strictly not, but helps TS */,
value
const desc = getDescriptorFromProto(latest(state), prop)
if (desc?.set) {
desc.set.call(state.draft_, value)
return true
if (!state.modified_) {
const current = peek(latest(state), prop)
const currentState: ProxyObjectState = current?.[DRAFT_STATE]
if (currentState && currentState.base_ === value) {
state.copy_![prop] = value
state.assigned_[prop] = false
return true
if (is(value, current) && (value !== undefined || has(state.base_, prop)))
return true
prepareCopy(state)
markChanged(state)
state.copy_![prop] === value &&
// special case: NaN
typeof value !== "number" &&
// special case: handle new props with value 'undefined'
(value !== undefined || prop in state.copy_)
return true
state.copy_![prop] = value
state.assigned_[prop] = true
return true

-
getter
主要用来懒初始化代理对象,当代理对象的属性被访问的时候才会生成其代理对象 -
举个例子:当访问
draft.a
时,通过自定义getter
生成draft.a
的代理对象darftA
所用访问draft.a.x
相当于darftA.x
,同时如果draft.b
没有访问,也不会浪费资源生成draftB
-
setter
:当对draft
对象发生修改,会对base
进行浅拷贝保存到copy
上,同时将modified
属性设置为true
,更新在copy
对象上
step3、解析结果
export function processResult(result: any, scope: ImmerScope) {
scope.unfinalizedDrafts_ = scope.drafts_.length
const baseDraft = scope.drafts_![0]
const isReplaced = result !== undefined && result !== baseDraft
if (!scope.immer_.useProxies_)
getPlugin("ES5").willFinalizeES5_(scope, result, isReplaced)
if (isReplaced) {
//虽然 Immer 的 Example 里都是建议用户在 recipe 里直接修改 draft,但用户也可以选择在 recipe 最后返回一个 result,不过得注意“修改 draft”和“返回新值”这个两个操作只能任选其一,同时做了的话processResult函数就会抛出错误
if (baseDraft[DRAFT_STATE].modified_) {
revokeScope(scope)
die(4)
if (isDraftable(result)) {
//核心处理
result = finalize(scope, result)
if (!scope.parent_) maybeFreeze(scope, result)
if (scope.patches_) {
getPlugin("Patches").generateReplacementPatches_(
baseDraft[DRAFT_STATE],
result,
scope.patches_,
scope.inversePatches_!
} else {
// Finalize the base draft.
result = finalize(scope, baseDraft, [])
revokeScope(scope)
if (scope.patches_) {
scope.patchListener_!(scope.patches_, scope.inversePatches_!)
return result !== NOTHING ? result : undefined
function finalize(rootScope: ImmerScope, value: any, path?: PatchPath) {
// Don't recurse in tho recursive data structures
if (isFrozen(value)) return value
const state: ImmerState = value[DRAFT_STATE]
// A plain object, might need freezing, might contain drafts
if (!state) {
each(
value,
(key, childValue) =>
finalizeProperty(rootScope, state, value, key, childValue, path),
true // See #590, don't recurse into non-enumarable of non drafted objects
return value
// Never finalize drafts owned by another scope.
if (state.scope_ !== rootScope) return value
// Unmodified draft, return the (frozen) original
if (!state.modified_) {
maybeFreeze(rootScope, state.base_, true)
return state.base_
// Not finalized yet, let's do that now
if (!state.finalized_) {
state.finalized_ = true
state.scope_.unfinalizedDrafts_--
const result =
// For ES5, create a good copy from the draft first, with added keys and without deleted keys.
state.type_ === ProxyType.ES5Object || state.type_ === ProxyType.ES5Array
? (state.copy_ = shallowCopy(state.draft_))
: state.copy_
// Finalize all children of the copy
// For sets we clone before iterating, otherwise we can get in endless loop due to modifying during iteration, see #628
// Although the original test case doesn't seem valid anyway, so if this in the way we can turn the next line
// back to each(result, ....)
each(
state.type_ === ProxyType.Set ? new Set(result) : result,
(key, childValue) =>
finalizeProperty(rootScope, state, result, key, childValue, path)
// everything inside is frozen, we can freeze here
maybeFreeze(rootScope, result, false)
// first time finalizing, let's create those patches
-
解析结果:其核心在于
result = finalize(scope, baseDraft, [])
。当produce
执行完成,所有的用户修改也完成。 -
如果
state.modified_=false
未被标记修改:说明没有更改该对象,直接返回原始base
-
如果
state.finalized_=false
未被标记结束:递归base
和copy
的子属性执行finalizeProperty
,如果相同则返回,否则递归整个过程。最后返回的对象是由base
的一些没有修改的属性和copy
修改的属性拼接而成,最终使用freeze
冻结copy
属性,将finalized
设为true
。
六、Immer.js 在 React 项目中的优势
setState
中使用
Immer.js
onClick = () => {
this.setState(prevState => ({
user: {
...personState.user,
age: personState.user.age + 1
// 使用immer
onClickImmer = () => {
this.setState(produce(draft=>{
draft.user.age +=1
以 hook 的方式使用 Immer.js
Immer.js
还提供了一个 React Hook 库
use-Immer
,可以在React的项目中以
hook
的形式使用
Immer
useImmer
和
useState
非常像。它接收一个初始状态,返回一个数组。数组第一个值为当前状态,第二个值为状态更新函数。状态更新函数和
produce
中的
recipe
一样。举个 :
import React from "react"
import { useImmer } from "use-immer"
const [person, updatePerson] = useImmer({name:'tom',age:25})