likes
comments
collection
share

React - 关于 immer 的一些使用以及原理

作者站长头像
站长
· 阅读数 10

前言

如果使用 react,无论是在 类组件中,或者是函数式组件中,应该都听说过 immer,在函数时组件最典型的就是 useImmer 这个第三方库,但是最基本的也是依赖于 immer

首先先用类组件距离,函数式也是一样的,后面会说到为什么一样。

为什么要用 immer,在 react 中,state 是一个对象,如果是简单的 state 对象,直接调用 setState 进行设置就没有什么问题,但是要是 state 有一个参数是一个复杂对象,比如说

this.state = {
      name: "freewheelLee",
      gender: "male",
      phone: "12345678",
      address: {
        country: 'China',
        city: {
          name: 'Shanghai',
          area: 'PuDong',
          postcode: 200000, // 邮编号码
	    },
    }
};

假设现在有一个操作能够直接修改地址内的邮编以及地区,考虑到 state 的只读属性,我们就需要这样写

 this.setState({
      address: {
        ...this.state.address,
        city: {
          ...this.state.address.city,
          area: 'JingAn'
          postcode: this.state.address.city.postcode + 10, 
        }
      }
  }); 

这里直接使用 this.state 是不行的,可能会存在某些 bug,具体原因参考

[[react - setState]]

 legacy.reactjs.org/docs/state-…

需要将其改为函数的写法才比较安全

 this.setState((prevState) => {
      return {
        address: {
          ...prevState.address,
          city: {
            ...prevState.address.city,
            area: 'JingAn',
            postcode: prevState.address.city.postcode + 10, 
          }
        }
      }
  }); 

上面就能够看出来,在我们需要修改一个非常复杂的state属性的时候,可能需要不断地使用扩展语法,这样会导致错误的几率上升。

而使用 immer 就是为了解决这种问题,我们可以先来对比看一下如果不用 immer 可以怎么解决这个问题

解决方案

我们可以在 setState 之前或者函数当中深拷贝一次之前的对象,然后在这个新对象的基础上修改属性,最后进行 setState

this.setState((prevState) => {
  const newState = deepClone(prevState);
  newState.address.city.area = 'JingAn';
  newState.address.city.postcode = newState.address.city.postcode + 10;
  return newState;
}); 

这样看起来确实方便了很多,但是这个方法存在性能问题,深拷贝不论在什么情况都会去拷贝整个对象,万一对象特别的大,就可能会导致性能问题。

那么就可以使用 immer 来解决这个问题,至于为什么 immer 不会有性能问题,后面在婉婉道来。

使用 immer

在使用了 immer 以后,我们上面的代码就可以变成

import {produce} from 'immer';

...

this.setState((prevState) => {
  return produce(prevState, draftState =>{
	draftState.address.city.area = 'JingAn';
	draftState.address.city.postcode = draftState.address.city.postcode + 10;
  });
});

或者

this.setState(produce(draftState => {
  draftState.address.city.area = "JingAn";
  draftState.address.city.postcode = draftState.address.city.postcode + 10;
}));

第一种写法很好理解,第二种写法属于一种 函数柯里化 的写法,具体什么是函数柯里化,用一句话表示: 柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

举一个简单的例子,比如你原本有一个 add 函数 入参 为 xy 返回 xy 的和

function add (x, y) {
	return x + y
}

这样在你使用的时候就需要传入两个参数,然后在柯里化以后变为

function add (x) {
	return function (y) {
		return x + y
	}
}

第一次调用只用传入一个参数,相对应的,返回值变成了一个函数,利用函数闭包的特性,我们可以调用返回的函数传入第二个参数,然后得到答案。

那么柯里化的好处在哪呢,在于我们可以先缓存部分参数,比方说上面的例子,在我们第一次传入 x = 1 以后,拿着返回的函数,你可以无数次的去调用,传入不同的 y 并且他们都能够得到正确的和。

也就是说 produce 这个函数在只传入一个函数的时候会返回一个函数,这个函数就可以用于传入state,这个函数的柯里化用法要记住,后面会用到

在我们使用 immer 的api的时候,immer 内部帮我们暂存着刚刚的对象,并且暴露一个草稿给我们,在我们对草稿进行修改回传以后, immer 会根据传入的草稿来修改原对象,然后返回一个新的对象。这也就是为什么 immer 不存在深拷贝的性能问题的原因

函数式组件的 immer

在函数式组件当中,我们会使用 useImmer 来借用 immer 的能力,那么 useImmer 是怎么对 immer 进行包装的,我们可以参考一下部分源码。

先来看一下 useImmer 的用法,以及使用 useStateuseImmer 的区别在什么地方。

import { useState } from 'react';
...
const [person, setPerson] = useState({
	name: 'Niki de Saint Phalle',
	artwork: {
		title: 'Blue Nana',
		city: 'Hamburg',
		image: 'https://i.imgur.com/Sd1AgUOm.jpg',
	}
});
...
function handleTitleChange(e) {
	setPerson({
		...person,
		artwork: {
			...person.artwork,
			title: e.target.value
		}
	});
}

可以看到,在使用 useState 来修改负责对象的时候,不可避免的会有和上面一样的问题,需要多次使用扩展语法,然后来看一下使用 useImmer 是怎么样的

import { useImmer } from 'react';
...
const [person, setPerson] = useImmer({
	name: 'Niki de Saint Phalle',
	artwork: {
		title: 'Blue Nana',
		city: 'Hamburg',
		image: 'https://i.imgur.com/Sd1AgUOm.jpg',
	}
});
...
function handleTitleChange(e) {
	setPerson(draft => {
		draft.artwork.title = e.target.value;
	});
}

使用 useImmer 可以是代码变得更加的简洁,也能够避免多次使用扩展语法出现错误,然后来看一下 useImmer 是怎么是实现的。

use-immer 这个项目 srcindex 当中,我们能找到 useImmer 的定义 :

import { produce, Draft, nothing, freeze } from "immer";
...
export function useImmer(initialValue: any) {
  const [val, updateValue] = useState(() =>
    freeze(
      typeof initialValue === "function" ? initialValue() : initialValue,
      true
    )
  );
  return [
    val,
    useCallback((updater) => {
      if (typeof updater === "function") updateValue(produce(updater));
      else updateValue(freeze(updater));
    }, []),
  ];
}

其中 produce 就是上面提到过的函数,能够接收两种参数,这里主要用的是接收一个函数的用法,这个用法会返回一个函数,返回的函数用于接收一个对象,,并且在第一个函数当中会去改变这个对象,在上面的代码当中,传入的 updater 就是第一个函数,updateValue 就是 useState 的修改回调,会接收到当前的 state,到这里就会发现,其实函数式的用法和类组件的柯里化函数用法是完全一样的。

freeze 函数我们可以看一下 immer 中对它的定义

/**
 * Freezes draftable objects. Returns the original object.
 * By default freezes shallowly, but if the second argument is `true` it will freeze recursively.
 *
 * @param obj
 * @param deep
 */
declare function freeze<T>(obj: T, deep?: boolean): T;

就会发现它只是将传入值冻结(变为不可更改)然后返回原始对象,并没有多余的操作对象的逻辑,所以直接无视也是可以的。

在理解了两个函数的作用后,就会发现其实 useImmer 就只是 useState 进行了一层包装,将修改这个回调用 produce 包装了一层,才使得在外面使用的时候能够直接修改对象下面的属性。

总结

最后,我们总结了 immer 的使用方法,以及使用的部分场景,当然这并不是唯一,在需要其他需要更新复杂对象的场景当中,使用 immer 可以使得语法更加的简洁,也减少大量使用扩展语法的繁琐以及避免深克隆带来的性能问题。