useReducer基础用法

3/10/2023 React

React Hook useReducer基础用法

# useReducer概念解释

第四个Hook(钩子函数)是useReducer,他的作用是"钩住"某些自定义数据对应的dispatch所引发的数据更改事件。useReducer可以替代useState。实现更为复杂逻辑的数据修改。

在React16.8版本以前,通常需要使用第三方Redux来管理React的公共数据,但自从 React Hook 概念出来以后,可以使用 useContext + useReducer 轻松实现 Redux 相似功能。

# useReducer是来解决什么问题的?

useReducer是useState的升级版(实际上应该是原始版),可以实现复杂逻辑修改,而不是像useState那样只是直接赋值修改的。

补充说明:

  1. 在React源码中,实际上useState就是由useReducer实现的,所以useReducer准确来说是useState的原始版。
  2. 无论哪一个Hook函数,本质上都是通过事件驱动来实现视图层更新的。

# useReducer函数源码

首先看一下React源码中的ReactHooks.js (opens new window)

export function useReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useReducer(reducer, initialArg, init);
}
1
2
3
4
5
6
7
8

还是那句话看不懂也没关系,这并不是Hook源码分析,而是如何使用Hook,之所以贴出源码只是重点要看useReducer函数的第3个参数,一般的话是传2个参数就好了,但是我上次在github看到有个人的项目传了3个参数,可以这么理解,第3个参数其实只是第1个和第2个参数的某种转化。可以完全忽略这个问题,每次传值只需要传2个参数就好了。

# useReducer基本用法

useReducer(reducer, initiaValue)函数通常传入2个参数,第1个参数为我们定义的一个"由dispatch引发的数据修改处理函数",第2个参数为自定义数据的默认值,useReducer函数会返回自定义变量的引用和该自定义变量对应的"dispatch"。

当看到了dispatch,想到了原生JS中的EvenetEmitter,事实上React Hook做了底层的事件驱动处理,拿到的dispatch以及"事件处理函数"reducer,都是被React Hook 封装过后的,并不是真正的抛出和事件处理函数。

但是我接下来说这个useReducer依旧还会使用到"事件抛出、事件处理函数"等文字。

其实这个useReducer的用法很简单的,了解了事件驱动,使用过EventEmitter、或者使用过Redux,就很好理解了。

# 代码形式

import React, { useReducer } from 'react'; // 引入useReducer

// 定义好"事件处理函数" reducer
function reducer(state, action) {
    switch (action) {
        case 'xx':
            return xxxx;
        case 'xx':
            return xxxx;
        default:
            return xxxx;
    }
}

function Component() {
    // 声明一个变量xxx,以及对应修改xxx的dispatch
    // 将事件处理函数reducer的默认值initialVal作为参数传递给useReducer
    const [xxx, dispatch] = useReducer(reducer, initialValue);
    
    // 若想获取xxx的值,直接使用xxx即可
    
    // 若想修改xxx的值,通过dispatch来修改
    dispatch('xx');
    
}

// 注意:上述代码中的action只是最基础的字符串形式,事实上action可以是多属性的object,这样可以自定义更多属性和更多参数值
// 例如 action 可以是 {type: xx, param: xxx}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 拆解说明

  1. 具体已经在示例代码中做了多项注释,不再重复。(不想浪费时间)

'reducer'补充说明

  1. reducer英文单词本身意思是"减速器、还原剂",但是我一直把reducer称呼为"事件处理函数",但事实上reducer确实扮演了一个事件处理函数。
  2. 千万别把useReducer中的reducer和原生中的Array.prototype.reduce()搞混,他们两个只是刚好都是用了reduce单词而已,两者本身没有任何内在关联。(上海的南京路与南京的上海路)

'xxx'补充说明

假设定义的变量名为xxx,那么只能通过dispatch来修改xxx,不要尝试通过 xxx = newValue 这种形式直接修改变量的值,React 不允许这样做。

'dispatch'补充说明

再次强调一下,dispatch并不是真正的Event.dispatch,但是完全可以把它当成Event.dispatch来理解,只不过useReducer中的的dispatch(xxx)函数抛出内容并不是event,而是一个包含修改信息的对象,该对象不仅可以是字符串,还可以是复杂对象。

'initalValue'补充说明

initalValue是自定义变量的默认值,该值可以是简单类型(number、string),也可以是复杂类型(object、array)。
个人建议供参考:即使该值是简单类型,也建议单独定义出来而不是直接将值写在useReducer函数中,因为单独定义可以更加清晰的读懂数据结构,尤其是initalValue为复杂类型的时候。

# useReducer使用示例1

举例:某React组建内部有一个变量count,有3个button,点击之后分别可以修改count的值。3个按钮具体的功能为:第1个button点击之后count+1,第2个button店址之后count-1,第3个button点击之后count x 2 (翻倍)。

如果使用useState来实现,那肯定是没问题的,每个button点击之后分别运算得到相对应的新值,然后将该值直接通过setCount赋值给count。

使用useReducer来实现相同功能,代码如下:

import React, { useReducer } from 'react';

function reducer(state, action) {
    switch (action) {
        case 'add':
            return state + 1;
        case 'sub':
            return state - 1;
        case 'mul':
            return state * 2;
        default:
            console.log('默认');
            return state;
    }
}

function CountComponent() {
    
    const [count, dispatch] = useReducer(reducer, 0)
    
    return <div>
        {count}
        <button onClick={() => { dispatch('add') }}>add</button>
        <button onClick={() => { dispatch('sub') }}>sub</button>
        <button onClick={() => { dispatch('mul') }}>mul</button>
    </div>
}

export default CountComponent;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 代码分析

  1. 3个按钮点击之后,不再具体去修改count的值,而是采用dispatch('xxx')的形式"抛出修改count的事件",事件处理函数reducer"捕获到修改count的事件后",根据该事件携带的命令类型来进一步判断,并且真正执行对count的修改。 之前我说过,我只是以事件驱动的语言来描述整个过程,因为能够更加理解。
  2. 3个按钮只是负责通知reducer"我要做的事情",具体怎么做完全由reducer来执行,这样实现了修改数据具体执行逻辑与按钮点击处理函数的抽离。

如果不使用useReducer,而是使用useState,那么对count的每一种修改逻辑代码、都必须分散写在每个按钮的点击事件处理函数中。

# useReducer使用示例2

若只是修改count的功能,那么useReducer的优势还没有全部体现出来,来继续写。

举例:在上面的例子中对count执行的修改,数值变动都是固定的,即+1、-1、*2。那么来实现一下点击按钮之后,能够自主控制增加多少、减多少、或者乘以多少。

很简单,将dispatch('xxx')中的xxx由字符串改为obj,obj可以携带更多属性作为参数传给reducer。比如之前对"加"的命令dispatch('add'),修改为dispatch({type: 'add', param: 2)。reducer可以通过action.type来区分是哪种命令、通过action.param来获取对应的参数。

为了简化代码,点击按钮后,随机产生一个数字,并将该数字作为param的值,传递给reducer。

修改后的代码:

import React, { useReducer } from 'react';

function reducer(state, action) {
    // 根据action.type来判断该执行哪种操作
    switch (action.type) {
        // count 最终加多少,取决于 action.param的值
        case 'add':
            return state + action.param;
        case 'sub':
            return state - action.param;
        case 'mul':
            return state * action.param;
        default:
            console.log('默认');
            return state;
    }
}

function getRandom() {
    return Math.floor(Math.random()* 10);
}

function CountComponent() {
    const [count, dispatch] = useReducer(reducer);
    
    return <div>
        {count}
        <button onClick={() => { dispatch({type: 'add',param: getRandom()})}}>add</button>
        <button onClick={() => { dispatch({type: 'sub',param: getRandom()})}}>add</button>
        <button onClick={() => { dispatch({type: 'mul',param: getRandom()})}}>add</button>
    </div>
}

export default CountComponent;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

同样的道理,把示例中的count由简单类型改为复杂类型,来存储更多的变量。但是,建议不要把useReducer对应的变量设计的多余复杂。

使用了useReducer,可以使用比较复杂的逻辑和参数对内部变量进行修改。

但是有一点,示例1和示例2中所有的变量都是在同一个组件内定义和修改的,现实项目中肯定牵扯到不同模块组建之间共享并且修改某个变量,具体的,下次再写吧。🤪🤪🤪