ReactHooks由浅入深:所有 hooks 的梳理、汇总与解析

2024年 4月 17日 67.7k 0

Vue 中的指令、React 中的 hooks 都是框架的核心知识点。但是对于很多同学来说,因为日常工作中开发的局限性,所以对这些 指令 或 hooks 认知的并不全面,一旦在面试的时候被问到不熟悉的 指令 或者 hooks 可能就会吃亏。

所以说,咱们今天就先来整理一下 React 中的 hooks,整理的过程会 由浅入深 同时配合一些代码进行说明。争取让哪怕不是很熟悉 react 的同学,也可以在本文章中有一定的收获。

一、React 基础普及

1、什么是 react 中的组件

在 React 16.8 之后,react 使用 函数 表示组件,称为 函数式组件。

例如以下代码,就是两个基础函数式组件(App 与 Greeting):

import React from 'react';

// 函数组件
function Greeting(props) {
  return Hello, {props.name}!;
}

// 使用函数组件
function App() {
  return (
    
      
      
      
    
  );
}

export default App;

2、什么是 react hooks

在 React 中 以 use 开头的函数 就被称之为 hooks。

React 默认提供一些 hooks。同样的,我们也可以自定义一些函数(以 use)开头,那么该函数就可以被称为是 hooks。

import { useState } from 'react';

// 最简单的自定义Hooks
function useMyCustomHook() {
  // 在这个例子中,我们只是返回一个固定的值
  const [value] = useState("Hello, World!");

  return value;
}

export default useMyCustomHook;

3、hooks 解决了什么问题?

如果没有Hooks,函数组件的功能相对有限,只能接受 Props、渲染UI,以及响应来自父组件的事件。

因此,Hooks的出现主要是为了解决是哪个问题:

  • 强化函数式组件:让函数组件具备类组件(16.8之前)的功能,可以有自己的状态、处理副作用、获取ref,以及进行数据缓存。
  • 提高复用性: hooks本质上是以 use 开头的函数,通过函数封装逻辑,解决逻辑复用的问题。
  • 函数式编程:react 推荐函数式编程,摒弃面向对象编程。

二、React Hooks 划分

React Hooks 根据性能可以划分为 5 大类:

1.1 状态处理:useState

useState是React提供的一个Hook,它让函数组件也能像类组件一样拥有状态。通过useState,你可以让组件在内部管理一些数据,并在数据更新时重新渲染视图。

在使用useState时,你会得到一个包含两个值的数组:

  • state:当前状态的值,它用于提供给UI,作为渲染视图的数据源。
  • dispatch:一个函数,用于改变state的值,从而触发函数组件的重新渲染。
  • useState的基础用法如下:

    const DemoState = (props) => {
       // number是当前state的值,setNumber是用于更新state的函数
       let [number, setNumber] = useState(0) // 0为初始值
       return (
           
               {number}
                {
                 setNumber(number + 1)
                 console.log(number) // 这里的number是不能够即时改变的
               }}>增加
           
       )
    }

    在使用useState时需要注意:

  • 在同一个函数组件的执行过程中,state的值是固定不变的。比如,如果你在一个定时器中改变state的值,组件不会重新渲染。
  • 如果两次dispatch传入相同的state值,那么组件就不会更新,因为React会认为状态没有改变。
  • 当你在当前执行上下文中触发dispatch时,无法立即获取到最新的state值,只有在下一次组件重新渲染时才能获取到。
  • 1.2 状态处理:useReducer

    useReducer是React Hooks提供的一个功能,类似于Redux的状态管理工具。

    在使用useReducer时,你会得到一个包含两个值的数组:

  • state:state是更新后的状态值
  • dispatch:dispatch是用于派发更新的函数,与useState中的dispatch函数类似
  • 基础用法如下:

    const DemoUseReducer = () => {
        // number为更新后的state值, dispatchNumbner为当前的派发函数
        const [number, dispatchNumber] = useReducer((state, action) => {
            const { payload, name } = action;
            // 根据不同的action类型来更新state
            switch (name) {
                case 'add':
                    return state + 1;
                case 'sub':
                    return state - 1;
                case 'reset':
                    return payload;
                default:
                    return state;
            }
        }, 0);
    
        return (
            
                当前值:{number}
                {/* 派发更新 */}
                 dispatchNumber({ name: 'add' })}>增加
                 dispatchNumber({ name: 'sub' })}>减少
                 dispatchNumber({ name: 'reset', payload: 666 })}>赋值
                {/* 把dispatch和state传递给子组件 */}
                
            
        );
    };

    在useReducer中,你需要传入一个reducer函数,这个函数接受当前的state和一个action作为参数,并返回新的state。如果新的state和之前的state指向的是同一个内存地址,那么组件就不会更新。

    1.3 状态处理:useSyncExternalStore

    useSyncExternalStore的出现与React版本18中更新模式下外部数据的撕裂(tearing)密切相关。它允许React组件在并发模式下安全有效地读取外部数据源,并在组件渲染过程中检测数据的变化,以及在数据源发生变化时调度更新,以确保结果的一致性。

    基础介绍如下:

    useSyncExternalStore(
        subscribe,
        getSnapshot,
        getServerSnapshot
    )
  • subscribe是一个订阅函数,当数据发生变化时会触发该函数。useSyncExternalStore会使用带有记忆功能的getSnapshot来判断数据是否发生变化,如果有变化,则强制更新数据。
  • getSnapshot可以看作是带有记忆功能的选择器。当数据源变化时,通过getSnapshot生成新的状态值,该状态值可作为组件的数据源使用。getSnapshot能够检查订阅的值是否发生变化,一旦发生变化,则触发更新。
  • getServerSnapshot用于hydration模式下的getSnapshot。
  • 基础用法示例如下:

    import { combineReducers, createStore } from 'redux';
    
    /* number Reducer */
    function numberReducer(state = 1, action) {
        switch (action.type) {
            case 'ADD':
                return state + 1;
            case 'DEL':
                return state - 1;
            default:
                return state;
        }
    }
    
    /* 注册reducer */
    const rootReducer = combineReducers({ number: numberReducer });
    /* 创建 store */
    const store = createStore(rootReducer, { number: 1 });
    
    function Index() {
        /* 订阅外部数据源 */
        const state = useSyncExternalStore(store.subscribe, () => store.getState().number);
        console.log(state);
        return (
            
                {state}
                 store.dispatch({ type: 'ADD' })}>点击
            
        );
    }

    当点击按钮时,会触发reducer,然后会触发store.subscribe订阅函数,执行getSnapshot得到新的number,判断number是否发生变化,如果有变化,则触发更新。

    1.4 状态处理:useTransition

    在React v18中,引入了一种新概念叫做过渡任务。这些任务与立即更新任务相对应,通常指的是一些不需要立即响应的更新,比如页面从一个状态过渡到另一个状态。

    举个例子,当用户点击tab从tab1切换到tab2时,会产生两个更新任务:

  • 第一个任务是hover状态从tab1变成tab2。
  • 第二个任务是内容区域从tab1内容变换到tab2内容。
  • 这两个任务中,用户通常希望hover状态的响应更迅速,而内容的响应可能需要更长时间,比如请求数据等操作。因此,第一个任务可以视为立即执行任务,而第二个任务则可以视为过渡任务。

    useTransition的基础介绍如下:

    import { useTransition } from 'react';
    
    /* 使用 */
    const [isPending, startTransition] = useTransition();

    useTransition会返回一个数组,其中包含两个值:

  • 第一个值是一个标志,表示是否处于过渡状态。
  • 第二个值是一个函数,类似于上述的startTransition,用于将更新任务转换为过渡任务。
  • 在基础用法中,除了切换tab的场景外,还有很多其他场景适合产生过渡任务,比如实时搜索并展示数据。这种情况下,有两个优先级的任务:第一个是受控表单的实时响应,第二个是输入内容改变后数据展示的变化。

    下面是一个基本使用useTransition的示例:

    const mockList1 = new Array(10000).fill('tab1').map((item,index)=>item+'--'+index )
    const mockList2 = new Array(10000).fill('tab2').map((item,index)=>item+'--'+index )
    const mockList3 = new Array(10000).fill('tab3').map((item,index)=>item+'--'+index )
    
    const tab = {
      tab1: mockList1,
      tab2: mockList2,
      tab3: mockList3
    }
    
    export default function Index(){
      const [ active, setActive ] = React.useState('tab1') //立即响应的任务,立即更新任务
      const [ renderData, setRenderData ] = React.useState(tab[active]) //不需要立即响应的任务,过渡任务
      const [ isPending,startTransition  ] = React.useTransition() 
      const handleChangeTab = (activeItem) => {
         setActive(activeItem) //立即更新
         startTransition(()=>{ //startTransition里面的任务优先级低
           setRenderData(tab[activeItem])
         })
      }
      return 
        
           { Object.keys(tab).map((item)=> handleChangeTab(item)} >{ item } ) }
        
        
      { isPending && loading... } { renderData.map(item=>
    • {item}
    • ) }
    }

    以上示例中,当切换tab时,会产生两个优先级任务:第一个任务是setActive控制tab的active状态的改变,第二个任务是setRenderData控制渲染的长列表数据(在实际场景中,这可能是一些数据量大的可视化图表)。

    1.5 状态处理:useDeferredValue

    在React 18中,引入了useDeferredValue,它可以让状态的更新滞后于派生。useDeferredValue的实现效果类似于transition,在紧急任务执行后,再得到新的状态,这个新的状态就称之为DeferredValue。

    useDeferredValue基础介绍:

    useDeferredValue和前面提到的useTransition有什么异同呢?

    相同点: useDeferredValue和useTransition本质上都是标记为过渡更新任务。

    不同点: useTransition将内部的更新任务转换为过渡任务transition,而useDeferredValue则是通过过渡任务得到新的值,这个值作为延迟状态。一个是处理一段逻辑,另一个是生成一个新的状态。

    useDeferredValue接受一个参数value,通常是可变的state,返回一个延迟状态deferredValue。

    const deferredValue = React.useDeferredValue(value)

    useDeferredValue基础用法:

    下面将上面的例子改用useDeferredValue来实现。

    export default function Index(){
      const [ active, setActive ] = React.useState('tab1') //需要立即响应的任务,立即更新任务
      const deferredActive = React.useDeferredValue(active) // 将状态延迟更新,类似于过渡任务
      const handleChangeTab = (activeItem) => {
         setActive(activeItem) // 立即更新
      }
      const renderData = tab[deferredActive] // 使用延迟状态
      return 
        
           { Object.keys(tab).map((item)=> handleChangeTab(item)} >{ item } ) }
        
        
      { renderData.map(item=>
    • {item}
    • ) }
    }

    上述代码中,active是正常改变的状态,deferredActive是延迟的active状态。我们使用正常状态来改变tab的active状态,而使用延迟状态来更新视图,从而提升了用户体验。

    2.1 副作用执行:useEffect

    React hooks提供了API,用于弥补函数组件没有生命周期的不足。主要利用了hooks中的useEffect、useLayoutEffect和useInsertionEffect。其中,最常用的是useEffect。现在我们来看一下useEffect的使用。

    useEffect基础介绍:

    useEffect(() => {
        return cleanup;
    }, dependencies);

    useEffect的第一个参数是一个回调函数,返回一个清理函数cleanup。cleanup函数会在下一次回调函数执行之前调用,用于清除上一次回调函数产生的副作用。

    第二个参数是一个依赖项数组,里面可以包含多个依赖项。当依赖项发生变化时,会执行上一次callback返回的cleanup函数,并执行新的effect回调函数。

    对于useEffect的执行,React采用了异步调用的处理逻辑。对于每个effect的回调函数,React会将其放入任务队列中,类似于setTimeout回调函数的方式,等待主线程任务完成、DOM更新、JS执行完成以及视图绘制完成后才执行。因此,effect回调函数不会阻塞浏览器的视图绘制。

    useEffect基础用法:

    /* 模拟数据交互 */
    function getUserInfo(a){
        return new Promise((resolve)=>{
            setTimeout(()=>{ 
               resolve({
                   name:a,
                   age:16,
               }) 
            },500)
        })
    }
    
    const Demo = ({ a }) => {
        const [userMessage, setUserMessage] = useState({});
        const div = useRef();
        const [number, setNumber] = useState(0);
    
        /* 模拟事件监听处理函数 */
        const handleResize = () => {};
    
        /* useEffect使用 */
        useEffect(() => {
           /* 请求数据 */
           getUserInfo(a).then(res => {
               setUserMessage(res);
           });
    
           /* 定时器 延时器等 */
           const timer = setInterval(() => console.log(666), 1000);
    
           /* 操作dom */
           console.log(div.current); /* div */
    
           /* 事件监听等 */
           window.addEventListener('resize', handleResize);
    
           /* 此函数用于清除副作用 */
           return function() {
               clearInterval(timer);
               window.removeEventListener('resize', handleResize);
           };
        /* 只有当props->a和state->number改变的时候, useEffect副作用函数重新执行,如果此时数组为空[],证明函数只有在初始化的时候执行一次,相当于componentDidMount */
        }, [a, number]);
    
        return (
            
                {userMessage.name}
                {userMessage.age}
                 setNumber(1)}>{number}
            
        );
    };

    上述代码中,在useEffect中做了以下功能:

  • 请求数据。
  • 设置定时器、延时器等。
  • 操作DOM,在React Native中可以通过ref获取元素位置信息等内容。
  • 注册事件监听器,在React Native中可以注册NativeEventEmitter。
  • 清除定时器、延时器、解绑事件监听器等。
  • 2.2 副作用执行:useLayoutEffect

    useLayoutEffect基础介绍:

    useLayoutEffect和useEffect的不同之处在于它采用了同步执行的方式。那么它和useEffect有什么区别呢?

    ① 首先,useLayoutEffect在DOM更新之后、浏览器绘制之前执行。这使得我们可以方便地修改DOM、获取DOM信息,从而避免了不必要的浏览器回流和重绘。相比之下,如果将DOM布局修改放在useEffect中,那么useEffect的执行是在浏览器绘制视图之后进行的,接着再去修改DOM,可能会导致浏览器进行额外的回流和重绘。由于两次绘制,可能会导致视图上出现闪现或突兀的效果。

    ② useLayoutEffect回调函数中的代码执行会阻塞浏览器的绘制。

    useLayoutEffect基础用法:

    const DemoUseLayoutEffect = () => {
        const target = useRef();
    
        useLayoutEffect(() => {
            /* 在DOM绘制之前,移动DOM到指定位置 */
            const { x, y } = getPositon(); // 获取要移动的x,y坐标
            animate(target.current, { x, y });
        }, []);
    
        return (
            
                
            
        );
    };

    2.3 副作用执行:useInsertionEffect

    useInsertionEffect基础介绍:

    useInsertionEffect是React v18新增的hooks之一,其用法与useEffect和useLayoutEffect相似。那么这个hooks用于什么呢?

    在介绍useInsertionEffect用途之前,先来看一下useInsertionEffect的执行时机。

    React.useEffect(() => {
        console.log('useEffect 执行');
    }, []);
    
    React.useLayoutEffect(() => {
        console.log('useLayoutEffect 执行');
    }, []);
    
    React.useInsertionEffect(() => {
        console.log('useInsertionEffect 执行');
    }, []);

    打印结果为:useInsertionEffect执行 -> useLayoutEffect执行 -> useEffect执行。

    可以看到,useInsertionEffect的执行时机要比useLayoutEffect提前。在useLayoutEffect执行时,DOM已经更新了,但是在useInsertionEffect执行时,DOM还没有更新。useInsertionEffect主要是解决CSS-in-JS在渲染中注入样式的性能问题。这个hooks主要适用于这个场景,在其他场景下React不建议使用这个hooks。

    useInsertionEffect模拟使用:

    export default function Index() {
        React.useInsertionEffect(() => {
            /* 动态创建style标签插入到head中 */
            const style = document.createElement('style');
            style.innerHTML = `
                .css-in-js {
                    color: red;
                    font-size: 20px;
                }
            `;
            document.head.appendChild(style);
        }, []);
    
        return hello, useInsertionEffect;
    }

    上述代码模拟了useInsertionEffect的使用。

    3.1 状态传递:useContext

    useContext基础介绍

    可以使用useContext来获取父级组件传递过来的context值,这个值是最近的父级组件Provider设置的value值。useContext的参数通常是由createContext方式创建的context对象,也可以是父级上下文context传递的(参数为context)。useContext可以代替context.Consumer来获取Provider中保存的value值。

    const contextValue = useContext(context);

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

    useContext基础用法:

    /* 用useContext方式 */
    const DemoContext = () => {
        const value = useContext(Context);
        /* my name is alien */
        return  my name is {value.name};
    }
    
    /* 用Context.Consumer方式 */
    const DemoContext1 = () => {
        return (
            
                {/* my name is alien */}
                {value =>  my name is {value.name}}
            
        );
    }
    
    export default () => {
        return (
            
                
                    
                    
                
            
        );
    }

    3.2 状态传递:useRef

    useRef基础介绍:

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

    const cur = React.useRef(initState);
    console.log(cur.current);

    useRef基础用法:

    获取DOM元素: 在React中,可以利用useRef来获取DOM元素。在React Native中虽然没有DOM元素,但是同样可以利用useRef来获取组件的节点信息(Fiber信息)。

    const DemoUseRef = () => {
        const dom = useRef(null);
        const handleSubmit = () => {
            console.log(dom.current); // 表单组件 DOM节点
        }
        return (
            
                {/* ref标记当前DOM节点 */}
                表单组件
                提交
            
        );
    }

    保存状态: 可以利用useRef返回的ref对象来保存状态,只要当前组件不被销毁,状态就会一直存在。

    const status = useRef(false);
    /* 改变状态 */
    const handleChangeStatus = () => {
        status.current = true;
    }

    3.3 状态传递:useImperativeHandle

    useImperativeHandle基础介绍:

    useImperativeHandle配合forwardRef可以自定义向父组件暴露的实例值。对于函数组件,如果我们想让父组件能够获取子组件的实例,就可以使用useImperativeHandle和forwardRef来实现。

    useImperativeHandle接受三个参数:

  • 第一个参数ref:接受forwardRef传递过来的ref。
  • 第二个参数createHandle:处理函数,返回值作为暴露给父组件的ref对象。
  • 第三个参数deps:依赖项deps,当依赖项改变时形成新的ref对象。
  • useImperativeHandle基础用法:

    我们通过一个示例来说明,使用useImperativeHandle使得父组件能够控制子组件中的input自动聚焦并设置值。

    function Son(props, ref) {
        const inputRef = useRef(null);
        const [inputValue, setInputValue] = useState('');
    
        useImperativeHandle(ref, () => {
            const handleRefs = {
                onFocus() {
                    inputRef.current.focus();
                },
                onChangeValue(value) {
                    setInputValue(value);
                }
            };
            return handleRefs;
        }, []);
    
        return (
            
                
            
        );
    }
    
    const ForwardSon = forwardRef(Son);
    
    class Index extends React.Component {
        inputRef = null;
    
        handleClick() {
            const { onFocus, onChangeValue } = this.inputRef;
            onFocus();
            onChangeValue('let us learn React!');
        }
    
        render() {
            return (
                
                     (this.inputRef = node)} />
                    操控子组件
                
            );
        }
    }

    4.1 性能优化:useMemo

    useMemo基础介绍:

    useMemo 可以在函数组件的渲染过程中同步执行一个函数逻辑,并将其返回值作为一个新的状态进行缓存。这个 hooks 的作用在于优化性能,避免不必要的重复计算或渲染。

    基本语法:

    const cachedValue = useMemo(create, deps)
    • create: 第一个参数是一个函数,函数的返回值将作为缓存值。
    • deps: 第二个参数是一个数组,包含当前 useMemo 的依赖项。当这些依赖项发生变化时,会重新执行 create 函数以获取新的缓存值。
    • cachedValue: 返回值,为 create 函数的返回值或上一次的缓存值。

    基本用法:

    1. 派生新状态:

    function Scope() {
        const keeper = useKeep()
        const { cacheDispatch, cacheList, hasAliveStatus } = keeper
       
        const contextValue = useMemo(() => {
            return {
                cacheDispatch: cacheDispatch.bind(keeper),
                hasAliveStatus: hasAliveStatus.bind(keeper),
                cacheDestory: (payload) => cacheDispatch.call(keeper, { type: ACTION_DESTORY, payload })
            }
          
        }, [keeper])
        return (
            
            
        )
    }

    在上面的示例中,通过 useMemo 派生出一个新的状态 contextValue,只有 keeper 发生变化时,才会重新生成 contextValue。

    2. 缓存计算结果:

    function Scope(){
        const style = useMemo(()=>{
          let computedStyle = {}
          // 大量的计算
          return computedStyle
        },[])
        return 
    }

    在这个例子中,通过 useMemo 缓存了一个计算结果 style,只有当依赖项发生变化时,才会重新计算 style。

    3. 缓存组件,减少子组件重渲染次数:

    function Scope ({ children }){
       const renderChild = useMemo(()=>{ children()  },[ children ])
       return { renderChild } 
    }

    通过 useMemo 缓存了子组件的渲染结果 renderChild,只有当 children 发生变化时,才会重新执行子组件的渲染。

    4.2 性能优化:useCallback

    useCallback基础介绍:

    useCallback 和 useMemo 接收的参数类似,都是在其依赖项发生变化后才执行,都返回缓存的值。但是它们的区别在于,useMemo 返回的是函数运行的结果,而 useCallback 返回的是一个经过处理的函数本身。它主要用于优化性能,避免不必要的函数重新创建,特别是在向子组件传递函数时,避免因为函数重新创建而导致子组件不必要的重新渲染。

    基本用法:

    const cachedCallback = useCallback(callbackFunction, deps)
    • callbackFunction: 第一个参数是一个函数,需要进行缓存的函数。
    • deps: 第二个参数是一个数组,包含当前 useCallback 的依赖项。当这些依赖项发生变化时,会重新创建新的缓存函数。
    • cachedCallback: 返回值,为经过处理的缓存函数。

    基础用法示例:

    const DemoChildren = React.memo((props)=>{
        console.log('子组件更新')
        useEffect(()=>{
            props.getInfo('子组件')
        },[])
        return 子组件
    })
    
    const DemoUseCallback=({ id })=>{
        const [number, setNumber] = useState(1)
        
        const getInfo  = useCallback((sonName)=>{
            console.log(sonName)
        }, [id]) // 只有当 id 发生变化时,才会重新创建 getInfo 函数
        
        return (
            
                setNumber(number+1) }>增加
                
            
        )
    }

    在上面的示例中,getInfo 函数通过 useCallback 进行了缓存,只有当 id 发生变化时,才会重新创建 getInfo 函数。这样可以避免因为函数重新创建而导致子组件不必要的重新渲染。

    5.1 工具类:useId

    在组件的顶层调用 useId 生成唯一 ID:

    import { useId } from 'react';
    
    function PasswordField() {
      const passwordHintId = useId();
      // ...

    5.2 工具类:useDebugValue

    在你的 自定义 Hook 的顶层调用 useDebugValue,以显示可读的调试值:

    import { useDebugValue } from 'react';
    
    function useOnlineStatus() {
      // ...
      useDebugValue(isOnline ? 'Online' : 'Offline');
      // ...
    }

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论