Vue 中的指令、React 中的 hooks 都是框架的核心知识点。但是对于很多同学来说,因为日常工作中开发的局限性,所以对这些 指令 或 hooks 认知的并不全面,一旦在面试的时候被问到不熟悉的 指令 或者 hooks 可能就会吃亏。
所以说,咱们今天就先来整理一下 React 中的 hooks,整理的过程会 由浅入深 同时配合一些代码进行说明。争取让哪怕不是很熟悉 react 的同学,也可以在本文章中有一定的收获。
在 React 16.8 之后,react 使用 函数 表示组件,称为 函数式组件。
例如以下代码,就是两个基础函数式组件(App 与 Greeting):
import React from 'react';// 函数组件function Greeting(props) { return <h1>Hello, {props.name}!</h1>;}// 使用函数组件function App() { return ( <div> <Greeting name="Alice" /> <Greeting name="Bob" /> <Greeting name="Charlie" /> </div> );}export default App;
在 React 中 以 use 开头的函数 就被称之为 hooks。
React 默认提供一些 hooks。同样的,我们也可以自定义一些函数(以 use)开头,那么该函数就可以被称为是 hooks。
import { useState } from 'react';// 最简单的自定义Hooksfunction useMyCustomHook() { // 在这个例子中,我们只是返回一个固定的值 const [value] = useState("Hello, World!"); return value;}export default useMyCustomHook;
如果没有Hooks,函数组件的功能相对有限,只能接受 Props、渲染UI,以及响应来自父组件的事件。
因此,Hooks的出现主要是为了解决是哪个问题:
React Hooks 根据性能可以划分为 5 大类:
useState是React提供的一个Hook,它让函数组件也能像类组件一样拥有状态。通过useState,你可以让组件在内部管理一些数据,并在数据更新时重新渲染视图。
在使用useState时,你会得到一个包含两个值的数组:
useState的基础用法如下:
const DemoState = (props) => { // number是当前state的值,setNumber是用于更新state的函数 let [number, setNumber] = useState(0) // 0为初始值 return ( <div> <span>{number}</span> <button onClick={() => { setNumber(number + 1) console.log(number) // 这里的number是不能够即时改变的 }}>增加</button> </div> )}
在使用useState时需要注意:
useReducer是React Hooks提供的一个功能,类似于Redux的状态管理工具。
在使用useReducer时,你会得到一个包含两个值的数组:
基础用法如下:
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 ( <div> 当前值:{number} {/* 派发更新 */} <button onClick={() => dispatchNumber({ name: 'add' })}>增加</button> <button onClick={() => dispatchNumber({ name: 'sub' })}>减少</button> <button onClick={() => dispatchNumber({ name: 'reset', payload: 666 })}>赋值</button> {/* 把dispatch和state传递给子组件 */} <MyChildren dispatch={dispatchNumber} state={{ number }} /> </div> );};
在useReducer中,你需要传入一个reducer函数,这个函数接受当前的state和一个action作为参数,并返回新的state。如果新的state和之前的state指向的是同一个内存地址,那么组件就不会更新。
useSyncExternalStore的出现与React版本18中更新模式下外部数据的撕裂(tearing)密切相关。它允许React组件在并发模式下安全有效地读取外部数据源,并在组件渲染过程中检测数据的变化,以及在数据源发生变化时调度更新,以确保结果的一致性。
基础介绍如下:
useSyncExternalStore( subscribe, getSnapshot, getServerSnapshot)
基础用法示例如下:
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 ( <div> {state} <button onClick={() => store.dispatch({ type: 'ADD' })}>点击</button> </div> );}
当点击按钮时,会触发reducer,然后会触发store.subscribe订阅函数,执行getSnapshot得到新的number,判断number是否发生变化,如果有变化,则触发更新。
在React v18中,引入了一种新概念叫做过渡任务。这些任务与立即更新任务相对应,通常指的是一些不需要立即响应的更新,比如页面从一个状态过渡到另一个状态。
举个例子,当用户点击tab从tab1切换到tab2时,会产生两个更新任务:
这两个任务中,用户通常希望hover状态的响应更迅速,而内容的响应可能需要更长时间,比如请求数据等操作。因此,第一个任务可以视为立即执行任务,而第二个任务则可以视为过渡任务。
import { useTransition } from 'react';/* 使用 */const [isPending, startTransition] = useTransition();
useTransition会返回一个数组,其中包含两个值:
在基础用法中,除了切换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 <div> <div className='tab' > { Object.keys(tab).map((item)=> <span className={ active === item && 'active' } onClick={()=>handleChangeTab(item)} >{ item }</span> ) } </div> <ul className='content' > { isPending && <div> loading... </div> } { renderData.map(item=> <li key={item} >{item}</li>) } </ul> </div>}
以上示例中,当切换tab时,会产生两个优先级任务:第一个任务是setActive控制tab的active状态的改变,第二个任务是setRenderData控制渲染的长列表数据(在实际场景中,这可能是一些数据量大的可视化图表)。
在React 18中,引入了useDeferredValue,它可以让状态的更新滞后于派生。useDeferredValue的实现效果类似于transition,在紧急任务执行后,再得到新的状态,这个新的状态就称之为DeferredValue。
useDeferredValue和前面提到的useTransition有什么异同呢?
相同点: useDeferredValue和useTransition本质上都是标记为过渡更新任务。
不同点: useTransition将内部的更新任务转换为过渡任务transition,而useDeferredValue则是通过过渡任务得到新的值,这个值作为延迟状态。一个是处理一段逻辑,另一个是生成一个新的状态。
useDeferredValue接受一个参数value,通常是可变的state,返回一个延迟状态deferredValue。
const deferredValue = React.useDeferredValue(value)
下面将上面的例子改用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 <div> <div className='tab' > { Object.keys(tab).map((item)=> <span className={ active === item && 'active' } onClick={()=>handleChangeTab(item)} >{ item }</span> ) } </div> <ul className='content' > { renderData.map(item=> <li key={item} >{item}</li>) } </ul> </div>}
上述代码中,active是正常改变的状态,deferredActive是延迟的active状态。我们使用正常状态来改变tab的active状态,而使用延迟状态来更新视图,从而提升了用户体验。
React hooks提供了API,用于弥补函数组件没有生命周期的不足。主要利用了hooks中的useEffect、useLayoutEffect和useInsertionEffect。其中,最常用的是useEffect。现在我们来看一下useEffect的使用。
useEffect(() => { return cleanup;}, dependencies);
useEffect的第一个参数是一个回调函数,返回一个清理函数cleanup。cleanup函数会在下一次回调函数执行之前调用,用于清除上一次回调函数产生的副作用。
第二个参数是一个依赖项数组,里面可以包含多个依赖项。当依赖项发生变化时,会执行上一次callback返回的cleanup函数,并执行新的effect回调函数。
对于useEffect的执行,React采用了异步调用的处理逻辑。对于每个effect的回调函数,React会将其放入任务队列中,类似于setTimeout回调函数的方式,等待主线程任务完成、DOM更新、JS执行完成以及视图绘制完成后才执行。因此,effect回调函数不会阻塞浏览器的视图绘制。
/* 模拟数据交互 */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 ( <div ref={div}> <span>{userMessage.name}</span> <span>{userMessage.age}</span> <div onClick={() => setNumber(1)}>{number}</div> </div> );};
上述代码中,在useEffect中做了以下功能:
useLayoutEffect和useEffect的不同之处在于它采用了同步执行的方式。那么它和useEffect有什么区别呢?
① 首先,useLayoutEffect在DOM更新之后、浏览器绘制之前执行。这使得我们可以方便地修改DOM、获取DOM信息,从而避免了不必要的浏览器回流和重绘。相比之下,如果将DOM布局修改放在useEffect中,那么useEffect的执行是在浏览器绘制视图之后进行的,接着再去修改DOM,可能会导致浏览器进行额外的回流和重绘。由于两次绘制,可能会导致视图上出现闪现或突兀的效果。
② useLayoutEffect回调函数中的代码执行会阻塞浏览器的绘制。
const DemoUseLayoutEffect = () => { const target = useRef(); useLayoutEffect(() => { /* 在DOM绘制之前,移动DOM到指定位置 */ const { x, y } = getPositon(); // 获取要移动的x,y坐标 animate(target.current, { x, y }); }, []); return ( <div> <span ref={target} className="animate"></span> </div> );};
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。
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 <div className="css-in-js">hello, useInsertionEffect</div>;}
上述代码模拟了useInsertionEffect的使用。
可以使用useContext来获取父级组件传递过来的context值,这个值是最近的父级组件Provider设置的value值。useContext的参数通常是由createContext方式创建的context对象,也可以是父级上下文context传递的(参数为context)。useContext可以代替context.Consumer来获取Provider中保存的value值。
const contextValue = useContext(context);
useContext接受一个参数,一般是context对象,返回值是context对象内部保存的value值。
/* 用useContext方式 */const DemoContext = () => { const value = useContext(Context); /* my name is alien */ return <div> my name is {value.name}</div>;}/* 用Context.Consumer方式 */const DemoContext1 = () => { return ( <Context.Consumer> {/* my name is alien */} {value => <div> my name is {value.name}</div>} </Context.Consumer> );}export default () => { return ( <div> <Context.Provider value={{ name: 'alien', age: 18 }}> <DemoContext /> <DemoContext1 /> </Context.Provider> </div> );}
useRef可以用来获取元素,缓存状态。它接受一个初始状态initState作为初始值,并返回一个ref对象cur。cur对象上有一个current属性,该属性就是ref对象需要获取的内容。
const cur = React.useRef(initState);console.log(cur.current);
获取DOM元素: 在React中,可以利用useRef来获取DOM元素。在React Native中虽然没有DOM元素,但是同样可以利用useRef来获取组件的节点信息(Fiber信息)。
const DemoUseRef = () => { const dom = useRef(null); const handleSubmit = () => { console.log(dom.current); // <div>表单组件</div> DOM节点 } return ( <div> {/* ref标记当前DOM节点 */} <div ref={dom}>表单组件</div> <button onClick={handleSubmit}>提交</button> </div> );}
保存状态: 可以利用useRef返回的ref对象来保存状态,只要当前组件不被销毁,状态就会一直存在。
const status = useRef(false);/* 改变状态 */const handleChangeStatus = () => { status.current = true;}
useImperativeHandle配合forwardRef可以自定义向父组件暴露的实例值。对于函数组件,如果我们想让父组件能够获取子组件的实例,就可以使用useImperativeHandle和forwardRef来实现。
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 ( <div> <input placeholder="请输入内容" ref={inputRef} value={inputValue} /> </div> );}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 ( <div style={{ marginTop: '50px' }}> <ForwardSon ref={(node) => (this.inputRef = node)} /> <button onClick={this.handleClick.bind(this)}>操控子组件</button> </div> ); }}
useMemo 可以在函数组件的渲染过程中同步执行一个函数逻辑,并将其返回值作为一个新的状态进行缓存。这个 hooks 的作用在于优化性能,避免不必要的重复计算或渲染。
const cachedValue = useMemo(create, deps)
基本用法:
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 ( <KeepaliveContext.Provider value={contextValue}> </KeepaliveContext.Provider> )}
在上面的示例中,通过 useMemo 派生出一个新的状态 contextValue,只有 keeper 发生变化时,才会重新生成 contextValue。
function Scope(){ const style = useMemo(()=>{ let computedStyle = {} // 大量的计算 return computedStyle },[]) return <div style={style} ></div>}
在这个例子中,通过 useMemo 缓存了一个计算结果 style,只有当依赖项发生变化时,才会重新计算 style。
function Scope ({ children }){ const renderChild = useMemo(()=>{ children() },[ children ]) return <div>{ renderChild } </div>}
通过 useMemo 缓存了子组件的渲染结果 renderChild,只有当 children 发生变化时,才会重新执行子组件的渲染。
useCallback 和 useMemo 接收的参数类似,都是在其依赖项发生变化后才执行,都返回缓存的值。但是它们的区别在于,useMemo 返回的是函数运行的结果,而 useCallback 返回的是一个经过处理的函数本身。它主要用于优化性能,避免不必要的函数重新创建,特别是在向子组件传递函数时,避免因为函数重新创建而导致子组件不必要的重新渲染。
const cachedCallback = useCallback(callbackFunction, deps)
const DemoChildren = React.memo((props)=>{ console.log('子组件更新') useEffect(()=>{ props.getInfo('子组件') },[]) return <div>子组件</div>})const DemoUseCallback=({ id })=>{ const [number, setNumber] = useState(1) const getInfo = useCallback((sonName)=>{ console.log(sonName) }, [id]) // 只有当 id 发生变化时,才会重新创建 getInfo 函数 return ( <div> <button onClick={ ()=>setNumber(number+1) }>增加</button> <DemoChildren getInfo={getInfo} /> </div> )}
在上面的示例中,getInfo 函数通过 useCallback 进行了缓存,只有当 id 发生变化时,才会重新创建 getInfo 函数。这样可以避免因为函数重新创建而导致子组件不必要的重新渲染。
在组件的顶层调用 useId 生成唯一 ID:
import { useId } from 'react';function PasswordField() { const passwordHintId = useId(); // ...
在你的 自定义 Hook 的顶层调用 useDebugValue,以显示可读的调试值:
import { useDebugValue } from 'react';function useOnlineStatus() { // ... useDebugValue(isOnline ? 'Online' : 'Offline'); // ...}
本文链接://www.dmpip.com//www.dmpip.com/showinfo-26-84029-0.htmlReactHooks由浅入深:所有 hooks 的梳理、汇总与解析
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com