🔥从零开始学React:不可变状态更新/useContext/useReducer(第二期)🔥

96 阅读7分钟

前言

上一期我们学习了useState & useEffect,这一期我们选择了稍微比它们难那么一丢丢的useContext和useReducer,因为难度是循序渐进的,这样学起来比较好理解,学完今天的内容后,你大概就能够搞懂React是如何工作的了,OK,话不多说,我们进入今天的主题!

useContext

Why 存在?

它为什么会存在呢?看接下来的例子你就懂了:

import { useState, useContext } from 'react';

export const ContextExample = () => {
    const [isToggle, setIsToggle] = useState(false);

    return (

        <>
            <h1>Parent Component</h1>
            <ChildToggle setIsToggle={setIsToggle} />
            <ChildDisplay isToggle={isToggle} />
        </>

    )

};

const ChildToggle = ({ setIsToggle }) => {
    return (
        <>
            <button onClick={() => setIsToggle((prev) => !prev)}>
                Toggle State
            </button>
        </>
    )
}


const ChildDisplay = ({ isToggle }) => {
    return (
        <>
            <p>Current State: {isToggle ? 'Show' : 'Hide'}</p>
        </>
    )
}


在这里我们有一个父组件ContextExample,里面包含两个子组件ChildToggleChildDisplay,这个组件的功能就是:按一下按钮,按钮对应的Current State就会改变为Show或者Hide.

在这里我们从父组件传递了两个数据状态:isTogglesetIsToggle,我们通过ChildToggle改变IsToggle的值,来使得ChildDisplay的显示值变化。

但是如果我们给ChildDisplay再加上一个子组件呢?我们让这个子组件也要获取isToggle的信息,并发生响应式变化呢?

const ChildDisplay = ({ isToggle }) => {
    return (
        <>
            <p>Current State: {isToggle ? 'Show' : 'Hide'}</p>
            <GrandChild />
        </>
    )
}

那么我们就得从ChildDisplay的父组件拿到数据状态,再将其传递给GrandChild,如果GrandChild再有一个玄孙也需要这个数据呢?我们又需要将它从祖先一直传到玄孙手上,跟个传家宝一样,麻烦得很。

<Parent>
  <Child1>
    <Child2>
      <Child3>
        {/* 底层组件需要顶层数据,必须一层层 props 传递 */}
        <Child4 data={data} /> 
      </Child3>
    </Child2>
  </Child1>
</Parent>

而且呢,这样的数据传递也会带来渲染的压力:当你传输一个数据状态给某个组件的时候,即使这个组件没有用到这个数据状态,只要这个传递的数据发生了变化,这个组件也一定会被重新渲染

也就是说,如果我从祖爷爷这一辈一直都没用这个数据状态,只有玄孙用了,当数据状态变化时,那他的几个祖宗加上爸爸爷爷全部都会被重新渲染

为了避免这种情况,为了让组件之间通信传递更加方便高效,我们就有了useContext.

useContext 详解

它是这么用的:

image.png

拿上面按钮例子来说,改写应该成这个样子:

import { useState, useContext, createContext } from 'react';

const GlobalContext = createContext(null);

export const ContextExample = () => {
    const [isToggle, setIsToggle] = useState(false);

    return (
        <>
            <h1>Parent Component</h1>
            <GlobalContext.Provider value={{ setIsToggle, isToggle }}>
                <ChildToggle />
                <ChildDisplay />
            </GlobalContext.Provider>
        </>
    )
};

const ChildToggle = () => {
    const { setIsToggle } = useContext(GlobalContext);
    return (
        <>
            <button onClick={() => setIsToggle((prev) => !prev)}>
                Toggle State
            </button>
        </>
    )
}


const ChildDisplay = () => {
    const { isToggle } = useContext(GlobalContext);

    return (
        <>
            <p>Current State: {isToggle ? 'Show' : 'Hide'}</p>

        </>
    )
}


我们首先利用了const GlobalContext = createContext(null);创建了一个context,

createContext是一个函数,所以要加括号进行调用,括号里面可以加内容,当有组件没有被Provider包裹起来就利用了GlobalContext的内容时就会显示我们传入的默认值,在这里我们传入了null.

其次,我们利用了<GlobalContext.Provider> </GlobalContext.Provider>将要用这个context的组件包裹了起来,Provider嘛!提供者!将GlobalContext提供给谁,提供给谁我就把谁给包裹起来!

之后就是传值,如果传一个值我们就这么用:<GlobalContext.Provider value={isToggle}>

如果传递多个值,就需要多加一个{}:<GlobalContext.Provider value={{ setIsToggle, isToggle }}>

Why?为什么一会是{}一会是{{ }} ???

JSX语法要求value必须只能传递一个值,把多个值变为一个值的办法有什么呢?

那只能是把多个值变成对象传递过去啦!

最后呢,我们从需要用到GlobalContext的子组件的里面,利用解构,拿到想要的值:

const { isToggle } = useContext(GlobalContext);

也就是useContext,用哪里的context? —————— 用GlobalContext

然后从里面拿出isToggle,将其赋值给我子组件的isToggle

OK,解析完毕,虽然useContext是一个比较好的传递数据状态的方法,但是我们以后的Zustand会更加有用。

useReducer

OK,基础的三件套我们学完了,现在该学一些进阶的了!

useReducer是React提供的一个用于状态管理的Hook,它借鉴了Redux的核心思想,适合用来处理复杂的状态逻辑。与useState类似,useReducer也用于管理组件内部的状态,你可以理解为它是useState的升级版,可以被用于状态结构复杂的数据(如嵌套对象、数组)

为什么要叫Reducer?它是什么意思?

是因为它类似于Redux的Reducer,而在Redux文档中对Reducer有这样一句解释:

之所以将这样的函数称之为reducer,是因为这种函数与被传入 Array.prototype.reduce(reducer, ?initialValue) 的回调函数属于相同的类型。

  var sum = [1, 2, 3, 4, 5].reduce(function(acc, val) {
  return acc + val;
  }, 0);
  // sum = 15

  /* 注意这当中的回调函数 (prev, curr) => prev + curr
   与我们redux当中的reducer模型 (previousState, action) => newState 看起来是不是非常相似
   而这与我们接下来的 (state,action) => newState 也很相似
   */

现在如果还不懂,看到下面再回来看看你就绝对明白了!

useReducer详解

假设我们有这么一段例子:

import { useReducer } from 'react';

const [count,setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={(count) => (count+1)>+</button>
      <button onClick={(count) => (count-1)>-</button>
      <button onClick={(count) => (count*2)>*</button>
      <button onClick={(count) => (count/2)>/</button>
    </div>
  );
}

一个按钮对应一种功能,我们要对这一个数据状态做出这么多操作,那就要在页面多写几个接口,写接口倒没事,但是里面的逻辑如果很复杂,就显得很乱了,如果我有个对象,对象里面嵌套对象,合计9981行,到时候一整个页面密密麻麻的button+一大段逻辑,密恐患者直接趋势了55555555, 所以为了方便我们的管理,我们就可以用上useReducer了,

它的语法是这样的:

image.png

它是这么用的:

image.png

import { useReducer } from 'react';

function ContextExample() {
    // 定义reducer
    function reducer(state, action) {
        switch (action.type) {
            case 'increment':
                return { ...state,count: state.count + 1, title: action.payload };
            case 'decrement':
                return { ...state,count: state.count - 1, title: action.payload };
            default:
                return state;
        }
    }
    // reducer和switch case一起用,做判断,让数据处理更清晰。
    const initialState = {
        count: 0,
        title: 'hello'
    }
    // 初始化
    const [state, dispatch] = useReducer(reducer, initialState);

    return (
        <div>
            <p>Count: {state.count}</p>
            <p>Title: {state.title}</p>

            <button onClick={() => dispatch({ type: 'increment', payload: 'Increment' })}>+</button>
            <button onClick={() => dispatch({ type: 'decrement', payload: 'Decrement' })}>-</button>
        </div>
    );
}
export default ContextExample;

这里reducer接收的两个值,第一个为state,也就是创建reducer时的数据状态,第二个为action,就是在dispatch函数中传递的一整个对象。

纯函数

纯函数是指满足以下两个条件的函数:

  1. 相同输入永远返回相同输出(确定性)
  2. 不产生副作用(无外部影响)

1. 不变的确定性(Referential Transparency)

// 纯函数示例
function add(a, b) {
  return a + b;
}
// 无论调用多少次,add(2,3)永远返回5

2. 无副作用(No Side Effects)

副作用包括但不限于:

  • 修改外部变量
  • 修改输入参数
  • 进行API调用
  • 操作DOM
  • 写入文件/数据库
  • 打印日志(严格来说)
  • 触发外部流程
// 非纯函数示例
let counter = 0;
function increment() {
  counter++; // 修改了外部状态
  return counter;
}

重点:不可变状态更新?!

如果你看到上面的reducer函数的返回值,你可能会奇怪为什么我要返回...state,这是因为reducer是严格的不可变状态更新,就是说它的改变不是在原对象上的改变,而是新创建了一个对象,将内容全部重写进去了。

就算我们要改变一部分对象属性的值,如果我们不传入之前没改变的值进去,新对象就会缺胳膊少腿,只留下了改变的值。

image.png

也就是说它们是两个完全不同的对象,尽管某些key-value是相同的,但是它们是完全不同的对象。

这个机制要从React的设计哲学说起:

React被设计成自动根据数据变化更新页面,那么它是怎么做到的呢?它是根据浅比较检测前后改变对象的地址来决定是否渲染页面及时更新的。 因为浅比较要比深比较快好几个量级,在性能上会很有优势。

那么我们之前用的setState呢?其实setState也是这样的,他也是根据改变的值创建了新的对象,就拿上面的例子来说,如果有代码是这样的:

const [obj, setObj] = useState({
        name: '张三',
        age: 18
    })
    <button onClick={() => setObj({ name: '李四' })}>改变姓名</button>

当我点击按钮后,age就消失了,因为我没有在setState中传入,新的对象中不存在age这个属性,这个和useReducer的行为是一模一样的!

总结

今天我们学习了useContextuseReducer,前者是为了解决组件通信使用的,利用context可以让其他组件使用,后者是useState的升级版,利用它的reducer函数我们可以更好地管理复杂的数据状态(简易的数据状态还是建议使用useState),最后我们学习了React的设计哲学:不可变状态更新,了解了这是页面渲染快速所如此设计的。