0基础进大厂,第17天,React框架基础篇——钩子函数(hooks)

148 阅读9分钟

引言

官方已经封装好了很多函数,这些钩子函数是react的核心之处
很多功能、效果的实现需要用到这些钩子函数
接下来要讲讲一些最常用的、最关键的钩子函数

准备

  • 依旧创建一个新的react项目
  • 删除不必要的文件,只留下App.jsx和index.jsx
  • 在APP.jsx删除不必要的代码
  • 凡事先看官方文档
  • 快速入门 – React 中文文档
  • 接下来的代码基本是写在src目录下,每个案例可以单独创建一个后缀为.jsx的文件,注意需要抛出和引入

1. useState - 状态管理的基石

useState 是最基础也是最常用的 Hook,用于在函数组件中添加状态。

基本用法

  • useState是官方封装好的函数,需要通过对象解构获取
  • 定义一个响应式变量,提供专门的方法修饰变量值
  • const [count, setCount] = useState(0);
  • 这行代码是声明了一个变量,变量名为count,初始值为0;声明了一个函数,函数名为setCount
  • 函数setCount接收一个参数,为变量count赋值
  • onClick={() => setCount(count + 1)}
  • 这是绑定了一个点击事件,里面放一个回调函数
  • 当点击相应的按钮,数字就是增加或减小
import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>当前计数: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        增加
      </button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>
        减少
      </button>
    </div>
  );
}

image.png

关键点一:数据自动重新渲染的秘密

  • 通过重新加载整个组件实现页面刷新,但是变量会做缓存,确保原先数据不会丢失(这话的意思是确保变量重复声明后还能获取到页面刷新前的值)
  • 观察下面这份代码,页面初次加载时打印了“页面刷新了”
  • 在button按钮上绑定了一个点击事件
  • 每次点击这个按钮,都会让num的值+1
  • num + 1后的值要在页面上重新渲染
  • 这时useState函数就会重新加载整个组件,但是会利用缓存保留之前的num值,使得数据重新渲染到页面上,而不是让num重新声明后回到初始值
  • 所以,每次点击button,都会打印“页面刷新了”,并且num值也在增加
import { useState } from "react";
function App() {
    let [num, setNum] = useState(1); //[1,function]

    function handle() {
        setNum(++num);
    }
    console.log("页面刷新了");

    return (
        <button onClick={handle}>{num}</button>
    )
}

export default App;

image.png

关键点二:useState赋初始值时不能放异步代码

  • 声明了函数query模拟一个接口
  • 将query的返回值传给useState作为num的初始值
  • 但是浏览器并未显示任何东西
  • 这就说明useState赋初始值时不能放异步代码
import { useState } from "react";
function App() {

    async function query() {
        const data = await new Promise((reslove) => {
            setTimeout(() => {
                reslove(2);
            }, 2000)
        })
        return data;
    }
    let [num, setNum] = useState(query()); 

    return (
        <button>{num}</button>
    )
}

export default App;

image.png

2. useEffect - 异步副作用函数

  • 组件每次加载(挂载)会触发
  • useEffect 第二个参数为一个空数组时,只会在组件初次渲染(挂载)时触发
  • useEffect 第二个参数为一个数组,数组中传入一个变量时,该变量每次修改值都会带来useEffect的重新执行
  • useEffect 第一个参数是函数,该函数内部返回出来一个新的函数,新函数会在组件不展示(卸载)时才触发

案例一:组件每次加载(挂载)会触发

  • 初次加载页面,会打印“页面刷新了”
  • 每次手动刷新页面,也都会打印“页面刷新了”
import { useEffect } from "react"

export default function App() {
    useEffect(() => {
        console.log("组件刷新了");
    })
    return (
        <div></div>
    )
}

image.png

案例二:useEffect 第二个参数为一个空数组时,只会在组件初次渲染(挂载)时触发

  • 这份案例里,useEffect 第二个参数没有放任何值
  • 每次点击按钮,都会让num的值+1,利用useState的渲染机制让页面重新刷新
  • useEffect在页面刷新时都会加载一遍
  • 所以出现了页面多次刷新
import { useState, useEffect } from "react";
function App() {

    let [num, setNum] = useState(1);

    useEffect(() => {
        console.log(`页面第${num}次刷新`);
    })

    return (
        <button onClick={handle}>{num}</button>
    )
}

export default App;

image.png 现在第二个参数放数组

  • 可以看到,useEffect里的代码只在页面初次加载时执行了一遍
  • num的值变更,虽然导致了组件被重新加载
  • 但是useEffect里的代码并没有被重新执行
  • 这就说明了useEffect 第二个参数为一个空数组时,只会在组件初次渲染(挂载)时触发
import { useState, useEffect } from "react";
function App() {

    let [num, setNum] = useState(1);

    useEffect(() => {
        console.log(`页面第${num}次刷新`);
    },[])

    return (
        <button onClick={handle}>{num}</button>
    )
}

export default App;

image.png

案例三:useEffect 第二个参数为一个数组,数组中传入一个变量时,该变量每次修改值都会带来useEffect的重新执行

  • 利用钩子函数useState声明了一个变量num
  • 在按钮上绑定了一个点击事件
  • 每次点击都会使得num的值+1
  • useState的渲染机制会让组件重新刷新
  • 这时useEffect的第二个参数为[num]
  • num的值发生改变,就会重新执行useEffect里的代码
  • 所以,浏览器的控制台上就会打印页面第xx次刷新
import { useLayoutEffect, useState, useEffect } from "react";

function App() {
    const [num, setNum] = useState(1)
    
    useEffect(() => {
        console.log(`页面第${num}次刷新`);
    }, [num])

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

export default App;

image.png

3. useLayoutEffect - 同步副作用处理

  • useLayoutEffectuseEffect 类似
  • useLayoutEffect是同步代码

案例:为什么页面始终显示100

  • num值变更引起了组件重新的重新渲染
  • num虽然已经+1,但是在并未渲染到浏览器上时
  • 这段时间,num值变更,会引起组件重新加载,代码从上往下执行,到return时页面才会重新渲染
  • 先执行useLayoutEffect,因为useLayoutEffect第二个参数是[num],机制与useEffect是一样的
  • 在useLayoutEffect里有setNum(100),所以,这时,num值从2变成了100
  • 所以,不管点击多少次,都只能在页面上看到100
function App() {
    const [num, setNum] = useState(1)

    useLayoutEffect(() => {
        setNum(100);
    }, [num])

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

export default App;

image.png

4. useEffect vs useLayoutEffect - 关键区别

特性useEffectuseLayoutEffect
执行时机DOM 更新后异步执行DOM 更新后同步执行
阻塞渲染不阻塞阻塞浏览器绘制
使用场景数据获取、订阅、一般副作用DOM 测量、防止闪烁
性能影响较小可能影响性能

对比示例发现

  • useLayoutEffect防止布局闪烁

5. useReducer + Immer - 复杂状态管理

  • useReducer 适合处理复杂的状态逻辑,配合 Immer 可以更方便地处理不可变数据。

安装 Immer

npm install immer

案例

  • 这是一份简单的案例,模拟了加法和减法运算,并将最终的结果渲染到页面上
  • const [res, dispatch] = useReducer(reducer, { result: 0 })
  • 这时useReducer声明新变量的方式,[]里第一个是声明了一个变量,接收一个对象{ result: 0 },第二个是一个函数
  • 例如这里的dispatch是一个函数,可以理解为一个中转站,会将接收到的值传递给函数reducer,reducer专门处理复杂的运算,这里用加减法简单模拟了一下
  • reducer拿到值后,将值传递给了自己的第二个参数action
  • 初始值{ result: 0 }传递给state
  • useReducer里的参数不可调换位置,第一个必须是一个函数,第二个必须是一个对象
  • reducer里接收两个参数,不可调换位置
import { useReducer } from "react"
import { produce } from 'immer'

function reducer(state, action) {
    // { result: 0 }  { type: 'add', num: 2 }
    console.log(state, action);

    switch (action.type) {
        case 'add':
            return produce(state, (state) => {
                state.result += action.num;
            })
        case 'minus':
            return produce(state, (state) => {
                state.result -= action.num;
            })
    }
}

function App() {
    const [res, dispatch] = useReducer(reducer, { result: 0 })

    return (
        <div>
            <h3>{res.result}</h3>
            <button onClick={() => dispatch({ type: 'add', num: 2 })}>+</button>
            <button onClick={() => dispatch({ type: 'minus', num: 2 })}>-</button>
        </div>
    )
}

export default App

image.png

6. useRef - DOM 引用和值持久化

  • useRef :用于获取Dom结构
  • 与原生JS里的那些方法有所类似
  • 都要先“标记”一下元素
  • const input = useRef(null);
  • 先用useRef声明一个变量
  • 再在Dom结构上加上ref={变量名}
  • 这样就完成了Dom结构的抓取

DOM 操作

import { useEffect, useRef } from "react";

function App() {
    const input = useRef(null);

    useEffect(() => {
        // 页面初次加载将光标聚焦在输入框上
        input.current.focus()
        console.log(input);
    }, [])

    return (
        <div>
            <input type="text" ref={input} />
        </div>
    )
}
export default App;

image.png

7. useContext - 跨组件状态共享

useContext 用于在组件树中共享数据,避免 props 逐层传递。

  • import { createContext, useContext } from "react";
  • 从react中结构出两个钩子函数
  • createContext用于创建上下文对象
  • 这行代码就是声明了一个上下文对象const numContext = createContext()
  • useContext用于获取上下文对象
  • 看代码,需要用声明的对象作为标签
  • <numContext.Provider value={num}> </numContext.Provider>
  • value={num}传递值

案例

  • Child1被包含在父组件里
  • Child2被包含在Child1组件里
  • Child2组件拿到了父组件传递的值
  • 虽然整个组件只抛出了App
  • 但是,页面上三个组件都能正常加载出来
import { createContext, useContext } from "react";


const numContext = createContext()

function App() {
    const num = 100;
    return (
        <div>
            <numContext.Provider value={num}>
                <h1>父组件</h1>
                <Child1 />
            </numContext.Provider>
        </div>
    )
}

function Child1() {
    return (
        <div>
            <h2>子组件</h2>
            <Child2 />
        </div>
    )

}

function Child2() {
    const count = useContext(numContext)
    return (
        <div>
            <h3>孙子组件---{count}</h3>
        </div>
    )

}

export default App;

image.png

9. Hooks 最佳实践

1. 规则遵循

  • 只在最顶层调用 Hook:不要在循环、条件或嵌套函数中调用
  • 只在 React 函数中调用 Hook:函数组件和自定义 Hook
  • 使用 ESLint 插件eslint-plugin-react-hooks 帮助检查规则

2. 性能考虑

// 好的做法:合理使用依赖数组
useEffect(() => {
  fetchData(id);
}, [id]); // 只有 id 变化时才重新获取

// 避免:依赖数组过于频繁变化
useEffect(() => {
  fetchData(user.id);
}, [user]); // user 对象引用变化会导致频繁执行

// 更好的做法:解构需要的值
useEffect(() => {
  fetchData(user.id);
}, [user.id]); // 只关心 id 的变化

总结

React Hooks 为函数组件提供了强大的状态管理和副作用处理能力。通过合理使用这些 Hooks,可以编写出更简洁、更可维护的 React 应用:

hooks(钩子函数)

  • 由 react 官方封装好的的一系列函数,它们的用法和作用

useState ---定义一个响应式变量,提供专门的方法修饰变量值

  • 通过重新加载整个组件实现页面刷新,但是变量会做缓存,确保原先数据不会丢失(这话的意思是确保变量重复声明后还能获取到页面刷新前的值)

useEffect --- 副作用函数

  • 组件每次加载(挂载)会触发
  • useEffect 第二个参数为一个空数组时,只会在组件初次渲染(挂载)时触发
  • useEffect 第二个参数为一个数组,数组中传入一个变量时,该变量每次修改值都会带来useEffect的重新执行
  • useEffect 第一个参数是函数,该函数内部返回出来一个新的函数,新函数会在组件不展示(卸载)时才触发

useLayoutEffect

  • JS的加载会影响HTML的渲染
  • 中的 effect 函数作为同步函数来执行

useReducer

  • 当修改state的逻辑比较复杂时,用useReducer
  • 传入的 reducer 函数中不能直接修改原 state,必须要返回一个新对象
  • 配合 immer 一起使用,直接修改原对象,如果原对象太复杂,返回新对象太麻烦了
  • npm i immer

useRef

  • 获取 DOM 结构

useContext

  • 跨多层组件进行数据传递

掌握这些 Hooks 的使用方法和最佳实践,将让你在 React 开发中更加得心应手。记住,选择合适的工具来解决特定的问题,避免过度工程化,始终保持代码的简洁和可读性。