7. React Hooks

news/2025/2/13 8:18:54/
  • 官方文档: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 (<divstyle={{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 改变时才会重新调用 countTotalconst 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:绑定一个 Domconst titleDom = useRef();const showTitleDom = () => {console.log(titleDom.current);};// useCallback 没有依赖,所以回调函数不会重新生成// 即使 count 改变,App 重新渲染,回调函数也不会重新生成// 回调函数不重新生成, 回调函数内部的count,只会是第一次渲染时的值const addCount = useCallback(() => {// count 会一直都是 0setCount(count + 1);}, []);// 用法2:绑定一个值const countRef = useRef()countRef.current = count;const addCount2 = useCallback(() => {// count 会一直都是 0setCount(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;
    

http://www.ppmy.cn/news/1571653.html

相关文章

基于轨道角动量自由度在空间频域中的可选择特性

将光的轨道角动量自由度应用到全息领域&#xff0c;证实了轨道角动量全息&#xff1b;实现了高维轨道角动量复用全息技术&#xff0c;获得了高安全的全息加密和超高容量全息信息系统。 1、轨道角动量自由度在全息中的引入 如图1所示&#xff0c;当全息图中没有携带轨道角动量的…

LeetCode刷题第4题【寻找两个正序数组的中位数】---解题思路及源码注释

LeetCode刷题第4题【寻找两个正序数组的中位数 】—解题思路及源码注释 结果预览 目录 LeetCode刷题第4题【寻找两个正序数组的中位数结果预览一、题目描述二、解题思路1、问题理解2、解题思路 三、代码实现及注释1、源码实现2、代码解释 四、执行效果1、时间和空间复杂度分析…

普通人怎样用好Deepseek?

DeepSeek R1由于超强的推理能力&#xff0c;可以赋能很多真实场景&#xff0c;比如编程、数学、数据分析、办公提效等&#xff0c;最近刚开工&#xff0c;打工人们都惊叹其解决问题的能力之强&#xff0c;失业恐慌之感油然而生&#xff0c;所以大家都在加紧在研究DeepSeek怎么帮…

DeepScaleR:仅用 1.5B 参数超越 OpenAI O1-Preview 的强化学习模型

1. 项目概述 1.1 项目目标与意义 DeepScaleR 项目旨在通过强化学习技术推动人工智能模型的性能提升&#xff0c;以更低的成本实现更优的推理能力。其核心目标是开发出在特定任务上超越现有模型的高效模型&#xff0c;同时为开源社区提供技术参考&#xff0c;促进技术的普惠和…

【橘子ES】Metric aggregations指标聚合

一、简介 上文我们说到了聚合的三类聚合&#xff0c;其中包括指标聚合。 Metric aggregations&#xff1a;指标聚合是根据字段值计算量度(如总和或平均值)的量度聚合。 此系列中的聚合基于以某种方式从正在聚合的文档中提取的值来计算度量。这些值通常从文档的字段中提取&…

Docker 安装指南:Windows、Mac、Linux

Docker 是一种非常流行的容器化平台&#xff0c;用于开发、部署和运行应用程序。它通过容器的方式使得应用程序与其依赖环境一同打包并在不同环境中一致地运行。在本篇文章中&#xff0c;我们将介绍如何在 Windows、Mac 和 Linux 上安装 Docker。 1. Docker 安装前的准备工作 …

关于 IoT DC3 中位号(Point)的理解

在开源IoT DC3物联网系统中&#xff0c;位号&#xff08;Point&#xff0c;有的系统也叫Tag、Variable、Node、Signal等&#xff09;用于数据采集、状态监测和报警管理。 位号数据可以通过 PLC、传感器或数据采集模块读取&#xff0c;并存储在数据库或云端&#xff0c;以供进一…

在C++的DLL文件中定义的结构体,在DLL外可以使用吗,如何使用?

在C的DLL文件中定义的结构体&#xff0c;在DLL外可以使用&#xff0c;但需要正确处理类型匹配和内存管理。‌ 在DLL外使用DLL中定义的结构体的方法&#xff1a; 一、 ‌使用extern "C"声明‌&#xff1a; 在DLL的导出声明中使用extern "C"可以确保函数和…