您现在的位置是:首页 >技术交流 >7. React Hooks网站首页技术交流

7. React Hooks

草木红 2025-05-29 12:01:02
简介7. React Hooks
  • 官方文档:https://zh-hans.react.dev/reference/react/hooks
  • 官方文档:https://zh-hans.legacy.reactjs.org/docs/hooks-intro.html
  • Router6 的一个中文文档:https://baimingxuan.github.io/react-router6-doc/
  • react:版本 18.2.0
  • node: 版本18.19.1
  • 脚手架:版本 5.0.1
  • 用例中的干净的脚手架的创建可以参考另一篇文章:3.React 组件化开发

一、认识 Hooks

(一)类组件与 函数组件对比

【1】优势

  • class组件可以定义自己的state,用来保存组件自己内部的状态;
    • 函数式组件不可以,因为函数每次调用都会产生新的临时变量;
  • class组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑;
    • 比如在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次;
    • 函数式组件在学习hooks之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求;
  • class组件可以在状态改变时只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate等;
    • 函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次;
  • 所以,在Hook出现之前,对于上面这些情况我们通常都会编写class组件。

【2】劣势

  • 复杂组件变得难以理解:
    • 我们在最初编写一个class组件时,往往逻辑比较简单,并不会非常复杂。但是随着业务的增多,我们的class组件会变得越来越复杂;
    • 比如componentDidMount中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在componentWillUnmount中移除);
    • 而对于这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度;
  • 难以理解的class:
    • 很多人发现学习ES6的class是学习React的一个障碍。
    • 比如在class中,我们必须搞清楚this的指向到底是谁,所以需要花很多的精力去学习this;
    • 虽然我认为前端开发人员必须掌握this,但是依然处理起来非常麻烦;
  • 组件复用状态很难:
    • 在前面为了一些状态的复用我们需要通过高阶组件;
    • 像我们之前学习的redux中connect或者react-router中的withRouter,这些高阶组件设计的目的就是为了状态的复用;
    • 或者类似于Provider、Consumer来共享一些状态,但是多次使用Consumer时,我们的代码就会存在很多嵌套;
    • 这些代码让我们不管是编写和设计上来说,都变得非常困难;

(二)Hook 介绍

  • Hook的出现,可以解决上面提到的这些问题;
  • 简单总结一下hooks:
    • 它可以让我们在不编写class的情况下使用state以及其他的React特性;
    • 但是我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决;
  • Hook的使用场景:
    • Hook的出现基本可以代替我们之前所有使用class组件的地方;
    • 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它;
    • Hook只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用;
  • 在我们继续之前,请记住 Hook 是:
    • 完全可选的:你无需重写任何已有代码就可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook。
    • 100% 向后兼容的:Hook 不包含任何破坏性改动。
    • 现在可用:Hook 已发布于 v16.8.0。

二、Hook API

建议直接看官网:https://zh-hans.react.dev/reference/react/useState

(一)useState

  • 在组件的顶层调用 useState 来声明一个 状态变量。如果没有传递参数,那么初始化值为undefined。

  • useState 返回一个由两个值组成的数组:

    • 当前的 state。在首次渲染时,它将与你传递的 initialState 相匹配。
    • set 函数,它可以让你将 state 更新为不同的值并触发重新渲染。
    const [state, setState] = useState(initialState)
    

(二)useEffect

  • useEffect 可以让你来完成一些类似于class中生命周期的功能;

  • useEffect 是一个 React Hook,它允许你 将组件与外部系统同步。

    useEffect(setup, dependencies?)
    
    • setup:处理 Effect 的函数。setup 函数选择性返回一个 清理(cleanup) 函数。当组件被添加到 DOM 的时候,React 将运行 setup 函数。在每次依赖项变更重新渲染后,React 将首先使用旧值运行 cleanup 函数(如果你提供了该函数),然后使用新值运行 setup 函数。在组件从 DOM 中移除后,React 将最后一次运行 cleanup 函数。
    • dependencies:该useEffect在哪些state发生变化时,才重新执行;(受谁的影响)
  • Hook 允许我们按照代码的用途分离出多个useEfffect , 而不是像生命周期函数那样

  1. 创建一个干净的脚手架

  2. 在src 文件夹下创建 Son.jsx 文件,用于创建子组件

    import React, { memo, useEffect, useState } from 'react';
    
    const Son = (props) => {
      const [conter, setConter] = useState(0);
      const [msg, setMsg] = useState('一条信息');
    
      // 每个 useEffect 对应一个功能
      // 每次渲染时执行
      useEffect(() => {
        console.log(props);
    
        console.log('Son组件挂载了');
    
        const timer = setInterval(() => {
          console.log('Son组件内部定时器');
        }, 1000);
    
        // 组件卸载时清除定时器,(类似componentWillUnmount 生命周期函数)
        // 如果不清除,定时器会一直存在
        return () => {
          timer && clearInterval(timer);
          console.log('Son组件卸载了');
        };
      });
    
      // 每次渲染时执行
      useEffect(() => {
        document.title = conter;
      });
    
      // 回调函数只在 msg 变化时执行,
      // 不受别的变量的影响
      useEffect(() => {
        console.log('msg 被修改');
      }, [msg]);
    
      // 不受任何的变量的影响,只执行一次
      // 类似 componentDidMount 生命周期函数
      useEffect(() => {
        console.log('不受任何的变量的影响');
      }, []);
    
      return (
        <div>
          <p>{props.title}</p>
          Son:{conter}
          <p>{msg}</p>
          <button onClick={(e) => setConter(conter + 1)}>增加 + 1</button>
          <button onClick={(e) => setMsg('另一个消息')}>修改 msg</button>
        </div>
      );
    };
    
    export default Son;
    
  3. 修改sr/App.js 文件内容如下

    import React, { memo, useState } from 'react';
    import Son from './Son';
    
    const App = () => {
      const [flag, setFlag] = useState(true);
      const [title, setTitle] = useState('标题');
    
      const changeFlag = () => {
        setFlag(!flag);
      };
    
      const changeTitle = () => {
        setTitle('标题2');
      };
    
      return (
        <div>
          {flag && <Son title={title} />}
          <button onClick={changeFlag}>销毁/挂载子组件</button>
          {/* 修改 title 后 Son组件将会被销毁并重新挂载 */}
          <button onClick={changeTitle}>修改title</button>
        </div>
      );
    };
    
    export default App;
    

(三)useContext

  • useContext 是一个 React Hook,可以让你读取和订阅组件中的 context。

    const value = useContext(SomeContext)
    
    • SomeContext:先前用 createContext 创建的 context。context 本身不包含信息,它只代表你可以提供或从组件中读取的信息类型。
  1. 创建一个干净的脚手架

  2. 在 src 文件夹下创建 context.js 文件,用于存放共用信息

    import React from 'react';
    
    export const themes = {
      light: {
        foreground: '#000000',
        background: '#eeeeee',
        color: '#000',
      },
      dark: {
        foreground: '#ffffff',
        background: '#222222',
        color: '#fff',
      },
    };
    
    // 创建context对象的
    export const ThemeContext = React.createContext();
    
    export const user = {
      id: '001',
      name: '张三',
      age: 18,
      gender: '男',
    };
    
    // 创建context对象的, 将 user 作为默认值
    export const UserContext = React.createContext(user);
    
  3. 在src 文件夹下创建 User.jsx 文件,用于展示用户信息

    import React, { memo, useContext } from 'react';
    import { UserContext, ThemeContext } from './context';
    
    const User = memo(() => {
      // user 使用的是 createContext 设置的默认值
      const user = useContext(UserContext);
      // theme 使用的是 ThemeContext.Provider 设置的默认值
      const theme = useContext(ThemeContext);
    
      return (
        <div
          style={{
            color: theme.color,
            background: theme.background,
          }}
        >
          <p>姓名:{user.name}</p>
          <p>年龄:{user.age}</p>
        </div>
      );
    });
    
    export default User;
    
  4. 修改App.js 文件如下

import React, { memo } from 'react';
import { themes, ThemeContext } from './context';
import User from './User';

const App = memo(() => {
  return (
    <div>
      {/* 给 ThemeContext 设置值*/}
      <ThemeContext.Provider value={themes.dark}>
        <User />
      </ThemeContext.Provider>
    </div>
  );
});

export default App;

(四)useCallback

  • useCallback实际的目的是为了进行性能的优化。

  • useCallback 是一个允许你在多次渲染中缓存函数的 React Hook。

    const cachedFn = useCallback(fn, dependencies)
    
  • useCallback会返回一个函数的 memoized(记忆的) 值;

  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的;

  • 通常使用useCallback的目的是不希望子组件进行多次渲染,并不是为了函数进行缓存

  1. 创建一个干净的脚手架
  2. 修改src/App.js 文件内容如下:
    import React, { memo, useCallback, useState } from 'react';
    
    // 创建一个子组件
    const Child = memo((props) => {
      console.log('Child 组件重新渲染了');
      const { customChangeCounter } = props;
      return (
        <div>
          Child
          <button onClick={customChangeCounter}>修改counter</button>
        </div>
      );
    });
    
    // 每次 counter 改变时,会重新渲染整个 App 组件
    const App = memo(() => {
      const [counter, setCounter] = useState(0);
      const [name, setName] = useState('张三');
    
      console.log('App 组件重新渲染了');
    
      function changeName() {
        setName('李四');
      }
    
      // 每次 counter 改变时,会重新渲染整个 App 组件
      // changeCounter1 也会被重新定义
      function changeCounter1() {
        console.log('changeCounter1被定义');
    
        setCounter(counter + 1);
      }
    
      // 如果没有第二个参数,和上边的 changeCounter1 没有区别
      // 第二个参数是依赖项,只有依赖项改变时,useCallback 返回值才会是一个新的函数
      // 注:App 组件重新渲染时 useCallback 的第一个参数会被重新定义,
      //     只是 useCallback 返回值会根据依赖项改变而改变
      const changeCounter2 = useCallback(() => {
        console.log('changeCounter2被定义');
        setCounter(counter + 1);
        console.log(counter);
      }, [counter]);
    
      // 依赖项不改变,useCallback 返回值不会改变
      const changeCounter3 = useCallback(() => {
        console.log('changeCounter3被定义');
        // counter 将不会改变
        setCounter(counter + 1);
        console.log(counter);
      }, []);
    
      return (
        <div>
          <p>{counter}</p>
          <p>{name}</p>
          {/**
           * 1.customChangeCounter 传递changeCounter1 时,修改name 后,Child1 也会重新渲染
           *   因为 name 被修改后 App 组件重新渲染, changeCounter1 被重新定义,
           *   customChangeCounter 获取到一个新值,所以 Child1 重新渲染
           * 2.customChangeCounter 传递changeCounter2 时,修改name 后,Child1 不重新渲染
           *   因为 counter 没有改变,changeCounter2 没有被重新定义,
           */}
          <Child customChangeCounter={changeCounter2} />
          <button onClick={() => changeCounter1()}>button1:+1</button>
          <button onClick={() => changeCounter2()}>button2:+1</button>
          <button onClick={() => changeCounter3()}>button3:+1</button>
          <button onClick={changeName}>修改name</button>
        </div>
      );
    });
    
    export default App;
    

(五)useMemo

  • useMemo 是一个 React Hook,它在每次重新渲染的时候能够缓存计算的结果。

  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的;

    const cachedValue = useMemo(calculateValue, dependencies)
    
  1. 创建一个干净的脚手架
  2. 修改src/App.js 文件内容如下:
    import React, { memo, use, useMemo } from 'react';
    
    // 子组件
    const Child = memo((props) => {
      console.log('Child 被渲染');
      const { user } = props;
      return (
        <div>
          {user.name}-{user.age}
        </div>
      );
    });
    
    // 计算属性
    const countTotal = (count) => {
      let total = 0;
      console.log('countTotal 被调用');
      for (let i = 1; i <= count; i++) {
        total += i;
      }
    
      return total;
    };
    
    const App = memo(() => {
      console.log('App 被渲染');
      const [count, setCount] = React.useState(0);
      const [user, setUser] = React.useState({ name: '张三', age: 18 });
    
      const changeUser = () => {
        setUser({ name: '李四', age: 20 });
      };
    
      /**
       * user 改变时,会重新渲染整个 App 组件
       * countTotal 也会被重新调用
       */
      // const total = countTotal(count);
    
      // 只有 count 改变时才会重新调用 countTotal
      const total2 = useMemo(() => {
        return countTotal(count);
      }, [count]);
    
      return (
        <div>
          <p>{count}</p>
          <p>{total2}</p>
          <Child user={user} />
          {/* 修改 count, 子组件没有被重新渲染 */}
          <button onClick={() => setCount(count + 1)}>+1</button>
          <button onClick={changeUser}>修改 User</button>
        </div>
      );
    });
    
    export default App;
    
    

(六)useRef

  • useRef 是一个 React Hook,它能帮助引用一个不需要渲染的值。

  • useRef返回一个ref对象,返回的ref对象再组件的整个生命周期保持不变

    const ref = useRef(initialValue)
    
  1. 创建一个干净的脚手架
  2. 修改src/App.js 文件内容如下:
    import React, { memo, useCallback, useRef } from 'react';
    
    const App = memo(() => {
      const [count, setCount] = React.useState(0);
      console.log('App 被渲染');
    
      // 用法1:绑定一个 Dom
      const titleDom = useRef();
      const showTitleDom = () => {
        console.log(titleDom.current);
      };
    
      // useCallback 没有依赖,所以回调函数不会重新生成
      // 即使 count 改变,App 重新渲染,回调函数也不会重新生成
      // 回调函数不重新生成, 回调函数内部的count,只会是第一次渲染时的值
      const addCount = useCallback(() => {
        // count 会一直都是 0
        setCount(count + 1);
      }, []);
    
      // 用法2:绑定一个值
      const countRef = useRef()
      countRef.current = count;
      const addCount2 = useCallback(() => {
        // count 会一直都是 0
        setCount(countRef.current + 1);
      }, []);
    
      return (
        <div>
          {count}
          <h2 ref={titleDom}>标题</h2>
          <button onClick={showTitleDom}>显示标题Dom</button>
          <button onClick={addCount}>增加Count</button>
          <button onClick={addCount2}>增加Count2</button>
        </div>
      );
    });
    
    export default App;
    

(七)useImperativeHandle

  • useImperativeHandle 是 React 中的一个 Hook,它能让你自定义由 ref 暴露出来的句柄(通过useImperativeHandle可以只暴露固定的操作)
    useImperativeHandle(ref, createHandle, dependencies?)
    
  1. 创建一个干净的脚手架
  2. 修改src/App.js 文件内容如下:
    import React, { forwardRef, memo, useImperativeHandle, useRef } from 'react';
    
    // 子组件1
    const Child1 = memo(forwardRef((props, ref) => {
      return (
        <div>
          <input ref={ref} type="text" />
        </div>
      );
    }));
    
    // 子组件2
    const Child2 = memo(forwardRef((props, ref) => {
      const inputRef = useRef();
    
      // 父组件通过ref,调用子组件的方法
      // 只能使用 useImperativeHandle,第二个参数所暴露的方法
      useImperativeHandle(ref, () => ({
        // 只暴露聚焦方法
        focus: () => {
          console.log(inputRef.current);
          inputRef.current.focus();
        },
      }));
    
      return (
        <div>
          <input ref={inputRef} type="text" />
        </div>
      );
    }));
    
    const App = memo(() => {
      const childRef1 = useRef();
      const childRef2 = useRef();
    
      return (
        <div>
          <Child1 ref={childRef1} />
          <Child2 ref={childRef2} />
    
          <button onClick={() => childRef1.current.focus()}>聚焦Child1</button>
          <button onClick={() => childRef1.current.value = 2}>修改值Child1</button>
          <button onClick={() => childRef2.current.focus()}>聚焦Child2</button>
          <button onClick={() => childRef2.current.value = 2}>修改值Child2</button>
        </div>
      );
    });
    
    export default App;
    

(八)useLayoutEffect

  • useLayoutEffect 可能会影响性能。尽可能使用 useEffect。

  • useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已:

    • useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新;
    • useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新;
  • useLayoutEffect 是 useEffect 的一个版本,在浏览器重新绘制屏幕之前触发。

    useLayoutEffect(setup, dependencies?)
    
  1. 创建一个干净的脚手架
  2. 修改src/App.js 文件内容如下:
    import React, { memo, useState, useEffect, useLayoutEffect } from 'react';
    
    const App = memo(() => {
      const [count, setCount] = useState(5);
    
      // 会有闪烁现象
      useEffect(() => {
        console.log('useEffect');
        if (count === 0) {
          setCount(Math.random() + 88);
        }
      });
    
      // useLayoutEffect(() => {
      //   console.log('useEffect');
      //   if (count === 0) {
      //     setCount(Math.random() + 88);
      //   }
      // });
    
      return (
        <div>
          <p>{count}</p>
          <button onClick={() => setCount(0)}> count 设置为0</button>
        </div>
      );
    });
    
    export default App;
    
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。