学习immer的使用以及函数式编程思想

588 阅读4分钟

学习 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树,被修改的地方返回他的拷贝值,未被修改的地方返回原值,这就是他的一个核心理念 ** 结构共享**。

总结:

  1. immer允许我们利用原生的javascript数据结构和api来生成immutable的状态数据
  2. 对对象类型支持很好
  3. 结构共享,返回没有变化的部分
  4. freeze变化的部分
  5. 精简代码

参考:

Introducing Immer: Immutability the easy way