React18 (四) Context,Effect,Reducer和Memo

代码地址:https://github.com/showkawa/react18-ZeroToOne/tree/main/react03

1.Context

在React中组件间的数据通信是通过props进行的,父组件给子组件设置props,子组件给后代组件设置props,props在组件间自上向下逐层传递数据,层级变多了维护起来很麻烦。有些数据需要夸多个组件中共同使用,props传递的方式已经不适用了。
Context为我们提供了一种在不同组件间共享数据的方式,在外层组件中统一设置,设置后内层所有的组件都可以访问到Context中所存储的数据。Context类似于JS中的全局作用域,可以将一些公共数据设置到一个同一个Context中,使得所有的组件都可以访问到这些数据。创建Context:

const GlobalContext = React.createContext(defaultValue);

React.createContext(defaultValue)用来创建一个Context对象,它需要一个初始值作为参数,这个初始值可以是一个原始值,也可以是一个JS对象。调用以后,方法将会返回一个Context对象,当我们想在其他组件中访问Context中的数据时,必须要通过这个对象。由于Context对象需要在不同的组件中被使用,所以通常我们会将Context对象设置到一个单独的模块中并设置为默认导出,例如下面的样例代码:

import React from 'react';

const CartContext = React.createContext({
    items: [],
    totalAmount: 0,
    totalPrice: 0,
    // addItem: () => { },
    // removeItem: () => { },
    // clearItem: () => { },
    cartsDispatch: () => { },
});

export default CartContext;

React为我们提供了一个钩子函数`useContext()`,我们只需要将Context对象作为参数传递给钩子函数,它就会直接给我们返回Context对象中存储的数据。

import classes from './Cart.module.css';
import iconImg from '../../../asset/bag.png';
import CartContext from '../../../store/CartContext';
import { useCallback, useContext, useEffect, useState } from 'react';
import CartDetails from './CartDetails/CartDetails';
import Checkout from './Checkout/Checkout';

const Cart = () => {

    const ctx = useContext(CartContext);

    const [showDetails, setShowDetails] = useState(false);

    const [showCheckout, setShowCheckout] = useState(false);

    const toggleDetailsHandler = (e) => {
        if (ctx.totalAmount === 0) { return }
        setShowDetails(preValue => !preValue);
    }

    const showCheckoutHandler = useCallback(() => {
        if (ctx.totalAmount === 0) { return }
        setShowCheckout(true);
    },[ctx]);


    const hideCheckoutHandler = (e) => {
        e.stopPropagation();
        setShowCheckout(false);
    }

    useEffect(() => {
        if (ctx.totalAmount === 0) {
            setShowDetails(false);
            setShowCheckout(false);
        }
    }, [ctx])



    return (
        <div className={classes.Cart} onClick={toggleDetailsHandler}>
            {showCheckout && <Checkout onHide={hideCheckoutHandler} />}
            {showDetails && <CartDetails />}

            <div className={classes.Icon}>
                <img src={iconImg} alt="bag" />
                {
                    ctx.totalAmount === 0 ? null : <span className={classes.TotalAmount}>{ctx.totalAmount}</span>
                }
            </div>
            {
                ctx.totalPrice === 0 ? <p className={classes.NoMeal}>请选择商品</p> : <p className={classes.Price}>{ctx.totalPrice}</p>
            }
            <button className={`${classes.Button} ${ctx.totalPrice === 0 ? classes.Disable : ''}`} onClick={showCheckoutHandler}>去结算</button>
        </div>)
        ;
}

export default Cart;

有了消费方的组件就需要提供方,React提供了Provider,用来在组件中指定Context值,如样例代码我们在App.js中用<CartContext.Provider> 根元素,提供一种全局的共享对象

function App() {
  ......return (
    <CartContext.Provider value={{ ...carts, cartsDispatch }}>
    ......
    </CartContext.Provider>
  );
}

export default App;

Provider译为生产者,和Consumer消费者对应。Provider会设置在外层组件中,通过value属性来指定Context的值。Context的搜索流程和JS中函数作用域类似,当我们获取Context时,React会在它的外层查找最近的Provider,然后返回它的Context值。如果没有找到Provider,则会返回Context模块中设置的默认值。

2.Effect

React组件有部分逻辑如果直接写在函数体中,会影响到组件的渲染,这部分会产生“副作用”的代码,是一定不能直接写在函数体中。例如,如果直接将修改state的逻辑编写到了组件之中,就会导致组件不断的循环渲染,直至调用次数过多内存溢出。

React.StrictMode

编写React组件时,我们要极力的避免组件中出现那些会产生“副作用”的代码。同时,如果你的React使用了严格模式,也就是在React中使用了React.StrictMode标签,那么React会非常“智能”的去检查你的组件中是否写有副作用的代码。React的严格模式,在处于开发模式下,会主动的重复调用一些函数,以使副作用显现。所以在处于开发模式且开启了React严格模式时,这些函数会被调用两次:

类组件的的 constructor, render, 和 shouldComponentUpdate 方法
类组件的静态方法 getDerivedStateFromProps
函数组件的函数体
参数为函数的setState
参数为函数的useState, useMemo, or useReducer

重复的调用会使副作用更容易凸显出来,你可以尝试着在函数组件的函数体中调用一个console.log你会发现它会执行两次,如果你的浏览器中安装了React Developer Tools,第二次调用会显示为灰色。

使用Effect

React专门为我们提供了钩子函数useEffect(),专门用来处理那些不能直接写在组件内部的代码。哪些代码不能直接写在组件内部呢?像是:获取数据、记录日志、检查登录、设置定时器等。简单来说,就是那些和组件渲染无关,但却有可能对组件产生副作用的代码。

useEffect语法:

useEffect(()=>{
    /* 编写那些会产生副作用的代码 */
});

useEffect()中的回调函数会在组件每次渲染完毕之后执行,这也是它和写在函数体中代码的最大的不同,函数体中的代码会在组件渲染前执行,而useEffect()中的代码是在组件渲染后才执行,这就避免了代码的执行影响到组件渲染。

通过使用这个Hook,我设置了React组件在渲染后所要执行的操作。React会将我们传递的函数保存(我们称这个函数为effect),并且在DOM更新后执行调用它。React会确保effect每次运行时,DOM都已经更新完毕。

清除Effect

组件的每次重新渲染effect都会执行,有一些情况里,两次effect执行会互相影响。比如,在effect中设置了一个定时器,总不能每次effect执行都设置一个新的定时器,所以我们需要在一个effect执行前,清除掉前一个effect所带来的影响。要实现这个功能,可以在effect中将一个函数作为返回值返回,像是这样:

useEffect(()=>{
    /* 编写那些会产生副作用的代码 */
    
    return () => {
        /* 这个函数会在下一次effect执行前调用 */
    };
});

effect返回的函数,会在下一次effect执行前调用,我们可以在这个函数中清除掉前一次effect执行所带来的影响。

限制Effect

组件每次渲染effect都会执行,这似乎并不总那么必要。因此在useEffect()中我们可以限制effect的执行时机,在useEffect()中可以将一个数组作为第二个参数传递,像是这样:

useEffect(()=>{
    /* 编写那些会产生副作用的代码 */

    return () => {
        /* 这个函数会在下一次effect执行前调用 */
    };
}, [a, b]);

上例中,数组中有两个变量a和b,设置以后effect只有在变量a或b发生变化时才会执行。这样即可限制effect的执行次数,也可以直接传递一个空数组,如果是空数组,那么effect只会执行一次。

例如下面的搜索组件,input框输入的keyword变化时,就会触发useEffect,只是过滤方法会在500毫秒后在执行,只要在500毫秒内有不间断的输入变化,就可以看到防抖效果,而不是每次输入变化都会触发过滤方法 

import React, { useEffect, useState } from "react";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import classes from './FilterMeals.module.css';

const FilterMeals = (props) => {

    const [keyword, setKeyword] = useState('');

    const inputChangeHandler = e => {
        setKeyword(e.target.value.trim());

    }

    // 搜索防抖
    useEffect(() => {
        const timer = setTimeout(() => {
            console.log('--- fliter ---')
            props.onFilter(keyword);
        }, 500);

        return () => {
            console.log('--- return filter ---')
            clearTimeout(timer);
        }
    }, [keyword])


    return (<div className={classes.FilterMeals}>
        <div className={classes.InputOuter}>
            <input
                value={keyword}
                className={classes.SearchInput}
                type="text"
                placeholder={"请输入关键字"}
                onChange={inputChangeHandler}
            />
            <FontAwesomeIcon className={classes.SearchIcon} icon={faSearch} />
        </div>
    </div>);
}

export default FilterMeals;

3.Reducer

在React的函数组件中,我们可以通过useState()来创建state。但是这种方式也存在着一些不足,因为所有的修改state的方式都必须通过setState()来进行,如果遇到一些复杂度比较高的state时,这种方式似乎就变得不是那么的优雅。为了解决复杂State带来的不便,React为我们提供了一个新的使用State的方式。Reducer横空出世,reduce单词中文意味减少,它的作用就是将那些和同一个state相关的所有函数都整合到一起,方便在组件中进行调用。

和State相同Reducer也是一个钩子函数,语法如下:

const [state, dispatch] = useReducer(reducer, initialArg, init);

它的返回值和useState()类似,第一个参数是state用来读取state的值,第二个参数同样是一个函数,不同于setState()这个函数我们可以称它是一个“派发器”,通过它可以向reducer()发送不同的指令,控制reducer()做不同的操作。

它的参数有三个,第三个我们暂且忽略,只看前两个。reducer()是一个函数,也是我们所谓的“整合器”。它的返回值会成为新的state值。当我们调用dispatch()时,dispatch()会将消息发送给reducer(),reducer()可以根据不同的消息对state进行不同的处理。initialArg就是state的初始值,和useState()参数一样。样例代码如下:

import './App.css';
import React, { useReducer, useState } from 'react'
import CartContext from './store/CartContext';
import { Route } from 'react-router-dom';
import Home from './component/Home/Home';
import Shopping from './component/Shopping/Shopping';
import Member from './component/Member/Member';
import Bonus from './component/Bonus/Bonus';
import BottomNavigation from '@mui/material/BottomNavigation';
import BottomNavigationAction from '@mui/material/BottomNavigationAction';
import RestoreIcon from '@mui/icons-material/Restore';
import FavoriteIcon from '@mui/icons-material/Favorite';
import ArchiveIcon from '@mui/icons-material/Archive';
import Paper from '@mui/material/Paper';
import { Link, Redirect, Switch, useHistory, useLocation } from 'react-router-dom/cjs/react-router-dom.min';
import Wechat from './component/Bonus/Wechat/Wechat';

const cartsReducer = (params, action) => {
  const newCarts = { ...params };
  switch (action.type) {

    case 'ADD':
      if (newCarts.items.indexOf(action.meal) === -1) {
        newCarts.items.push(action.meal);
        action.meal.attributes.amount = 1;
      } else {
        action.meal.attributes.amount += 1;
      }
      newCarts.totalAmount += 1;
      newCarts.totalPrice += action.meal.attributes.price;
      return newCarts;

    case 'REMOVE':
      if (--action.meal.attributes.amount <= 0) {
        newCarts.items.splice(newCarts.items.indexOf(action.meal), 1);
      }
      newCarts.totalAmount -= 1;
      newCarts.totalPrice -= action.meal.attributes.price;
      return newCarts;

    case 'CLEAR':
      newCarts.items.forEach(item => delete item.attributes.amount);
      newCarts.items = [];
      newCarts.totalAmount = 0;
      newCarts.totalPrice = 0;
      return newCarts;

    default:
      return params;
  }
}

function App() {

  const [carts, cartsDispatch] = useReducer(cartsReducer, {
    items: [],
    totalAmount: 0,
    totalPrice: 0
  });

  const [value, setValue] = useState('/');

  const _history = useHistory();

  const gotoHander = (event, newValue) => {
    setValue(newValue);
    _history.push(newValue);
  }

  const homeProps = {
    name: 'hanbao'
  }

  const memberProps = {
    name: 'member',
    params: {}

  }

  return (
    <CartContext.Provider value={{ ...carts, cartsDispatch }}>

      {/* <Route exact path = '/' component ={Home}/> */}
      <Route exact path='/' children={<Home {...homeProps} />} />
      <Route path='/bonus' component={Bonus} />
      <Switch>
        <Route exact path='/bonus/wechat' component={Wechat} />
      </Switch>
      <Route exact path='/shopping' component={Shopping} />
      <Route exact path='/home' component={Home} />
      <Route path='/member' render={() => <Member {...memberProps} />} />


      <Paper sx={{ position: 'fixed', bottom: 0, left: 0, right: 0 }} elevation={1}>
        <BottomNavigation
          showLabels
          value={value}
          onChange={gotoHander}
        >
          <BottomNavigationAction label="Home" value="/" icon={<RestoreIcon />} />
          <BottomNavigationAction label="Bonus Plan" value="/bonus" icon={<FavoriteIcon />} />
          <BottomNavigationAction label="Store" value="/shopping" icon={<ArchiveIcon />} />
          <BottomNavigationAction label="Member" value={`/member/${homeProps.name}`} icon={<ArchiveIcon />} />
        </BottomNavigation>
      </Paper>
    </CartContext.Provider>

  );
}

export default App;

在其他组件中,需要操作state时,只需先获取CartContext然后通过ctx.cartDispath操作购物车:

const ctx = useContext(CartContext); // 加载context
ctx.cartDispatch({type:'ADD_ITEM', meal:props.meal}); // 添加操作
ctx.cartDispatch({type:'REMOVE_ITEM', meal:props.meal}); // 删除操作

4.Memo

React组件会在两种情况下发生重新渲染。第一种,当组件自身的state发生变化时。第二种,当组件的父组件重新渲染时。第一种情况下的重新渲染无可厚非,state都变了,组件自然应该重新进行渲染。但是第二种情况似乎并不是总那么的必要。

 

三个组件的引用关系为,A组件是App的子组件、B组件是A组件的子组件:App –> A –> Bar

当App组件发生重新渲染时,A和Bar组件都会发生重渲染。当A组件重新渲染时,Bar组件也会重新渲染。假如Bar组件中没有state,甚至连props都没有设置。换言之,Bar组件无论如何渲染,每次渲染的结果都是相同的,虽然重渲染并不会应用到真实DOM上,但很显然这种渲染是完全没有必要的。为了减少像Bar组件这样组件的渲染,React为我们提供了一个方法React.memo()。该方法是一个高阶函数,可以用来根据组件的props对组件进行缓存,当一个组件的父组件发生重新渲染,而子组件的props没有发生变化时,它会直接将缓存中的组件渲染结果返回而不是再次触发子组件的重新渲染,这样一来就大大的降低了子组件重新渲染的次数。

样例如下修改:

import React from 'react';
import classes from './Bar.module.css';

const Bar = (props) => {

    console.log('--- Bar Component ---', props);
    return (
        <div className={classes.Bar}>
            <div className={classes.TotalPrice}>{props.totalPrice}</div>
            <button className={classes.Button}>去支付</button>
        </div>
    );
}

export default React.memo(Bar);

 

修改后的代码中,并没有直接将Bar组件向外导出,而是在Bar组件外层套了一层函数React.memo(),这样一来,返回的Bar组件就增加了缓存功能,只有当Bar组件的props属性发生变化时,才会触发组件的重新渲染。memo只会根据props判断是否需要重新渲染,和state和context无关,state或context发生变化时,组件依然会正常的进行重新渲染。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章