说明
immer 的主旨是通过更简单的方式使用 immutable 不可变数据,它是基于 copy-on-write 机制的(如果资源重复未被修改,则无需创建新的资源,资源会在副本与原始位置之间共享;修改则必须创建一个副本;通过这个机制 复制操作会被推迟到第一次写入时进行,显著的减少资源消耗,同时资源修改操作增加少量开销)
- 通常来讲 react 的 state 以及 redux 存放在 store 中的 state 都是不允许直接修改的, 需要我们手动去创建一个新的 state
- 如果我们存放的数据中有 Array 或者 Object 类型的就会比较麻烦,需要一层层解构
- 之前用的 immutable.js 是通过创造了新的数据类型 ImmutableMap、ImmutableSet 等,来提供方便的 api 进行新数据的创建
- Immer, 与 immutable.js 不同,他没有创建新数据类型,而是在我们改动 state 的操作时,提供一个 proxy 草稿对象,针对草稿对象,我们可以直接修改它的值,而不需要层层解构,Immer 会帮我们把修改后的 草稿对象转变为一个新的普通对象
- 也是就说使用 Immer 除了在改变 state 是需要介入,其他开发过程中我们与它的接触不多,操作的还是我们熟悉的普通JS内置对象
Immer 的 Api
immer 中有如下三种数据概念
- currentState: 原始数据
- draftState: 草稿数据,是一个以原始数据为基础的 Proxy , 数据的操作会在 draftState 上进行
- nextState: 一旦在 draftState 上的数据操作完成,immer 就会生成 nextState, 原始数据 currentState 将不会被改变
1. produce(baseState, draftState => {})
用于包装数据处理,并生产新数据
const baseState = {
obj: { b: 'xiaobai ' },
arr: [{ name: 'xiaobai', age: 20 }, { name: '小黑' }],
};
const nextState = produce(baseState, (draftState) => {
// 直接操作 draftState 即可,不需要返回值
draftState.arr.push({ name: 'ak' });
console.log(draftState); // Proxy { <target>: {…}, <handler>: {…} }
});
console.log(
nextState, // 返回的是普通对象
baseState,
nextState === baseState, // false 因为有值改变
nextState.obj === baseState.obj, // true 因为 baseState.obj 没有改变
nextState.arr === baseState.arr, // false
);
2. produce(draftState => {})(baseState)
用于创建一个 producer(预绑定生产者)
预绑定生产者 producer 可以接收一个 baseState 参数来生成 nextState
const baseState = {
obj: { b: 'xiaobai ' },
arr: [{ name: 'xiaobai', age: 20 }, { name: '小黑' }],
};
const baseState2 = {
obj: { b: 'bbb' },
arr: [{ name: 'xiaobai', age: 20 }, { name: '小白' }],
};
const producer = produce((draftState: typeof baseState) => {
draftState.arr.push({ name: 'ak' });
});
const nextState = producer(baseState);
const nextState2 = producer(baseState2);
console.log(nextState, nextState2);
3. produce((draftState, ...args) => {}, initialState)(baseState, ...args)
producer 可以接收更多的参数, 以及初始数据
const baseState = {
obj: { b: 'xiaobai ' },
arr: [{ name: 'xiaobai', age: 20 }, { name: '小黑' }],
};
const baseState2 = {
obj: { b: 'bbb' },
arr: [{ name: 'xiaobai', age: 20 }, { name: '小白' }],
};
const producer = produce(
(draftState: typeof baseState, data?: { name: string }) => {
if (data) draftState.arr.push(data);
},
{ obj: { b: '' }, arr: [] }, // initialState
);
const nextState = producer(); // 不传返回初始值
const nextState2 = producer(baseState2, { name: 'second' });
console.log(nextState, nextState2);
4. immer.current immer.original
用于在 produce 中提取 draft 的当前值或者原始传入值 (注意:操作昂贵,一般只用于测试)
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)
Immer 与 React、Redux 的结合使用
1. React setState 中怎么使用 immer
- setState 可以传一个函数,setState((prevState) => nextState);
- producer 的刚好是一个接收 原始数据,输出新数据的函数
- 我们可以将 producer 传入 setState
// 不使用 immer
const { remote } = this.state;
const { cache, data } = remote;
// 需要一层层解构
this.setState({ remote: { ...remote, cache: [...cache, data] } });
// 使用 immer
this.setState(
// import {Draft} from 'immer'
produce((state: Draft<State>) => {
// 直接操作数据即可
state.remote.cache.push(state.remote.data);
}),
);
// 或者
this.setState(
produce<State>((state) => {
// 直接操作数据即可
state.remote.cache.push(state.remote.data);
}),
);
2. redux 的 reducer 中如何使用 immer
// 不使用 immer
function counterReducer(state = { counter: 0 }, action: { type: string }) {
switch (action.type) {
case 'counter/incremented':
// 需要 return 一个新的 state
return { counter: state.counter + 1 };
case 'counter/decremented':
return { counter: state.counter - 1 };
default:
return state;
}
}
// 使用 immer 后
const INITIAL_DATA = { counter: 0 };
const counterReducer = produce(
(state: typeof INITIAL_DATA, action: { type: string }) => {
switch (action.type) {
case 'counter/incremented':
state.counter += 1;
break;
case 'counter/decremented':
state.counter -= 1;
break;
default:
}
},
INITIAL_DATA,
);
一般来讲使用 immer 的 reducer 不需要 return 任何内容,如果一定要 return 可以查看 Returning new data from producers
需要特殊说明的是,如果需要 return 一个 undefined 该怎么操作?,直接 return undefined 是不行的,者会被认为是 return draft,需要按照如下操作
import produce, {nothing} from "immer"
const state = {
hello: "world"
}
produce(state, draft => nothing) // 相当于将 state 置为 undefined
其他
- Redux 的版本到 4.x.x 以后以及趋于稳定,变更越来越少,但是基础版的 Api 使用起来比较繁琐,所以 Redux 团队又推出了一个封装版 Redux Toolkit , 这个封装版,是 redux 与 immer 的结合体。
如果想要使用 封装版请查看 Redux Toolkit