• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

React 组件支持 class、function 两种形式,但现在绝大多数情况下我们都是写 function 组件了。

官方文档也已经把 class 组件的语法划到了 legacy(遗产)的目录下:

所以说,除非你维护的代码里有历史代码用了 class 组件,否则就没必要看那些用法了。

而 function 组件主要是学习各种 hook 的使用。

这节我们就来过一遍常用 hook。

首先用 create-react-app 创建个项目:

npx create-react-app --template typescript hook-test

在 index.tsx 里把这三行代码注释掉:

React.StrictMode 会导致额外的渲染,下面那个上报性能数据的,cra 自带的,我们也用不到。

然后改下 App.tsx

import { useState } from "react";

function App() {
    const [num, setNum] = useState(1);

    return <div onClick={() => setNum(num + 1)}>{num}</div>;
}

export default App;

这里用到了第一个 hook:useState。

useState

什么是 state 呢?

像 111、'xxx'、{ a: 1 } 这种叫做数据,它们可以是各种数据类型,但都是固定不变的。

从一种数据变成另一种数据,这种就是状态(state)了。

也就是说,状态是变化的数据。

其实细想一下,组件的核心就是状态。

点击、拖拽等交互事件会改变状态,而状态改变会触发重新渲染。

组件内的状态是 useState 创建的,整个应用还可以加一个全局状态管理的库来管理 state。

所以说,state 虽然是基础,但它却是前端应用的核心。

回过头来跑一下上面的代码:

state 初始值是 1,点击改变状态,这个很简单。

如果初始状态需要经过复杂计算得到,可以传个函数来计算初始值:

const [num, setNum] = useState(() => {
    const num1 = 1 + 2;
    const num2 = 2 + 3;
    return num1 + num2;
});

这个函数只能写一些同步的计算逻辑,不支持异步。

useState 返回一个数组,包含 state 和 setXxx 的 api,一般我们都是用解构语法取。

这个 setXxx 的 api 也有两种参数:

可以直接传新的值,或者传一个函数,返回新的值,这个函数的参数是上一次的 state。

跑一下,功能正常:

除了在 click 事件处理函数里 setState,如果想在初次渲染的时候请求数据然后 setState 呢?

这时候就要用到 useEffect 了。

useEffect

effect 被翻译为副作用。

为啥叫副作用呢?

之前的函数组件是纯函数,传入 props,返回对应的结果,再次调用,传入 props,依然返回同样的结果。

但现在有了 effect 之后,每次执行函数,额外执行了一些逻辑,这些逻辑不就是副作用么?

我们写个 App2.tsx:

import { useEffect, useState } from "react";

async function queryData() {
    const data = (await new Promise())<number>((resolve) => {
        setTimeout(() => {
            resolve(666);
        }, 2000);
    });
    return data;
}

function App() {
    const [num, setNum] = useState(0);

    useEffect(() => {
        queryData().then((data) => {
            setNum(data);
        });
    }, []);

    return <div onClick={() => setNum((prevNum) => prevNum + 1)}>{num}</div>;
}

export default App;

注意,想用 async await 语法需要单独写一个函数,因为 useEffect 参数的那个函数不支持 async。

把 index.tsx 渲染的组件改为 App2

浏览器访问下,可以看到 2s 后,state 变为了 666:

请求数据、定时器等这些异步逻辑,我们都会放在 useEffect 里。

回过头来看下 useEffect:

第二个参数为什么传空数组呢?

这个数组叫做依赖数组,react 是根据它有没有变来决定是否执行 effect 函数的,如果没传则每次都执行。

我们加个打印:

现在这个组件会渲染两次,初始渲染和 2s 后 setNum 触发的渲染,也就是 function 会执行 2 次。

打开 devtools 看一下:

xxx 只执行了一次,因为 [] 每次都不变。

我也可以写任意的常量,因为它们都不变,所以不会触发 effect 的重新执行:

但如果其中有个变化的值,那就会触发重新执行了:

可以看到,现在每次渲染,effect 都会执行:

这个数组我们一般写依赖的 state,这样在 state 变了之后就会触发重新执行了。

不传 deps 数组的时候也是每次都会重新执行 effect 函数:

那 useEffect 里如果跑了一个定时器,依赖变了之后,再次执行 useEffect,又跑了一个,怎么清理上一个定时器呢?

这样写:

import { useEffect, useState } from "react";

function App() {
    const [num, setNum] = useState(0);

    useEffect(() => {
        console.log("effect");
        const timer = setInterval(() => {
            console.log(num);
        }, 1000);

        return () => {
            console.log("clean up");
            clearInterval(timer);
        };
    }, [num]);

    return <div onClick={() => setNum((prevNum) => prevNum + 1)}>{num}</div>;
}

export default App;

跑一下:

可以看到,当 deps 数组变了,重新执行 effect 之前,会先执行清理函数,打印了 clean up。

此外,组件销毁时也会调用 cleanup 函数来进行清理。

useLayoutEffect

和 useEffect 类似的还有一个 useLayoutEffect。

绝大多数情况下,你把 useEffect 换成 useLayoutEffect 也一样:

那为啥还要有这两个 hook 呢?

我们知道,js 执行和渲染是阻塞的:

useEffect 的 effect 函数会在操作 dom 之后异步执行:

异步执行就是用 setTimeout、Promise.then 等 api 包裹执行的逻辑。

这些逻辑会以单独的宏任务或者微任务的形式存在,然后进入 Event Loop 调度执行。

打开 Permormance 工具,可以看到 Event Loop 的详情:

可以看到,渲染的间隔是固定的,而 js 的任务在这些渲染的间隔中执行。

所以异步执行的 effect 逻辑就有两种可能:

灰色的部分是单独的任务。

有可能在下次渲染之前,就能执行完这个 effect。

也有可能下次渲染前,没时间执行这个 effect,所以就在渲染之后执行了。

这样就导致有的时候页面会出现闪动,因为第一次渲染的时候的 state 是之前的值,渲染完之后执行 effect 改了 state,再次渲染就是新的值了。

一般这样也没啥问题,但如果你遇到这种情况,不想闪动那一下,就用 useLayoutEffect。

它和 useEffect 的区别是它的 effect 执行是同步的,也就是在同一个任务里:

这样浏览器会等 effect 逻辑执行完再渲染。

好处自然就是不会闪动了。

但坏处也很明显,如果你的 effect 逻辑要执行很久呢?

不就阻塞渲染了?

超过 50ms 的任务就被称作长任务,会阻塞渲染,导致掉帧:

所以说,一般情况下,还是让 effect 逻辑异步执行的好。

也就是说,绝大多数情况下,用 useEffect,它能避免因为 effect 逻辑执行时间长导致页面卡顿(掉帧)。 但如果你遇到闪动的问题比较严重,那可以用 useLayoutEffect,但要注意 effect 逻辑不要执行时间太长。

同步、异步执行 effect 都各有各的问题和好处,所以 React 暴露了 useEffect 和 useLayoutEffect 这两个 hook 出来,让开发者自己决定。

useReducer

前面用的 setState 都是直接修改值,那如果在修改值之前需要执行一些固定的逻辑呢?

这时候就要用 useReducer 了:

添加一个 App3.tsx:

import { Reducer, useReducer } from "react";

interface Data {
    result: number;
}

interface Action {
    type: "add" | "minus";
    num: number;
}
function reducer(state: Data, action: Action) {
    switch (action.type) {
        case "add":
            return {
                result: state.result + action.num,
            };
        case "minus":
            return {
                result: state.result - action.num,
            };
    }
    return state;
}

function App() {
    const [res, dispatch] = useReducer<Reducer<Data, Action>>(reducer, {
        result: 0,
    });

    return (
        <div>
            <div onClick={() => dispatch({ type: "add", num: 2 })}>加</div>
            <div onClick={() => dispatch({ type: "minus", num: 1 })}>减</div>
            <div>{res.result}</div>
        </div>
    );
}

export default App;

useReducer 的类型参数传入 Reducer<数据的类型,action 的类型>

然后第一个参数是 reducer,第二个参数是初始数据。

点击加的时候,触发 add 的 action,点击减的时候,触发 minus 的 action。

在 index.tsx 里引入:

当然,你直接 setState 也可以:

import { useState } from "react";

function App() {
    const [res, setRes] = useState({ result: 0 });

    return (
        <div>
            <div onClick={() => setRes({ result: res.result + 2 })}>加</div>
            <div onClick={() => setRes({ result: res.result - 1 })}>减</div>
            <div>{res.result}</div>
        </div>
    );
}

export default App;

有同学可能会说,用 useState 比 useReducer 简洁多了。

确实,这个例子不复杂,没必要用 useReducer。

但如果要执行比较复杂的逻辑呢?

用 useState 需要在每个地方都写一遍这个逻辑,而用 useReducer 则是把它封装到 reducer 里,通过 action 触发就好了。

当修改 state 的逻辑比较复杂,用 useReducer。

回过头来继续看 useReducer:

const [res, dispatch] = useReducer<Reducer<Data, Action>, string>(
    reducer,
    "zero",
    (param) => {
        return {
            result: param === "zero" ? 0 : 1,
        };
    }
);

它还有另一种重载,通过函数来创建初始数据,这时候 useReducer 第二个参数就是传给这个函数的参数。

并且在类型参数里也需要传入它的类型。

useReducer + immer

此外,使用 reducer 有一个特别要注意的地方:

如果你直接修改原始的 state 返回,是触发不了重新渲染的:

必须返回一个新的对象才行。

但这也有个问题,如果对象结构很复杂,每次都创建一个新的对象会比较繁琐,而且性能也不好。

比如这样:

import { Reducer, useReducer } from "react";

interface Data {
    a: {
        c: {
            e: number;
            f: number;
        };
        d: number;
    };
    b: number;
}

interface Action {
    type: "add";
    num: number;
}

function reducer(state: Data, action: Action) {
    switch (action.type) {
        case "add":
            return {
                ...state,
                a: {
                    ...state.a,
                    c: {
                        ...state.a.c,
                        e: state.a.c.e + action.num,
                    },
                },
            };
    }
    return state;
}

function App() {
    const [res, dispatch] = useReducer<Reducer<Data, Action>, string>(
        reducer,
        "zero",
        (param) => {
            return {
                a: {
                    c: {
                        e: 0,
                        f: 0,
                    },
                    d: 0,
                },
                b: 0,
            };
        }
    );

    return (
        <div>
            <div onClick={() => dispatch({ type: "add", num: 2 })}>加</div>
            <div>{JSON.stringify(res)}</div>
        </div>
    );
}

export default App;

这里的 data 是一个复杂的对象结构,我们需要改的是其中的一个属性,但是为了创建新对象,要把其余属性依次复制一遍。

这样能完成功能,但是写起来很麻烦,也不好维护。

有没有什么更好的方式呢?

有,复杂对象的修改就要用 immutable 相关的库了。

最常用的是 immer:

npm install --save immer

用法相当简单,只有一个 produce 的 api:

return produce(state, (state) => {
    state.a.c.e += action.num;
});

第一个参数是要修改的对象,第二个参数的函数里直接修改这个对象的属性,返回的结果就是一个新的对象。

测试下:

功能正常。

用起来超级简单。

immer 是依赖 Proxy 实现的,它会监听你在函数里对属性的修改,然后帮你创建一个新对象。

刚才只说了 reducer 需要返回一个新的对象,才会触发渲染,其实 useState 也是。

比如这样:

import { useState } from "react";

function App() {
    const [obj, setObj] = useState({
        a: {
            c: {
                e: 0,
                f: 0,
            },
            d: 0,
        },
        b: 0,
    });

    return (
        <div>
            <div
                onClick={() => {
                    obj.a.c.e++;
                    setObj(obj);
                }}>
                加
            </div>
            <div>{JSON.stringify(obj)}</div>
        </div>
    );
}

export default App;

因为对象引用没变,同样不会重新渲染:

也可以用 immer:

setObj(
    produce(obj, (obj) => {
        obj.a.c.e++;
    })
);

综上,在 react 里,只要涉及到 state 的修改,就必须返回新的对象,不管是 useState 还是 useReducer。

如果是复杂的深层对象的修改,可以用 immer 来优化。

为什么大家会说 React 推崇的是数据不可变?

就是因为这个。

useRef

state 的保存和修改我们会了,那如何保存 dom 引用呢?

这时候就需要用 useRef 了。

创建 App4.tsx:

import { useEffect, useRef } from "react";

function App() {
    const inputRef = useRef < HTMLInputElement > null;

    useEffect(() => {
        inputRef.current?.focus();
    });

    return (
        <div>
            <input ref={inputRef}></input>
        </div>
    );
}

export default App;

useRef 的类型参数是保存的内容的类型。

这里通过 ref 保存 input 元素的引用,然后在 useEffect 里调用它的 focus 方法。

ref 的内容是保存在 current 属性上的。

在 index.tsx 引入:

可以看到,focus 生效了:

ref 其实就是一个有 current 属性的对象,除了可以保存 dom 引用,也可以放别的内容:

import { useRef } from "react";

function App() {
    const numRef = useRef < number > 0;

    return (
        <div>
            <div
                onClick={() => {
                    numRef.current += 1;
                }}>
                {numRef.current}
            </div>
        </div>
    );
}

export default App;

但它不会触发重新渲染:

想触发渲染,还是得配合 state:

import { useRef, useState } from "react";

function App() {
    const numRef = useRef < number > 0;
    const [, forceRender] = useState(0);

    return (
        <div>
            <div
                onClick={() => {
                    numRef.current += 1;
                    forceRender(Math.random());
                }}>
                {numRef.current}
            </div>
        </div>
    );
}

export default App;

不过一般不这么用,如果想改变内容会触发重新渲染,直接用 useState 或者 useReducer 就可以了。

useRef 一般是用来存一些不是用于渲染的内容的。

单个组件内如何拿到 ref 我们知道了,那如果是想把 ref 从子组件传递到父组件呢?

这种有专门的 api: forwardRef。

forwardRef + useImperativeHandle

之前拿到标签的 dom 元素是这样写的:

通过 useRef 创建个 ref 对象,然后把 input 标签设置到 ref。

如果是想从子组件传递 ref 到父组件,就需要 forwardRef 了,也就是把组件内的 ref 转发一下。

比如这样:

import { useRef } from 'react';
import { useEffect } from 'react';
import React from 'react';

const Guang: React.ForwardRefRenderFunction<HTMLInputElement> = (props, ref) => {
  return <div>
    <input ref={ref}></input>
  </div>
}

const WrapedGuang = React.forwardRef(Guang);

function App() {
  const ref = useRef<HTMLInputElement>(null);

  useEffect(()=> {
    console.log('ref', ref.current)
    ref.current?.focus()
  }, []);

  return (
    <div className="App">
      <WrapedGuang ref={ref}/>
    </div>
  );
}

export default App;

其实 forwardRef 这个 api 做的事情也很容易懂。

就是把 ref 转发到组件内部来设置:

这样就把子组件的 input 的 ref 传递到了父组件。

效果如下:

不过被 forwardRef 包裹的组件的类型就要用 React.forwardRefRenderFunction 了:

第一个类型参数是 ref 的 content 的类型,第二个类型参数是 props 的类型。

但有的时候,我不是想把原生标签暴露出去,而是暴露一些自定义内容。

这时候就需要 useImperativeHandle 的 hook 了。

它有 3 个参数,第一个是传入的 ref,第二个是是返回新的 ref 值的函数,第三个是依赖数组。

这样写:

import { useRef } from 'react';
import { useEffect } from 'react';
import React from 'react';
import { useImperativeHandle } from 'react';

interface RefProps {
  aaa: () => void;
}

const Guang: React.ForwardRefRenderFunction<RefProps> = (props, ref) => {
  const inputRef = useRef<HTMLInputElement>(null);

  useImperativeHandle(ref, () => {
    return {
      aaa() {
        inputRef.current?.focus();
      }
    }
  }, [inputRef]);

  return <div>
    <input ref={inputRef}></input>
  </div>
}

const WrapedGuang = React.forwardRef(Guang);

function App() {
  const ref = useRef<RefProps>(null);

  useEffect(()=> {
    console.log('ref', ref.current)
    ref.current?.aaa();
  }, []);

  return (
    <div className="App">
      <WrapedGuang ref={ref}/>
    </div>
  );
}

export default App;

也就是用 useImperativeHanlde 自定义了 ref 对象:

这样,父组件里拿到的 ref 就是 useImperativeHandle 第二个参数的返回值了。

效果是一样的:

useContext

跨任意层组件传递数据,我们一般用 Context。

创建 App5.tsx

import { createContext, useContext } from "react";

const countContext = createContext(111);

function Aaa() {
    return (
        <div>
            <countContext.Provider value={222}>
                <Bbb></Bbb>
            </countContext.Provider>
        </div>
    );
}

function Bbb() {
    return (
        <div>
            <Ccc></Ccc>
        </div>
    );
}

function Ccc() {
    const count = useContext(countContext);
    return <h2>context 的值为:{count}</h2>;
}

export default Aaa;

用 createContext 创建 context,在 Aaa 里面使用 xxxContext.Provider 修改它的值,然后在 Ccc 里面用 useContext 取出来。

在 index.tsx 里引入:

访问下:

可以看到,拿到的值是被 Provider 修改过的 222。

有的同学可能会问,有 Provider,那有 Consumer 么?

有,class 组件是通过 Consumer 来取 context 的值:

import { createContext, Component } from "react";

const countContext = createContext(111);

class Ccc extends Component {
    render() {
        return <h2>context 的 值为:{this.props.count}</h2>;
    }
}

function Bbb() {
    return (
        <div>
            <countContext.Consumer>
                {(count) => <Ccc count={count}></Ccc>}
            </countContext.Consumer>
        </div>
    );
}

不过现在很少写 class 组件了。

总结一下就是,用 createContext 创建 context 对象,用 Provider 修改其中的值, function 组件使用 useContext 的 hook 来取值,class 组件使用 Consumer 来取值。

组件库里用 Context 很多,比如 antd 里就有大量 Context 的使用:

配置数据基本都是用 Context 传递。

memo + useMemo + useCallback

有两个组件 Aaa、Bbb,Aaa 是 Bbb 的父组件:

import { memo, useEffect, useState } from "react";

function Aaa() {
    const [, setNum] = useState(1);

    useEffect(() => {
        setInterval(() => {
            setNum(Math.random());
        }, 2000);
    }, []);

    return (
        <div>
            <Bbb count={2}></Bbb>
        </div>
    );
}

interface BbbProps {
    count: number;
}

function Bbb(props: BbbProps) {
    console.log("bbb render");

    return <h2>{props.count}</h2>;
}

export default Aaa;

在 Aaa 里面不断 setState 触发重新渲染,问:

bbb render 打印几次?

答案是每 2s 都会打印。

也就是说,每次都会触发 Bbb 组件的重新渲染。

但很明显,这里 Bbb 并不需要再次渲染。

这时可以加上 memo:

import { memo, useEffect, useState } from "react";

function Aaa() {
    const [, setNum] = useState(1);

    useEffect(() => {
        setInterval(() => {
            setNum(Math.random());
        }, 2000);
    }, []);

    return (
        <div>
            <MemoBbb count={2}></MemoBbb>
        </div>
    );
}

interface BbbProps {
    count: number;
}

function Bbb(props: BbbProps) {
    console.log("bbb render");

    return <h2>{props.count}</h2>;
}

const MemoBbb = memo(Bbb);

export default Aaa;

memo 的作用是只有 props 变的时候,才会重新渲染被包裹的组件。

这样就只会打印一次了:

我们让 2s 后 props 变了呢?

import { memo, useEffect, useState } from "react";

function Aaa() {
    const [, setNum] = useState(1);

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

    useEffect(() => {
        setInterval(() => {
            setNum(Math.random());
        }, 2000);
    }, []);

    useEffect(() => {
        setTimeout(() => {
            setCount(Math.random());
        }, 2000);
    }, []);

    return (
        <div>
            <MemoBbb count={count}></MemoBbb>
        </div>
    );
}

interface BbbProps {
    count: number;
}

function Bbb(props: BbbProps) {
    console.log("bbb render");

    return <h2>{props.count}</h2>;
}

const MemoBbb = memo(Bbb);

export default Aaa;

props 变了会触发 memo 的重新渲染:

用 memo 的话,一般还会结合两个 hook:useMemo 和 useCallback。

memo 是防止 props 没变时的重新渲染,useMemo 和 useCallback 是防止 props 的不必要变化。

给 Bbb 加一个 callback 的参数:

参数传了一个 function,你会发现 memo 失效了:

因为每次 function 都是新创建的,也就是每次 props 都会变,这样 memo 就没用了。

这时候就需要 useCallback:

const bbbCallback = useCallback(function () {
    // xxx
}, []);

它的作用就是当 deps 数组不变的时候,始终返回同一个 function,当 deps 变的时候,才把 function 改为新传入的。

这时候你会发现,memo 又生效了:

同理,useMemo 也是和 memo 打配合的,只不过它保存的不是函数,而是值:

const count2 = useMemo(() => {
    return count * 10;
}, [count]);

它是在 deps 数组变化的时候,计算新的值返回。

所以说,如果子组件用了 memo,那给它传递的对象、函数类的 props 就需要用 useMemo、useCallback 包裹,否则,每次 props 都会变,memo 就没用了。

反之,如果 props 使用 useMemo、useCallback,但是子组件没有被 memo 包裹,那也没意义,因为不管 props 变没变都会重新渲染,只是做了无用功。

memo + useCallback、useMemo 是搭配着来的,少了任何一方,都会使优化失效。

但 useMemo 和 useCallback 也不只是配合 memo 用的:

比如有个值的计算,需要很大的计算量,你不想每次都算,这时候也可以用 useMemo 来缓存。

案例代码上传了小册仓库。

总结

这节我们过了一遍 React 常用的 hook:

  • useState:状态是变化的数据,是组件甚至前端应用的核心。useState 有传入值和函数两种参数,返回的 setState 也有传入值和传入函数两种参数。

  • useEffect:副作用 effect 函数是在渲染之外额外执行的一些逻辑。它是根据第二个参数的依赖数组是否变化来决定是否执行 effect,可以返回一个清理函数,会在下次 effect 执行前执行。

  • useLayoutEffect:和 useEffect 差不多,但是 useEffect 的 effect 函数是异步执行的,所以可能中间有次渲染,会闪屏,而 useLayoutEffect 则是同步执行的,所以不会闪屏,但如果计算量大可能会导致掉帧。

  • useReducer:封装一些修改状态的逻辑到 reducer,通过 action 触发,当修改深层对象的时候,创建新对象比较麻烦,可以结合 immer

  • useRef:可以保存 dom 引用或者其他内容,通过 xxRef.current 来取,改变它的内容不会触发重新渲染

  • forwardRef + useImperativeHandle:通过 forwardRef 可以从子组件转发 ref 到父组件,如果想自定义 ref 内容可以使用 useImperativeHandle

  • useContext:跨层组件之间传递数据可以用 Context。用 createContext 创建 context 对象,用 Provider 修改其中的值, function 组件使用 useContext 的 hook 来取值,class 组件使用 Consumer 来取值

  • memo + useMemo + useCallback:memo 包裹的组件只有在 props 变的时候才会重新渲染,useMemo、useCallback 可以防止 props 不必要的变化,两者一般是结合用。不过当用来缓存计算结果等场景的时候,也可以单独用 useMemo、useCallback

当然,react 的 hook 还有一些,那些不大常用的,等用到的时候再说。

class 组件被标记为 lagency 了,现在写 React 组件主要是 function 组件。

函数组件主要是学习 hook,所以把这节的内容掌握了,react 基础就算差不多了。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
1.关于本小册
Next
3.Hook的闭包陷阱的成因和解决方案