最近一直在做项目功能优化的工作,在排查功能方面的问题时发现项目中许多当地都存在重复烘托的问题,审查代码后发现其中存在不少乱用或许说误用 useMemo、useCallback、useContext 的场景,导致了页面的冗余烘托。所以决定总结这一篇协助小组内成员正确理解这三个 hook 的运用。

一、正确理解 useMemo、useCallback、memo 的运用场景

在咱们平常的开发中许多状况下咱们都在乱用 useMemo、useCallback这两个 hook, 实际上许多状况下咱们不需求甚至说是不应该运用,由于这两个 hook 在初次 render 时需求做一些额定工作来供给缓存

一起既然要供给缓存那必定需求额定的内存来进行缓存,归纳来看这两个 hook 其实并不利于页面的初次烘托甚至会拖慢初次烘托,这也是咱们常说的“不要在一开始就优化你的组件,出现问题的时分再优化也不迟”的根本原因。

那什么时分应该运用呢,无非以下两种状况:

  1. 缓存 useEffect 的引证类型依靠;
  2. 缓存子组件 props 中的引证类型。

1. 缓存 useEffect 的引证类型依靠

import { useEffect } from 'react'
export default () => {
  const msg = {
    info: 'hello world',
  }
  useEffect(() => {
    console.log('msg:', msg.info)
  }, [msg])
}

此时 msg 是一个目标该目标作为了 useEffect 的依靠,这儿本意是 msg 改动的时分打印 msg 的信息。可是实际上每次组件在render 的时分 msg 都会被从头创立,msg 的引证在每次 render 时都是不相同的,所以这儿 useEffect 在每次render 的时分都会从头履行,和咱们预期的不相同,此时 useMemo 就能够派上用场了:

import { useEffect, useMemo } from "react";
const App = () => {
  const msg = useMemo(() => {
    return {
      info: "hello world",
    };
  }, []);
  useEffect(() => {
    console.log("msg:", msg.info);
  }, [msg]);
};
export default App;

同理对于函数作为依靠的状况,咱们能够运用 useCallback:

import { useEffect, useCallback } from "react";
const App = (props) => {
  const print = useCallback(() => {
    console.log("msg", props.msg);
  }, [props.msg]);
  useEffect(() => {
    print();
  }, [print]);
};
export default App;

2. 缓存子组件 props 中的引证类型。

做这一步的意图是为了防止组件非必要的从头烘托形成的功能消耗,所以首先要清晰组件在什么状况下会从头烘托。

  1. 组件的 props 或 state 改动会导致组件从头烘托
  2. 父组件的从头烘托会导致其子组件的从头烘托

这一步优化的意图是:在父组件中跟子组件没有关系的状况改动导致的从头烘托能够不烘托子组件,形成不必要的糟蹋。

大部分时分咱们是清晰知道这个意图的,可是许多时分却并没有到达意图,存在一定的误区:

误区一:

import { useCallback, useState } from "react";
const Child = (props) => {};
const App = () => {
  const handleChange = useCallback(() => {}, []);
  const [count, setCount] = useState(0);
  return (
    <>
      <div
        onPress={() => {
          setCount(count + 1);
        }}
      >
        increase
      </div>
      <Child handleChange={handleChange} />
    </>
  );
};
export default App;

项目中有许多当地存在这样的代码,实际上完全不起作用,由于只要父组件从头烘托,Child 组件也会跟着从头烘托,这儿的 useCallback 完全是白给的。

误区二:

import { useCallback, useState, memo } from "react";
const Child = memo((props) => {});
const App = () => {
  const handleChange = () => {};
  const [count, setCount] = useState(0);
  return (
    <>
      <div
        onPress={() => {
          setCount(count + 1);
        }}
      >
        increase
      </div>
      <Child handleChange={handleChange} />
    </>
  );
};
export default App;

对于复杂的组件项目中会运用 memo 进行包裹,意图是为了对组件承受的 props 特点进行浅比较来判别组件要不要进行从头烘托。这当然是正确的做法,可是问题出在 props 特点里边有引证类型的状况,例如数组、函数,假如像上面这个例子中这样书写,handleChange 在 App 组件每次从头烘托的时分都会从头创立生成,引证当然也是不相同的,那么势必会形成 Child 组件从头烘托。所以这种写法也是白给的。

正确姿势:

import { useCallback, useState, memo, useMemo } from "react";
const Child = memo((props) => {});
const App = () => {
  const [count, setCount] = useState(0);
  const handleChange = useCallback(() => {}, []);
  const list = useMemo(() => {
    return [];
  }, []);
  return (
    <>
      <div
        onPress={() => {
          setCount(count + 1);
        }}
      >
        increase
      </div>
      <Child handleChange={handleChange} list={list} />
    </>
  );
};
export default App;

其实总结起来也很简单,memo 是为了防止组件在 props 没有改动时从头烘托,可是假如组件中存在类似于上面例子中的引证类型,还是那个原因每次烘托都会被从头创立,引证会改动,所以咱们需求缓存这些值保证引证不变,防止不必要的重复烘托。

二、useContext 运用留意事项

在项目中咱们现已重度依靠于 useContext 这个 api,一起结合 useReducer 代替 redux 来做状况办理,这也引入了一些问题。咱们把官方Demo整合下,先来看看怎么结合运用 useContext 和 useReducer。

import React, { createContext, useContext, useReducer } from "react";
const ContainerContext = createContext({ count: 0 });
const initialState = { count: 0 };
function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}
function Counter() {
  const { state, dispatch } = useContext(ContainerContext);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
    </>
  );
}
function Tip() {
  return <span>计数器</span>;
}
function Container() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <ContainerContext.Provider value={{ state, dispatch }}>
      <Counter />
      <Tip />
    </ContainerContext.Provider>
  );
}
export default Container;

运用起来非常方便,乍一看好像都挺美好的,可是其实有不少陷阱或许误区在里边。

useContext 的机制是运用这个 hook 的组件在 context 发生改动时都会从头烘托。这样会导致一些问题,我把我遇到过的和能想到的问题总结到下面,假如有弥补的能够再评论。

1. Provider 单独封装

在上面的 demo 中咱们应该看到了在 Provider 中有两个组件,Counter 组件在 state 发生改动的时分需求从头烘托这个没什么问题,那 Tip 组件呢,在 Tip 组件里边明显没有用到 Context 实际上是没有必要进行从头烘托的。可是现在这种写法每次state改动都会导致 Provider 中所有的子组件都跟着烘托。有没有什么办法处理呢,实际上也很简单,咱们把状况办理单独封装到一个 Provider 组件里边,然后把子组件通过 props.children 的方法传进去

...
function Provider(props) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <ContainerContext.Provider value={{ state, dispatch }}>
      {props.children}
    </ContainerContext.Provider>
  );
}
const App = () => {
  return (
    <Provider>
      <Counter />
      <Tip />
    </Provider>
  );
};
...

这个时分 APP 组件就成为了无状况组件,state 改动的时分 props.children 不会改动,不会被从头烘托,这个时分再看 Tip 组件,状况更新的时分就不会跟着从头烘托了。

那这样是不是就万事大吉呢,对不住没有,还有坑,接着看第二点。

2. 缓存 Provider value

在官方文档里边也提到了这个坑,简单说便是,假如 Provider 组件还有父组件,当 Provider 的父组件进行重烘托时,Provider 的value 特点每次烘托都会从头创立,原理和上面 useMemo useCallback 中提到的相同,所以最好的办法是对 value 进行缓存:

...
function Provider(props) {
const [state, dispatch] = useReducer(reducer, initialState);
const value = useMemo(() => ({ state, dispatch }), [state]);
  return (
    <ContainerContext.Provider value={value}>
      {props.children}
    </ContainerContext.Provider>
  );
}
...

3. memo 优化直接被穿透,不再起作用

在开发中咱们会运用 memo 来对组件进行优化,如上文中提到的,可是许多时分咱们又会在运用 memo 的组件中运用 context,用 context 的当地在context发生改动的时分无论怎么都会发生从头烘托,所以许多时分会导致 memo 优化实效,具体能够看这儿的评论,react 官方解释说规划如此,一起也给出了相应的主张,咱们项目中首要处理方案是把 context 往上提,然后通过特点传递,便是说咱们的组件一开始是这样写的:

React.memo(()=> {
 const {count} = useContext(ContainerContext);
 return <span>{count}</span>
})

这个时分context更新了,memo 属所以白给,咱们把 context 往上提一层,其实就能够处理这个问题:

const Child = useMemo((props)=>{
    ....
})
function Parent() {
  const {count} = useContext(ContainerContext);
  return <Child count={count} />;
}

这样保证了 Child 组件的外部状况的改动只会来自于 props,这样当然 memo 能够完美工作了。

4. 对 context 进行拆分整合

context 的运用场景应该是为一组享有公共状况的组件供给便当来获取状况的改动。
可是跟着业务代码越来越复杂,在不经意间咱们就会把一些不相关的数据放在同一个context 里边。这样就导致了context 中任何数据的改动都会导致运用这个 context 的组件从头 render。这明显不是咱们想看到的。这种状况下咱们应该要对contex 进行更细粒度的拆分,把真正相关的数据整合在一起,然后再供给给组件,至少这样不相关组件的状况改动不会相互影响,也就不会导致剩余的重复烘托。

总结

不过话又说话来,写个代码要留意这留意那,心智负担的确也蛮重的,只能说“要说爱你不容易”,这些根底 api 的运用给咱们带来便当的一起有时分也会让咱们感觉到难以控制,理解其中的内部烘托逻辑和api的规划初衷能协助咱们写出更好的代码。