学习 react 之后,接触到最多的概念就是纯函数,进而开始接触他后面的一系列函数式编程思想,但核心就是编写纯函数。
推荐一本关于函数式编程的文章函数式编程指北
有关纯函数是什么,以及为什么要用纯函数大家可以从上面的文章里寻找答案。 当然自己如果有一定项目积累的话可能理解比较快,我目前的水平看了之后因为和项目联系不到一块,所以还没有完全体会到其中的妙处,但本就起思想本身已经让我对编程有了不同的理解和认识。
回到正题:
what is immer
immer(baseState,producer) 是immer的基本用法,他接受一个原始的state,并且通过producer来对原state进行操作,避免修改原state。
producer是一个函数,他也接受一个参数,draft,可以理解为baseState的代理,
import produce from "immer"
const nextState = produce(currentState, draft => {
// empty function
})
console.log(nextState === currentState) // true
通过在draft进行操作来产出(produce)nextState,如果没有做任何操作,就像上面的代码,二者是相等的。 可以把代码语义化进行理解。
因为immer采用的是structural sharing,文字上理解是结构分享,就是baseState把结构分享了给draft。
需要注意的是,producer不返回任何值,关键的是我们通过producer来做一些操作。
import produce from "immer";
const arr = [{ name: "item1", done: false }, { name: "item2" }];
const nextState = produce(arr, (draft) => {
draft.push({ name: "item3" });
draft[0].done = true;
});
console.log(nextState === arr);//false
console.log(arr[1] === nextState[1]);//true
通过实践我们可以知道,如果我们不采取任何操作,那么nextState === baseState,如果我们在此基础上进行修改,那么将会创建一个新的状态树,但是没有被修改的部分依然被保留,所以我们的第二项还是相等的。
结合reducer
在redux我们必须保证我们的state不被污染,所以reducer函数必须是一个纯函数,所以我的代码一般是这样的
switch(action.type)
case 'ADD':
return {...state, newOne}
当层级比较深的时候,我不得不写出这样的代码
{
... state,
todos:[
{
...item,
done:true
},
...newTodos
],
}
不仅难以维护,还很恶心🤢
看一下immer下的效果:
const byId = (state, action) =>
produce(state, draft => {
switch (action.type) {
case RECEIVE_PRODUCTS:
action.products.forEach(product => {
draft[product.id] = product
})
break
}
})
我们的关注点只在我们需要做什么,那就在producer里做就可以,
另外我们甚至都省去了写default的部分,因为即使我们什么也不做返回的也是之前的state
awesome!
自动freeze(开发模式)
immer的另一个功能就是我们通过producer创建出来的数据结构会被冻结,实现真正的immutable,但是freeze整个state是一个成本很高的操作,所以immer采用办法是:** 只freeze那些变化的部分** 来提高执行效率
Currying
柯里化也是函数式编程中一个很重要的一个概念,在上面的例子中,produce函数传入两个函数,其实produce函数是一个柯里化的函数,这意味着我们可以把参数拆开来传入,我们可以只传入一个producer函数,那么这将会返回一个新的函数,我们再向这个函数中传入state的话,就在这个state上执行我们之前的producer的操作,如果你熟悉柯里化,那么应该不难理解,
talk is cheap , show me the code
import produce from "immer";
//这里我们传入了一个producer,这个producer做的事情就是把所有项设置为已完成,这移植性就体现出来了,我们可以在任何需要这个场景的代码中调用
const setItemDone = produce((draft) => {
draft.map((item: any) => (item.done = "true"));
});
const arr = [
{ name: "item1", done: "false" },
{ name: "item2", done: "false" }
];
const handleAllDone = ()=>{
setItemDone(arr)//这里我们只需要传入我们需要修改的state就可以啦
}
return (
<div>
<button onClick = {handleAllDone}>all done</button>
</div>
)
当然返回的新的函数,也就是上面的setItemDone也是一个柯里化函数
immer的工作模式
总结起来两句话: 1) copy-on-write (复制后修改) 2) proxy (draft作为baseState的代理)
我们再深层了解一下,当我们试图在proxy上进行修改的时候,首先会创建相关节点的一个浅拷贝,所以未来的任何读写操作都会在这个拷贝上进行,不会污染到源state,另外,未被修改的父节点也会被标记为modified
最后会遍历整个proxy树,被修改的地方返回他的拷贝值,未被修改的地方返回原值,这就是他的一个核心理念 ** 结构共享**。
总结:
- immer允许我们利用原生的javascript数据结构和api来生成immutable的状态数据
- 对对象类型支持很好
- 结构共享,返回没有变化的部分
- freeze变化的部分
- 精简代码
参考: