immer.js 应用和原理

代码仓库

bookmark

Immer核心功能

  • 基于 ES6 Proxy 实现的「不可变数据」结构,和原生JS对象无缝兼容
  • 默认开启「冻结」功能,修改后的对象,无法直接修改其上的属性,只能通过 immer 的 API 来修改
  • 支持「Patch」,可以记录针对对象的所有操作,方便实现「时间机器」
  • 提供 useImmerusermmerReducer 等API,配合react hook使用方便
  • redux/toolkit 默认集成 Immer.js

参考文章:

link_preview

bookmark

bookmark

React / Redux 中使用 immer 的好处

  • 由于要通过setState来更新状态,对于复杂对象,不再需要解构语法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // before
    setState(prev => ({
    ...prev,
    name: 'xxxx'
    }))
    // after
    setState(produce(draft => {
    prev.name = 'xxxx'
    }))
  • 避免直接更新复杂对象的属性,因为对象已经被冻结了

实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
const INTERNAL = Symbol('internal')

/**
* 浅复制
* @param {*} targetState
* @returns
*/
function createDraftstate(targetState) {
if (Array.isArray(targetState) !== true && typeof targetState === 'object') {
return Object.assign({}, targetState)
} else if (Array.isArray(targetState)) {
return [...targetState]
} else {
// 还有很多类型, 慢慢写
return targetState
}
}


/**
* 入口方法
* @param {*} targetState 需要被拷贝/代理的对象
* @param {*} producer 开发者传入的处理函数
* @returns
*/
function produce(targetState, producer) {
let proxyState = toProxy(targetState)
producer(proxyState);
const internal = proxyState[INTERNAL];
// 如果producer中,修改了属性值,那么changed就会变成false,则返回浅拷贝的值;否则,直接返回原来的值即可
// 通过浅拷贝,实现了 immutable tree 的特性
// 并且这里并不是从proxyState上读取属性,因为会触发getter,而是从每个proxy闭包上下文的internal对象上读取值
return internal.changed ? internal.draftstate : internal.targetState
}

/**
* 记录了用户都为哪些属性赋值
* @param {*} targetState
* @param {*} backTracking
* @returns
*/
function toProxy(targetState, backTracking = () => { }) {
// 每个属性都会有一个对应的internal
let internal = {
targetState,
keyToProxy: {}, // 记录哪些key被读取,以及对应的值
changed: false,
draftstate: createDraftstate(targetState),
}
console.log(">>> targetState is", targetState)
// 只有访问属性时(设置属性会先自动触发get),才会有这些Proxy对象的生成,从而是缓存
return new Proxy(targetState, {
get(_, key) {
console.log('>>> getter key is', key)
if (key === INTERNAL) {
return internal
}
const val = targetState[key];
if (key in internal.keyToProxy) {
return internal.keyToProxy[key]
} else {
console.log('>>> val is', val)
internal.keyToProxy[key] = toProxy(val, () => {
// 3、被触发,这里的key是person
console.log(">>> trigger proxy", key)
// 这里的internal是obj1(person的上一级)所有,也会被连动修改
internal.changed = true;
// 拿到obj1的person代理对象
const proxyChild = internal.keyToProxy[key];
// 更新当前internal的值(从obj1.person所属的internal读取)
internal.draftstate[key] = proxyChild[INTERNAL].draftstate;
// 继续调用上一层
backTracking()
})
}
return internal.keyToProxy[key]
},
set(_, key, value) {
// 1、对obj1.person对象的name属性做改动,这里的internal就是obj1.person所有
console.log(">>> set", key, value, targetState)
internal.changed = true;
internal.draftstate[key] = value
// 2、触发name的上一级,也就是obj1.person
backTracking()
return true
}
})
}

module.exports = {
produce
}

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const obj1 = {
person: {
name: 'dongyuanxin'
}
}

const obj2 = produce(obj1, (draft) => {
draft.person.name = 'hello'
})

// output 输出
>>> targetState is { person: { name: 'dongyuanxin' } }
>>> getter key is person
>>> val is { name: 'dongyuanxin' }
>>> targetState is { name: 'dongyuanxin' }
>>> set name hello { name: 'dongyuanxin' }
>>> trigger proxy person
>>> getter key is Symbol(internal)
>>> getter key is Symbol(internal)

参考文档:

bookmark