zoukankan      html  css  js  c++  java
  • [React Hooks长文总结系列二]渐入佳境,性能调优与自定义钩子

    序言

    这一节,将会主要介绍两个钩子:useCallbackuseMemo,以及对自定义hooks的理解。

    useCallback,反复渲染解决之道

    无限请求的“怪象”

    在介绍这个钩子之前,先来看一段react初学者很容易写出来的代码:

    const [detail, setDetail] = useState();

    // 通过后台接口请求数据,并设置到state中
    const fetchDetail = async () => {
        try {
            const res = await xxxx;
            setDetail(res?.data || {})
        } catch(err) {
            // xxx
        }
    };

    // 期望类似于在componentDidMount生命周期中调一次接口获取详情
    useEffect(() => {
        fetchDetail()
    }, [fetchDetail])

    这段代码其实会引发严重的bug,打开network面板,我们将看到浏览器在疯狂向后台发请求。

    这是因为重渲染的时候,上述的fetchDetail函数会被重新创建一遍,并放到deps依赖数组中,由于我们上文中说过,react对比deps里面的内容本质是通过Object.is来做的。两次渲染创建了两个函数,比较两个函数的引用,自然会得到不相等的结果。

    用法

    const memoFn = useCallback(fn, deps);

    比如:

    const showUA = useCallback(function () {
      console.log(navigator.userAgent);
    }, []);

    原理

    其本质就是将一个函数保存到组件外面,并将指向这个函数的指针返回。在这里我们可以粗略地认为:useCallback可以避免函数在每次渲染中被重复创建。

    由于useCallback返回的是一个函数的引用,这个引用自然而然就可以放心地放入deps数组中了。也就是说,每次渲染运行useCallback 钩子拿到的函数引用都是一样的,都指向堆内存的同一个地址。两个引用对比相等,则不会再次运行effectFn

    我们将开头的怪象代码改造如下:

    const [detail, setDetail] = useState();

    const fetchDetail = useCallback(async () => {
        try {
            const res = await xxxx;
            setDetail(res?.data || {})
        } catch(err) {
            // xxx
        }
    });

    useEffect(() => {
        fetchDetail()
    }, [fetchDetail])

    useMemo,Raect中的Computed

    useMemouseCallback的超集。

    useMemo用来缓存一个函数的执行结果。这非常类似于vue中的computed计算属性。假如一个函数的执行结果返回了一个函数,也就相当于缓存了一个函数。这就是为什么说useMemouseCallback的超集,如下:

    const memoFn = useCallback(() => console.log('111'), [])

    const memoFn2 = useMemo(() => () => console.log('111'), [])

    当然,我们一般不会这么做,让useCalback缓存函数引用,useMemo缓存函数执行结果吧!各司其职,易于理解。

    useMemo一般用法如下:

    const memoValue = useMemo(() => a * b, [a, b]);

    React.memo + useCallback,避免子组件无谓的渲染

    这是一个常用的性能优化手段。

    其实React.memo完全可以用useMemo来代替,这里就看个人使用习惯了,就我个人来说,比较喜欢在组件缓存时用React.memo

    我们做个小实验:

    // App.tsx
    import React, { useState } from "react";
    import Sub from "./Sub";

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

      const logFn = () => console.log("logging...");

      return (
        <>
          <Sub logFn={logFn} />
          <h1>{num}</h1>
          <button onClick={() => increaseNum((prev) => prev + 1)}>数字增加</button>
        </>

      );
    }

    export default App;


    // Sub.tsx
    import React, { useEffect } from "react";

    export interface SubProps {
      logFn() => void;
    }

    const Sub: React.FC<SubProps> = ({ logFn }) => {
      useEffect(() => {
        console.log("sub render");
      });

      return <button onClick={logFn}>打印</button>;
    };

    export default Sub;

    非常简单的一个示例,运行上述代码时可以发现:父组件点击数字+1,子组件莫名其妙被重渲染,打印出sub render

    所以这里我们需要两步:

    1. 缓存子组件,避免子组件无条件重渲染,这是一定要做的;
    2. 如果子组件用到了父组件內声明的函数,则缓存该函数,避免出现“父组件重渲染 --> 重新创建函数 --> 子组件受影响重渲染”这样的问题。

    上述代码关键地方改造如下(其他地方保持不变):

    // App.tsx
    const logFn = useCallback(() => console.log("logging..."), []);

    // Sub.tsx
    export default React.memo(Sub);

    改造完后,父组件再点击数字+1,可以发现子组件已经不会重渲染了。

    自定义hooks,逻辑封装与组合

    很喜欢一句话:大千世界无奇不有,但实际上不过是由一百多种元素组成。

    所谓自定义 hook,就是基于官方的几种 hook,通过各种排列组合+实际业务场景得到的独立逻辑体。自定义 hook 就像是 hooks 中的歌曲,只要有创造力,脑洞够大,就能写出令人惊叹的自定义 hook。

    用法

    如下是一个简单的示例:

    const useFetchUserInfo = () => {
        const [isLoading, setIsLoading] = useState(false);
        const [userInfo, setUserInfo] = useState();
        
        const fetchFn = useCallback(async () => {
            setIsLoading(true);
            try {
                const res = await xxxx;
                setUserInfo(res?.data || {})
            } finally {
                setIsLoading(false);
            }
        });
        
        return [isLoading, useInfo, fetchFn];
    }

    关于自定义hooks应该return一个数组还是对象,并没有明确的所谓“最佳实践”,我个人认为如果返回的内容足够简单,可以使用数组返回;如果内容较多,则使用对象返回。

    还需要注意两个问题:

    1. 自定义 hook 的命名格式一定要为useXXX。后面的单词首字母还必须得大写,如果尝试违反这种命名规则,编辑器会报错;

    2. react 官方的 hook 只能用在函数式组件(通过首字母大写+返回 ReactElement 来判断)里面、或者自定义 hook 里面,且只能在顶层调用。

    原理

    其实跟之前的 useStateuseEffect 调用的方式就一样,无论是生成链表还是读取链表,这个过程都完全一致。因为我们要明白自定义 hook 的本质:它不过就是把几个官方内置 hook 的调用过程封装成了一个可复用的函数,并没有增加新的内容。

    建议

    在这一部分中,我们在项目使用了阿里开源的ahooks,着实为项目解决了不少问题,这是一个非常成熟的生产级别的自定义hooks库,后续可以考虑继续使用。

  • 相关阅读:
    5月读书日志
    把代码搬到Git Hub 吧(一)
    RTX二次开发(二)(基于ASP.NET)
    RTX二次开发(一)(基于ASP.NET)
    文件夹下迭代查询文件
    JS URL传递中文参数时出现乱码的处理
    js实现上下滑动侧边栏
    基本select语句的生命周期
    NodeJs下的测试框架Mocha
    带新人感想
  • 原文地址:https://www.cnblogs.com/zhangnan35/p/14596436.html
Copyright © 2011-2022 走看看