说说对React Hooks的理解?
什么是hooks:
- Hooks 是 React 16.8 新增的特性。它可以在函数组件内使用 react 的特性,如生命周期、状态等。
为什么引入hooks?(解决了什么问题?)
- 组件之间的逻辑状态难以复用;
- 大型复杂的类组件很难拆分,每个生命周期函数中可能会包含着各种互不相关的逻辑在里面,导致难以维护;
- Class 语法的使用不友好,比如增加对类的学习成本以及类组件中的this学习成本;
- 函数式组件更加友好直观,但之前的函数组件没有状态和生命周期,只负责一些渲染工作。Hooks 使函数组件拥有了类组件的特性,例如组件内的状态、生命周期。
React Hooks的使用规则:
- 只在最顶层使用 Hook,不要在循环,条件或嵌套函数中调用 Hook。
- 在 React 的函数组件中调用 Hook,在自定义 Hook 中调用其他 Hook。
1. useState:
useState:为函数组件提供状态设置,返回一个数组,第一个参数是数据状态,第二个参数是修改数据状态的方法。
const [state, setState] = useState(0);
- useState修改数据的两种方式:直接传入一个值 或 传入一个函数。
1. 直接传入一个值:
setState(1); 或 setState(state + 1);
2. 传入一个函数:
setState((prevState) => prevState + 1); // prevState 是改变之前的 state 值,return 返回的值会作为新状态覆盖 state 值
- useState 异步回调获取不到最新值及解决方案:
- 通常情况下 setState 直接使用上述第一种方式传参即可,但在一些特殊情况下第一种方式会出现异常;在异步回调或闭包中,state引用的值是旧的,此时通过直接赋值的方式是获取不到实时状态的。
- 在 class 组件中我们可以使用 setState(options, callBack); 在 setState 的第二个参数回调函数中再次进行 setState,也不存在闭包作用域问题,但是 React Hook 中 useState 移除了 setState 的第二个参数,改为直接传入一个回调函数。
import React, { useState, useEffect } from 'react'
import { Button } from 'antd'
function useHooks(){
const [arr, setArr] = useState([0]);
useEffect(() => {
console.log(arr);
}, [arr]);
const handleClick = () => {
Promise.resolve().then(() => {
setArr([...arr, 1]); // 此时赋值前 arr 为:[0]
})
.then(() => {
setArr([...arr, 2]); // 此时赋值前 arr 为旧状态仍然为:[0]
});
}
const handleFnClick = () => {
Promise.resolve().then(() => {
setArr([...arr, 1]); // 此时赋值前 arr 为:[0]
})
.then(() => {
setArr((preState) => [...preState, 2]); // 这里必须改成回调函数传参方式,否则会读取旧状态,导致异常
});
}
return (
<>
<div>
<Button onClick={handleClick}>useState直接赋值</Button>
<Button onClick={handleFnClick}>useState传入函数</Button>
</div>
</>
);
}
export default useHooks;
useState直接赋值.png
useState传入函数.png
2. useEffect:
- useEffect可以让我们在函数组件中进行一些带有副作用的操作,回调函数在视图渲染后调用。useEffect是异步执行的,接收两个参数,第一个是一个回调函数,第二个是可选监听的指定的监听数据,它是一个数组。useEffect非常适合用于组件状态更改、数据获取、订阅设置或第三方插件的初始化。
- useEffect第一个参数接收一个回调函数
- 默认情况下,useEffect会在第一次渲染和更新之后都会执行,相当于在componentDidMount和componentDidUpdate两个生命周期函数中执行回调。
- 回调函数中可以返回一个清除函数,这是effect可选的清除机制,相当于类组件中componentWillUnmount生命周期函数,可做一些清除副作用的操作,例如取消定时器的监听等。
- 第二个参数是一个数组,
- 如果第二个参数是空数组,则相当于执行了componentDidMount,只会执行一次;
- 如果数组内指定了某个状态,则相当于对某个状态进行监听,当状态变化的时候,会执行一次回调。
- 如果第二个参数数组不存在时,每次渲染后都会触发执行回调函数。
import { useEffect,useRef } from 'react';
/*
useDidUpdateEffect 懒执行,不执行初始化
fn: 需要执行的函数
monitor: 依赖项,当依赖项发生变化时,执行fn
*/
const useDidUpdateEffect = (fn: () => void, monitor?: Array<any>) => {
const didMountRef = useRef(false);
useEffect(() => {
if (didMountRef.current) {
try{
fn();
}catch(err){
console.error('useDidUpdateEffect Error', err);
}
}else {
didMountRef.current = true;
}
}, monitor);
};
export default useDidUpdateEffect;
3. useLayoutEffect:
用法和差异:
- useLayoutEffect 的用法和 useEffect 相同,但是两者的执行时机是不相同的,useLayoutEffect 先于 useEffect 执行。
- useEffect 和 useLayoutEffect 都是发生在dom更新之后执行的,只不过useEffect 是发生在 dom更新完后并且视图已经重新绘制完成之后的异步执行。而useLayoutEffect 是在dom更新完后同步执行(立即执行),此时视图还没有重新绘制。
执行时机: useLayoutEffect 在 componentDidMount 、componentDidUpdate 之前执行;
用途: useLayoutEffect通常用于读取布局并同步重新渲染,以避免在屏幕上的闪烁或不一致。
缺点: useLayoutEffect 发生在绘制之前,如果计算量复杂的话,它可能会稍微阻塞浏览器的渲染过程,因此应该谨慎使用,以避免性能问题。
useMemo:
- useMemo 只有当依赖项发生改变的时候才会重新执行回调函数进行计算,否则返回缓存的值。useMemo一般作为一种优化手段。
- useMemo 接收两个参数,第一个参数是回调函数,函数需要返回一个值,这个值会被缓存。第二个参数是一个数组依赖项。只有在初始化的时候和依赖项发生变化的时候才会进行计算值。如果第二个参数不指定的话,那么组件每次重新渲染都会重新计算。
注: useMemo 在使用的时候是有成本开销的,只有大量计算的才有使用useMemo的必要,否则使用useMemo只会是反向优化。
useCallback:
- useCallback 用来返回函数引用,只要数组内的依赖项没有变化,每次组件重新渲染时,useCallback 返回的函数引用都是相同的。
useCallback 配合 React.memo 一起使用 可以起到优化子组件的性能,因为React.memo只会对props进行浅层比较,如果父组件每次渲染都会重新传一个新的函数引用,那就可能会导致子组件进行不必要的渲染。- useCallback 接收两个参数,第一个参数是回调函数,第二个参数是一个数组依赖项。只有在初始化的时候和依赖项发生变化的时候才会重新返回一个新的函数引用。如果第二个参数不指定的话,那么组件每次重新渲染都会重新返回一个新的函数。
- 注: useCallback 在使用的时候是有成本开销的,并不是所有的函数都需要使用useCallback来进行包裹缓存,只有配合 React.memo 一起使用来对子组件进行优化才有价值。否则使用useCallback只会是反向优化。
useContext:
- useContext 组件树中的任何位置读取和订阅上下文(Context)的值。
- Context 提供了一个无需为每层组件手动添加的 props,就能在组件树间进行数据传递的方法,其目的是实现跨层级数据共享。
- Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。
1. 创建上下文:
import React, { createContext, useContext, useState } from 'react';
const MyContext = createContext();
2. 使用 Provider 传递值:
import MyContext from './MyContext.ts'
function App() {
const [value, setValue] = useState('some data');
return (
<MyContext.Provider value={{ value, setValue }}>
<ChildComponent />
</MyContext.Provider>
);
}
3. 使用 useContext 读取值:
function ChildComponent() {
const contextValue = useContext(MyContext);
return (
<div>
<p>Context Value: {contextValue.value}</p>
<button onClick={() => contextValue.setValue('new data')}>Change Value</button>
</div>
);
}
useRef:
- useRef 的设计目标是提供一种访问 DOM 或保存组件实例的引用,也可以用来存储那些不需要触发渲染的数据,并且不参与到 React 的状态管理中;
- useRef的变化不会引发组件重新渲染,因为 useRef 的主要目的是跨渲染周期保持某些引用(如 DOM 元素或普通变量),而这些引用的变化与 React 的 UI 更新机制无关。
- useRef 的 .current 属性的修改不会触发 React 的重新渲染,它在组件的整个生命周期中是持续的,.current 会一直保存同一个值,除非组件完全卸载。
useImperativeHandle:
- useImperativeHandle 自定义由 ref 暴露出来的句柄,能够让父组件通过 ref 来获取子组件的特定方法或访问其属性。
useImperativeHandle(ref, createHandle, [deps]);
- ref:父组件传递给子组件的 ref(通过props传递的ref句柄)。
- createHandle:一个在组件渲染过程中调用的函数,用于创建需要暴露给父组件的属性和方法。返回值将会作为子组件的实例值。
- deps(可选):一个依赖数组,用于指定在 createHandle 中使用的依赖项。当这些依赖项发生变化时,useImperativeHandle 会重新运行以更新 ref 的 current 属性。
import { useImperativeHandle } frm 'react';
function MyInput({ ref }) {
useImperativeHandle(ref, () => {
return {
// ... 你的方法 ...
};
}, []);
// ...
}
forwardRef(React 19已经废除):
forwardRef 允许函数组件使用 ref 将 DOM 节点暴露给父组件或者将其传递给 useImperativeHandle作为句柄。
- forwardRef 接收一个函数,返回一个可以在 JSX 中渲染的 React 组件,返回的组件还能够接收 ref 属性。
- forwardRef 接收父组件传递过来的 props和传递的
ref属性。ref可以是一个对象或函数。如果父组件没有传递一个 ref,那么它将会是null。你应该将接收到的ref转发给另一个组件,或者将其传递给useImperativeHandle。
// 第一种
import React, { forwardRef } from "react"
type ChildProps = {}
type ChildRef = HTMLDivElement
const Child = forwardRef<ChildRef, ChildProps>((props, ref) => {
return (
<div ref={ref}></div>
);
})
export default Child;

