React中使用immer的实践探索|牛气冲天新年征文

2,849 阅读8分钟

作者:@wangly19,本文已授权掘金开发者社区公众号独家使用,包括但不限于编辑、标注原创等权益。

前言

对于MOBX,可能并不会陌生。而immer.js也是该开源项目作者的一大力作,用于immutable管理的实践。而今天,会对其做一个简单的小分享。

谈谈为什么会选择immer.js来作为immutable的解决方案,以及一些项目上的小实践。

什么是immutable ? 点我了解

immer的优缺点

在这里总结一些优缺点,其实对比是相当明显的,比时下immutable.js来说更加的接地气,实用。

优点

  • 上手快,学习成本较低
  • 原生语法实现,没有额外的方言依赖
  • 体积小,方便紧张的空间内实用
  • api精炼,理解较为容易

缺点

  • 浏览器需要支持proxy语法糖,否则会实用defineProperty代替
  • ES5后备实现的速度大约是代理实现速度的两倍,在某些情况下更糟。

用大白话来说就是,如果你需要在ie10上开发项目,那还是别用了。在这里也侧面突出了两个api的一个生产速度。在2021年的今天,ie10用户应该很少了吧。

工作模式

用过immutable.js大家都知道,它的实现方式是自己维护了一套自有的数据结构。虽然解决了问题,但也面临很多的问题。

开发者需要在兼顾原生类型的时候,还要抽时间学习对应的数据结构的使用。对于轻度用户来说,无疑是非常鸡肋的事情。

immer的话,没有额外的学习负担,更加的贴合应用场景,且改造代码也非常的快捷和优雅。基本的实现思路就如下图所表达的一样,我们所做的更改只是在当前数据代理上,一旦你的更改结束了,那么就会基于更改产生新的对象,这样就非常方便的在隔离沙箱中修改一个数据,且没有过多的副作用

immer工作流

如何使用

immer中,主要的操作是由produce函数来进行的,produce需要接收两个参数。

produce(currentState, producer: (draftState) => void): nextState
  • 当前数据: 元数据
  • 草稿函数: 代理函数

使用起来也非常简单。下面就简单的分享下immerReact中是如何工作的一些小实例。

为什么在React使用

在这里,我也谈谈为什么使用immer吧。 在之前,我们也知道React如何判断视图更新是依赖于浅比较,如下图事例。

当我们是基本数据类型的时候,看起来并没有什么问题,这个时候数据比较是不想等的。a

var a = 1
var b = a
b = 2

// a = 1, b = 2
console.log(a === b) // false

但是,如果是引用类型呢?

var a = { x: 1 }
var b = a

b.x = 2

// a = { x: 2 }, b = { x: 2 }
console.log(a === b) // true

这个时候,我们ab都指向了同一片内存单元,当我们在一处引用中修改了属性值,所有引用的数据都变了。另一个就是在浅比对的时候,对象的指向如果一样,那么React就不会刷新render,哪怕你的值已经修改了。因此,大多数时候,都是用一以下当时进行对象数组的更新的。

this.setState({
 ...state,
 count: 2
})

以上方式产生一个新的内存引用进行设置,来确保引用类型数据的新值当前值相对来说是没有瓜葛的。虽然解决了问题,但其实从解决的方式来说非常的暴力。所以,为了更加精准的管理数据,引入了immer对引用状态进行管理,减少不必要的状态变化和意外的render渲染。

在最后面留下了一个小问题,小伙伴们可以思考下。本文就不做赘述了。嘿嘿

为什么是浅比较而不是深比较?

Class组件

class组件中的state更新是通过触发setState来进行的,对于对象状态的更改形式非常的简单,参考官网的demo,对于状态来说非常的简单。在produce中直接对数据进行更改,是不是有vuex风味了,嘿嘿。

从代码开发的角度来讲,更加接地气。

一开始的时候,我只想改变一个属性,但是你告诉我改这个属性你需要考虑它的引用,需要返回一个新的对象等等。 但是,回到问题本身,我仅仅只想改了属性而已,我并不想考虑那么多的事情。


EthicalAds: A privacy-focused ad network for developers. Publishers & Advertisers wanted!
Ad by EthicalAds
egghead.io lesson 8: Using Immer with useState. Or: useImmer
Deep updates in the state of React components can be greatly simplified as well by using immer. Take for example the following onClick handlers (Try in codesandbox):

/**
 * Classic React.setState with a deep merge
 */
onBirthDayClick1 = () => {
    this.setState(prevState => ({
        user: {
            ...prevState.user,
            age: prevState.user.age + 1
        }
    }))
}

/**
 * ...But, since setState accepts functions,
 * we can just create a curried producer and further simplify!
 */
onBirthDayClick2 = () => {
    this.setState(
        produce(draft => {
            draft.user.age += 1
        })
    )
}

上述例子中,其实很多同学会看不懂,为什么没有原始数据呢?那么怎么知道你要代理的数据来源呢?

对于这个问题就要思考于函数柯里化(currying)了,得益于函数的柯里化(currying)。其作用非常的朴素,就是将接收多个参数的函数变成为接收一个参数且返回值为剩余参数的函数。

简单小事例:foo(1, 2, 3) => f(1)(2)(3)

这样理解的话,由于默认的setState会传入一个state参数,因此柯里化之后,只需要传递一个草稿函数就可以完成状态的更改了。

函数组件

对于无状态组件,immer将其可能用到State抽离成为了hook,名字是use-immer

useImmer

useImmer用于state的对象处理,其实本质上是一个自定义hook,对useState进行了一层包装。对于状态的更改,只需要和class一样传递一个函数就可以了。

import { useImmer } from 'use-immer';

const [person, setPerson] = useImmer({
  name: "wangly",
});

// button click event ...
const handleClick = () => {
 setPerson(state => {
 	state.name = 'wangly19 yes!!!'
 })
}

实现原理

export function useImmer(initialValue: any) {
  const [val, updateValue] = useState(initialValue);
  return [
    val,
    useCallback(updater => {
      updateValue(produce(updater));
    }, [])
  ];
}

对于state的使用方式更加的简介和易懂,且不需要关注其他函数改变副作用的担忧,在开发的时候能够明细修改的数据,不必刻意去成产一个新对象,将这些事情更多交给统一化处理,出错也更加容易定位。

其次,对于useReducer来说,和useState一半,也有一个自定义的hook来管理这个内容。使用方式也相对来说更加好理解。 参考以下事例,通过dispatch调用不同的action操作时,进行不同的更改操作。

对于以前手动创建新对象来说,代码会更加清晰明了,减少很多操作。也不担心一些副作用引起问题。

import { useImmerReducer } from "use-immer";

const initialState = { count: 0 };

function reducer(draft, action) {
  switch (action.type) {
    case "reset":
      return initialState;
    case "increment":
      return void draft.count++;
    case "decrement":
      return void draft.count--;
  }
}

代码实例点击查看

其他

由于项目使用的是umi-cli,那么必不可少的会使用dva来管理项目的部分状态,如果需要体验immer,只需要在配置文件中加上声明就可以享受immer的数据流了。

import { defineConfig } from 'umi';
import routes from './routes'

export default defineConfig({
  hash: true,
  antd: {
  },
  dva: {
    immer: true
  },
  history: {
    type: 'browser'
  },
  locale: {
    // default zh-CN
    default: 'zh-CN',
    antd: true,
  },
  routes
})

性能

对于性能来说,从官方给出一些对比数据来看,immer在大多数的场景下看起来消费差距并不是很大。相对一观之即可。

# wangly19 @ wangly19s-MacBook-Pro in ~/Desktop/项目/immer on git:master o [11:42:31] 
$ yarn test:perf
yarn run v1.22.5
$ cd __performance_tests__ && babel-node add-data.js && babel-node todo.js && babel-node incremental.js

# add-data - loading large set of data

just mutate: 0ms
just mutate, freeze: 1ms
handcrafted reducer (no freeze): 0ms
handcrafted reducer (with freeze): 0ms
immutableJS: 65ms
immutableJS + toJS: 34ms
seamless-immutable: 40ms
seamless-immutable + asMutable: 48ms
immer (proxy) - without autofreeze * 10000: 27ms
immer (proxy) - with autofreeze * 10000: 29ms
immer (es5) - without autofreeze * 10000: 93ms
immer (es5) - with autofreeze * 10000: 65ms

# todo - performance

just mutate: 1ms
just mutate, freeze: 195ms
deepclone, then mutate: 175ms
deepclone, then mutate, then freeze: 368ms
handcrafted reducer (no freeze): 19ms
handcrafted reducer (with freeze): 19ms
naive handcrafted reducer (without freeze): 19ms
naive handcrafted reducer (with freeze): 42ms
immutableJS: 5ms
immutableJS + toJS: 164ms
seamless-immutable: 52ms
seamless-immutable + asMutable: 143ms
immer (proxy) - without autofreeze: 64ms
immer (proxy) - with autofreeze: 74ms
immer (proxy) - without autofreeze - with patch listener: 87ms
immer (proxy) - with autofreeze - with patch listener: 84ms
immer (es5) - without autofreeze: 257ms
immer (es5) - with autofreeze: 257ms
immer (es5) - without autofreeze - with patch listener: 2439ms
immer (es5) - with autofreeze - with patch listener: 2715ms

# incremental - lot of small incremental changes

just mutate: 0ms
handcrafted reducer: 72ms
immutableJS: 17ms
immer (proxy): 988ms
immer (es5): 3520ms
immer (proxy) - single produce: 9ms
immer (es5) - single produce: 3ms
✨  Done in 67.52s.

参考资料

后话

immer本身来说并没有性能屏障,哪怕在文档的后面贴出了性能测试也并没有达到很大的提升。只能说在某些场景会优于现今的解决方案。如果在项目中存在困扰,不妨可以试试能不能更好的解决问题。

对于引用类型来说,很容易发生一些小意外,对于我来说,引入immer更多的是解决不可变数据(immutable)带来的负担,在开发的时候不需要因为一些隐式方法而改变数据导致BUG,哪怕这类问题出现的几率很小,但如果出现一次的话,排查起来是非常困难的。

在最后,祝大家2021年,牛气冲天。

2021年一起学习,可以加入我的小窝一起早起打卡和刷题吧。看资料微信,简单答题后即可加入,嘿嘿。