React---- Hooks大全

125 阅读15分钟

image.png

一 前言

1.1 Hooks的定义

  • react推荐使用函数组件,但是有时需要使用state或其他功能时,只能在类组件中使用。因为函数组件没有实例,没有生命周期,只有类组件中才有。

1.2 React hooks解决什么问题

如果没有Hooks,函数组件能够做的只是接受 Props、渲染 UI ,以及触发父组件传过来的事件。所有的处理逻辑都要在类组件中写,这样会使 class 类组件内部错综复杂,每一个类组件都有一套独特的状态,相互之间不能复用。【组件之间的复用状态】

  • 状态逻辑难复用:组件之间的复用状态逻辑很难,可能要用到render props(渲染属性)或者HOC(高阶组件),只能在原先的组件外包裹一层父容器,会导致层级冗余。
  • this指向问题:父组件给子组件传递函数时,必须绑定this,导致性能上有损耗。

1.3 Hooks执行原理

Hooks是在fiber节点上存放了memorizedState链表,每个hook都从对应的链表元素上存取自己的值。 组件第一次被渲染时,为每个hook创建一个对象。

type Hook = {
      memorizedState:any,
      baseState:any,
      baseUpdate:Update<any,any> | null,
      queue:UpdateQueue<any,any> | null,
      next:Hook | null
}

这个对象的memorizedState属性是用来存储组件上一次更新后的state,而next则指向下一个hook对象,最终形成一个链表。

二 hooks 之数据更新驱动

2.1 useState

useState使函数组件像类组件一样拥有state,函数组件通过useState可以让组件重新渲染,更新视图。

    // 方法一:初始值为基础数据类型或者Object
    const [ ①state , ②dispatch ] = useState(③initData)
    // 方法二:初始值为函数
    const [state,setState] = useState(()=> initialState)

①state,目的提供给UI,作为渲染视图的数据源

②dispatch改变state的函数

③initData有两种情况,第一种情况是非函数(作为state初始化的值)。第二种情况是函数,(函数返回值作为useState初始化的值)

2.1.1 useState对于复杂数据类型的写法

    const [arr,setArr] = useState([1,2,3])

    setArr([...arr,4])  // 末尾新增 扩展运算符
    setArr([0,...arr])  // 头部新增 扩展运算符
    setArr(arr.filter((item) => item !== 1)  // 删除指定元素
    setArr(arr.map(item => { return item == 2 ? 555 : item}))  //替换数据

2.1.2 useState修改初始值对象的值

    const [obj,setObj] = useState({k1:"v1",k2:"v2"})

    <button onClick={() =>setObj(obj =>({...obj,k2:obj.k2 + 1}))}> + </button>

2.1.3直接更新和函数更新的区别

  • 直接更新:在一次渲染中,无论调用多少次set函数,页面只会更新1次,相同的set函数只有最后一次调用会生效。
  • 函数更新:页面只会渲染一次,每一次调用都能够获取到上一次的状态进行计算,react会将函数放入到一个队列中,在渲染前会依次执行函数。
<!---->

    const [number,setNum] = useState(0)
    /*直接改变状态*/
    const increaseNumber = () =>{
    /*页面渲染一次,累加一次,最后一次有效*/
      setNum(number + 1)
      setNum(number + 2)
      setNum(number + 1)
    }
    /*函数更新*/
    const increaseNumber = () =>{
    /*页面渲染一次,累加3次,每一次都有效*/
      setNum((number)=> number + 1)
      setNum((number)=> number + 1)
      setNum((number)=> number + 1)
    }

2.1.4函数式更新(取最新值)

新的状态依赖于旧的状态,推荐使用函数式更新。因为状态更新可能是异步的。

    setArr(prevCount => prevCount + 1)

2.1.5 useState异步回调获取不到最新值?

xiaoshen.blog.csdn.net/article/det…

在异步回调或闭包中获取最新状态并设置状态出现异常。

解决方案:

封装一个hooks将state和ref关联,提供一个方法供异步中获取最新值使用。

const useGetState = (initVal)=>{
  const [state,setState] = useState(initVal)
  const ref = useRef(initVal)
  const setStateCopy = (newVal) =>{
    ref.current = newVal
    setState(newVal)
  }
  const getState = () =>ref.current
  return [state,setStateCopy,getState]
}

const App =() => {
  const [arr,setArr,getArr] = useGetState([0])
  useEffect(()=>{
    console.log(arr)
  },[arr])
  const handleClick = () =>{
    Promise.resolve().then(()=>{
      setArr([...getArr(),1])
    })
    .then(() =>{
     setArr([...getArr(),2])
    })
  }
  return (
   <>
   <button onClick={handleClick}>change</button>
   <>
  )
}

2.1.6 useState执行机制

  • React17版本
    • 组件生命周期或react合成事件中,是异步
    • 在setTimeout或原生dom事件中,是同步
  • react18版本
    • 都是异步执行,提高性能(批量更新)

2.1.7 useState返回数组而非对象的原因

  • 数组解构赋值允许自定义状态变量命名,而对象解构必须使用固定的属性名。
  • 每次渲染时。useState返回的状态值和更新函数保持相同的索引位置,这样可以确保函数组件在渲染时访问到对应的状态值和更新函数。

2.2 useReducer(state比较复杂)

useReducer 是 react-hooks 提供的能够在无状态组件中运行的类似redux的功能 api 。

    const [ ①state , ②dispatch ] = useReducer(③reducer,initialState,init?)

① 更新之后的 state 值。

② 派发更新的 dispatchAction 函数, 本质上和 useState 的 dispatchAction 是一样的。

③ reducer是一个函数 ,我们可以认为它就是一个 redux 中的 reducer , reducer的参数就是常规reducer里面的state和action, 返回改变后的state, 这里有一个需要注意的点就是:如果返回的 state 和之前的 state ,内存指向相同,那么组件将不会更新。

initialState是开始时的状态值

init是一个可选的初始化函数,用于延迟创建初始状态

2.2.1 useReducer使用场景

  • state状态比较复杂
  • 多个state状态之间有依赖关系

三 hooks之执行副作用

3.1 useEffect

useEffect,允许在函数组件中执行副作用的操作。副作用是指那些对外部世界产生影响的操作,例如数据获取、订阅、更改React组件之外的DOM等

使用useEffect:

useEffect(() =>{
  // 执行副作用逻辑
},[/* 依赖列表*/])
  • 1、 没有依赖(不传递第二个参数):如果不提供依赖数组,副作用函数在每次渲染后和所有更新后都会执行。
useEffect(() =>{
  // 在每次组件渲染后都会执行
})
  • 2 、空依赖(空数组):如果依赖数组为空([]),副作用函数只会在组件挂载(mount)后执行一次,并在组件卸载时执行清理(如果提供了清理函数)
useEffect(()=>{
 //只在组件挂载时执行一次
 return() =>{
  // 在组件卸载时执行清理
 }
},[])
  • 3、 具有依赖的useEffect:如果依赖数组中有值,首次挂载后执行,后续的每一次渲染后,React会使用Object.is()算法比较数组中每一个依赖项的当前值和上一次渲染时的值。【object.is()比较机制:原始类型,比较的是值;引用类型,比较的是引用地址】
useEffect(() =>{
  // 依赖值改变时执行
},[dependency])

image.png

3.1.1 useEffect的原理

基于React组件的生命周期函数。当组件的props或state发生变更时,会触发一个更新循环。调用useEffect中的函数,根据组件中获取的变更信息执行useEffect中定义的操作。

3.1.2 useEffect的副作用执行是在React渲染的那个阶段?

useEffect 的副作用执行是在 React 完成 DOM 更新之后,也就是在浏览器的重新绘制(repaint)和重排(reflow)之后。

3.1.3 useEffect使用场景

  • 初次加载页面(组件挂载)
  • 响应式变量发生变化,触发页面根据新值重新渲染(组件更新)
  • 关闭页面(组件卸载)

3.1.4 react18 useEffect执行两次?

在开发环境中除了必要的挂载之外,还“额外”模拟执行了一次组件的卸载和挂载

3.1.4.1让useEffect只执行一次【清理副作用】
/*清理事件监听------在返回函数内部"取消掉事件监听"即可*/
useEffect(()=>{
  function handleScroll(e){}
  window.addEventListener('scroll',handleScroll)
  return ()=> window.removeEventListener('scroll',handleScroll)
},[])
/*重置页面数据,清理属性状态*/
useEffect(()=>{
  const node.style.opacity = 1
  return ()=>{node.style.opacity = 0}
},[])

3.1.5 useEffect闭包陷阱

import React, { useState, useEffect } from 'react';
 
function MyComponent() {
  const [count, setCount] = useState(0);
 
  useEffect(() => {
    const intervalId = setInterval(() => {
      // 问题:这里的count总是最初的值,即使setCount被调用
      console.log(`Count: ${count}`);
    }, 1000);
 
    return () => clearInterval(intervalId);
  }, []); // 依赖数组为空,意味着effect不会重新运行
}

由于useEffect的依赖数组是空的([]),所以定时器只在组件挂载时设置一次。然而,定时器内部的闭包捕获了count的初始值(0),即使在组件内部count改变了,定时器中使用的count仍然是初始值。

  • 解决方案一:
import React, { useState, useEffect } from 'react';
 
function MyComponent() {
  const [count, setCount] = useState(0);
 
  useEffect(() => {
    const intervalId = setInterval(() => {
      console.log(`Count: ${count}`);
    }, 1000);
 
    return () => clearInterval(intervalId);
  }, [count]); // 依赖数组包含count,因此每当count变化时,effect都会重新运行
}

通过将count添加到依赖数组中,每当count更新时,useEffect都会重新运行,从而正确地使用最新的count值。

  • 解决方案二:使用函数和引用
import React, { useState, useEffect } from 'react';
 
function MyComponent() {
  const [count, setCount] = useState(0);
 
  useEffect(() => {
    const intervalId = setInterval(() => {
      const logCount = () => {
        console.log(`Count: ${count}`);
      };
      logCount(); // 使用函数来确保使用的是最新的count值
    }, 1000);
 
    return () => clearInterval(intervalId);
  }, [count]); // 仍然需要count在依赖数组中以确保正确的重新运行时机
}

3.1.6 useEffect依赖项是函数,如何处理

如果将一个函数作为依赖项,需要注意这个函数是如何定义的,因为它可能会导致无限循环或不必要的副作用执行。【当函数为依赖项时,每次组件更新渲染,如果该函数的内容发生变化,useEffect都会再次执行】

  • 解决方案一:使用useCallback
import React, { useState, useEffect, useCallback } from 'react';
 
function MyComponent() {
  const [count, setCount] = useState(0);
 
  // 使用useCallback来确保只有在依赖项变化时才重新创建函数
  const handleChange = useCallback(() => {
    // 你的逻辑
    console.log('Count changed:', count);
  }, [count]); // 确保依赖项是最新的count
 
  useEffect(() => {
    // 使用handleChange作为依赖项
    handleChange();
  }, [handleChange]); // 这样handleChange只有在变化时才会导致副作用执行
 
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

  • 解决方案二:直接在useEffect中使用匿名函数
import React, { useState, useEffect } from 'react';
 
function MyComponent() {
  const [count, setCount] = useState(0);
 
  useEffect(() => {
    // 直接在useEffect中定义匿名函数
    const handleChange = () => {
      console.log('Count changed:', count);
    };
    handleChange(); // 调用函数
  }, [count]); // count作为依赖项,确保只有在count变化时才执行副作用
 
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

⚠️:如果将一个依赖于其自身副作用输出的函数作为依赖项,可能会导致无限循环 例如:

useEffect(() => {
  const intervalId = setInterval(() => {
    // 这里使用了intervalId,但将它作为依赖项可能导致无限循环
    console.log('Interval tick');
  }, 1000);
  return () => clearInterval(intervalId); // 清理函数确保组件卸载时清除定时器
}, [intervalId]); // 错误!这将导致无限循环,因为intervalId依赖于其自身输出的值。

解决方法:不将定时器的ID作为依赖项,或使用useRef存储定时器ID

import React, { useState, useEffect, useRef } from 'react';
 
function MyComponent() {
  const intervalId = useRef(); // 使用useRef避免无限循环问题
  const [count, setCount] = useState(0); // 示例状态变量,实际应用中可能不需要它来控制定时器。
  useEffect(() => {
    intervalId.current = setInterval(() => {
      console.log('Interval tick');
      setCount(prevCount => prevCount + 1); // 更新状态以显示效果(可选)
    }, 1000);
    return () => clearInterval(intervalId.current); // 清理函数确保组件卸载时清除定时器
  }, []); // 不需要依赖项,因为我们不依赖于外部的state或props值。如果需要依赖于外部值(例如props),则不包括此空数组。但通常定时器不依赖于外部值。
  return (
    <div>Count: {count}</div> // 显示状态更新的效果(可选)
  );
}

3.1.7 useEffect依赖项是对象或数组,如何处理?

如果依赖项是一个对象或数组,要确保依赖项的引用不变性。React将不会在对象的属性或数组的元素发生变化时重新运行副作用。因为React通过比较引用判断是否需要重新运行副作用。

  • 解决方案一:使用useMemo包装对象或数组
import React, { useEffect, useMemo } from 'react';
 
function MyComponent() {
  const [data, setData] = useState({});
 
  const memoizedData = useMemo(() => {
    // 这里可以执行一些计算或处理,返回一个新的对象或数组
    return { ...data }; // 例如,返回data的一个深拷贝
  }, [data]); // data变化时,memoizedData会更新
 
  useEffect(() => {
    // 副作用逻辑,使用memoizedData作为依赖项
    console.log('副作用运行', memoizedData);
  }, [memoizedData]); // memoizedData变化时,副作用会重新运行
 
  return (
    <div>
      {/* 组件内容 */}
    </div>
  );
}

  • 解决方案二:使用useRef,useRef可以创建一个可变的引用对象,其.current属性持有的值在组件的整个生命周期内保持不变,除非显式修改
import React, { useEffect, useRef } from 'react';
 
function MyComponent() {
  const [data, setData] = useState({});
  const savedData = useRef(data); // 初始值设置为data
 
  useEffect(() => {
    // 当data变化时,更新savedData.current的值
    if (data !== savedData.current) {
      savedData.current = data;
    }
    // 副作用逻辑,使用savedData.current作为依赖项
    console.log('副作用运行', savedData.current);
  }, [data]); // data变化时,副作用会重新运行
 
  return (
    <div>
      {/* 组件内容 */}
    </div>
  );
}

3.1.8 useEffect 为啥不能直接使用异步async

异步函数(async函数)在JS中返回一个Promise对象,如果在useEffect中直接使用async函数,实际返回的是一个Promise.React中的useEffect期望返回清理函数或不返回。

useEffect(async () =>{
 // ....
})
//等价于:
useEffect(()=>{
  return new Promise(...)//违反React规则
})
3.1.8.1 useEffect正确使用async的方式
useEffect(() =>{
   let isActive = true  // 防止内存泄漏
   const loadData = async () =>{
      try {
        const res = await fetch('/api')
      } catch(err) {
        console.log('失败',err)
      }
   }
   loadData()
   
   // 清理函数
   return () =>{
     isActive = false
   }
},[/* 依赖项*/])
3.1.8.2 竞态条件,使用new AbortController()解决

3.2 useLayoutEffect(在浏览器重新绘制屏幕之前触发)

3.2.1使用场景

  • 需要同步测量DOM元素
  • 需要在视觉更新前进行DOM修改
  • 需要避免闪烁或布局抖动
  • 处理依赖于DOM布局的动画

3.3 useEffect与useLayoutEffect的区别

  • 执行时机
    • useEffect:在组件渲染到屏幕之后异步执行,不会阻塞浏览器绘制
    • useLayoutEffect:在所有DOM变更之后同步执行,会阻塞浏览器绘制,直到完成
  • 用途
    • useEffect:用于异步操作(数据获取、订阅、事件监听)和不影响布局的副作用
    • useLayoutEffect:用于需要同步执行的操作(读取DOM布局、同步DOM变更)和防止布局闪烁的副作用

image.png

3.4 useEffect与useLayoutEffect的执行顺序?

useLayoutEffect与useEffect都是在render后执行,并且先同步执行useLayoutEffect,后异步执行useEffect

执行顺序示例:

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

  useEffect(() => {
    console.log('useEffect 执行'); // 后执行
  });

  useLayoutEffect(() => {
    console.log('useLayoutEffect 执行'); // 先执行
  });

  return (
    <div onClick={() => setCount(c => c + 1)}>
      点击次数:{count}
    </div>
  );
}

四、 hooks之状态派生与保存[缓存数据]

4.1 useMemo:每次重新渲染的时候能够缓存计算的结果。

基础介绍:

const cachedValue = useMemo(calculateValue,dependencies)

  • ① calculateValue:第一个参数为一个函数,函数的返回值作为缓存值,如果dependencies没有发生变化,React将直接返回相同值。否则,将会再次调用calculateValue并返回最新结果,然后缓存该结果以便下次重复使用

  • ② dependencies: 第二个参数为一个数组,存放当前 useMemo 的依赖项,在函数组件下一次执行的时候,会对比 deps 依赖项里面的状态,是否有改变,如果有改变重新执行 create ,得到新的缓存值。

  • ③ cachedValue:返回值,执行 create 的返回值。如果 deps 中有依赖项改变,返回的重新执行 create 产生的值,否则取上一次缓存值。

示例:

import React, { useMemo, useState } from 'react';

export default function App() {
  const [arr, setArr] = useState(new Array(10).fill(0));
  const [expensiveValue, setExpensiveValue] = useState(null);

  // 使用 useMemo 来记忆计算结果
  const memoizedValue = useMemo(() => {
    console.log('memoizedValue 被调起使用');
    let result = 0;
    for (let i = 0; i < 1000000; i++) {
      result += Math.random();
    }
    return result;
  }, [arr.length]); // 仅当 arr.length 变化时重新计算

  // 模拟一个昂贵的计算
  const handleCalculate = () => {
    setExpensiveValue(memoizedValue);
  };

  return (
    <div>
      <p>Array.length: {arr.length}</p>
      <p>Value: {expensiveValue}</p>
      <button onClick={() => setArr(prev => [...prev, 0])}>数组长度+1</button>
      <button onClick={handleCalculate}>计算结果</button>
    </div>
  );
}

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

const cachedFn = useCallback(fn,dependencies)

使用场景

  • 优化子组件渲染次数

4.3 React.memo:memo 允许你的组件在 props 没有改变的情况下跳过重新渲染。

每次组件是否发生变化,与组件接收的参数props里的数据指向内存中映射地址有关,地址发生变化,组件会再次渲染,没有变化,则不渲染。

  • 传递不变的基础类型值,组件不会再次渲染
  • 传递会变化的基础类型值,组件会再次渲染
  • 传递引用数据类型值,组件会再次渲染
const MemoizedComponent = memo(SomeComponent, arePropsEqual?)

### 参数
1:

如何使用:

在父组件外部添加:

const MemoizedChildComponent = memo(ChildComponent)

import React, { useState, useEffect, memo } from 'react';

// 定义子组件的类型接口
interface PropsType {
  counter: number;
}

// 子组件
function ChildComponent({ counter }: PropsType) {
  console.log('子组件被渲染');
  return (
    <div>
      {counter}
    </div>
  );
}

// 使用 memo 包裹子组件
const MemoizedChildComponent = memo(ChildComponent,(oldProps,newProps)=>true);

// 父组件
export default () => {
  const [counter, setCounter] = useState<number>(0);

  // 处理计数器增加
  const handleCounter = () => {
    setCounter(prevState => prevState + 1);
  };
  console.log('父组件被渲染');
  return (
    <div>
      <h1>父组件</h1>
      <p>counter: {counter}</p>
      <button onClick={handleCounter}>点击+1</button>
      <MemoizedChildComponent counter={counter} />
    </div>
  );
};

4.4 React.memo、useMemo与useCallback的区别

  • React.memo是一个高阶组件,控制函数组件的重新渲染,将组件作为参数,函数返回值是一个新的组件

  • useMemo:是用来缓存计算结果,确保只有在依赖项发生变化时才会重新计算useMemo的实现方式是通过缓存计算结果,当依赖项发生变化时,重新计算结果并返回。【用来缓存DOM】

  • useCallback:是用于缓存函数,确保只有在依赖项发生变化时才会重新创建函数useCallback的实现方式是缓存函数本身,当依赖项发生变化时,重新创建函数并返回。【用来处理事件函数】

五、hooks之工具

5.1 useDebugValue

5.2 useId

六、 hooks之状态获取与传递

6.1 useRef

基础介绍:

useRef可以用来获取元素,缓存状态,接受一个状态initState作为初始值,返回一个ref对象cur,cur上有一个current属性就是ref对象需要获取的内容。

const cur = useRef(initState)

6.2 useContext

基础介绍: 使用useContext获取父组件传递的context值,这个当前值是最近的父级组件Provide设置的value的值,useContext参数一般是由createContext方式创建,

const contextValue = useContext(context)

useContext 接受一个参数,一般都是 context 对象,返回值为 context 对象内部保存的 value 值。

使用useContext时会出现的问题

  • 1、全局状态共享useContext允许在组件树中全局访问某个context值,当Context中的值发生变化时,所有使用该Context的组件都会重新渲染,即使组件实际上没有使用到发生变化的特定值。
  • 2、深层嵌套的组件树:在深层嵌套的组件树只能够使用useContext时,在顶层组件中没有频繁更新而底层组件频繁读取context值的情况下,可能顶层组件也会渲染。

为啥useContext会出现性能问题

Provider的value更新,所有useContext消费组件均重渲染,即使依赖数据未变。useContext订阅完整Context对象,React通过比较value的引用来判断变更。

解决上述问题

  • 拆分多个Context
  • 使用React.memo组件缓存
  • 使用useMemo优化Context值
  • 使用专门的状态管理库如Zustand等