在 React 的世界中,不同的 hooks 使命也是不同的,我这里对 React hooks 按照功能分类,分成了 数据更新驱动,状态获取与传递,执行副作用,状态派生与保存,和工具类型, 具体功能划分和使用场景如下:
一、数据更新驱动
1、useState
useState 可以使函数组件像类组件一样拥有 state,函数组件通过 useState 可以让组件重新渲染,更新视图。
useState 基础介绍:
const [ ①state , ②dispatch ] = useState(③initData)
① state,目的提供给 UI ,作为渲染视图的数据源。
② dispatchAction 改变 state 的函数,可以理解为推动函数组件渲染的渲染函数。
③ initData 有两种情况,第一种情况是非函数,将作为 state 初始化的值。 第二种情况是函数,函数的返回值作为 useState 初始化的值。
useState 基础用法:
const DemoState = (props) => { /* number为此时state读取值 ,setNumber为派发更新的函数 */ let [number, setNumber] = useState(0) /* 0为初始值 */ return (<div> <span>{ number }</span> <button onClick={ ()=> { setNumber(number+1) console.log(number) /* 这里的number是不能够即使改变的 */ } } ></button> </div>) } |
2、useReducer
useReducer 是 react-hooks 提供的能够在无状态组件中运行的类似redux的功能 api 。
useReducer 基础介绍:
const [ ①state , ②dispatch ] = useReducer(③reducer)
① 更新之后的 state 值。
② 派发更新的 dispatchAction 函数, 本质上和 useState 的 dispatchAction 是一样的。
③ 一个函数 reducer ,我们可以认为它就是一个 redux 中的 reducer , reducer的参数就是常规reducer里面的state和action, 返回改变后的state, 这里有一个需要注意的点就是:如果返回的 state 和之前的 state ,内存指向相同,那么组件将不会更新。
useReducer 基础用法
const DemoUseReducer = ()=>{ /* number为更新后的state值, dispatchNumbner 为当前的派发函数 */ const [ number , dispatchNumbner ] = useReducer((state,action)=>{ const { payload , name } = action /* return的值为新的state */ switch(name){ case 'add': return state + 1 case 'sub': return state - 1 case 'reset': return payload } return state },0) return <div> 当前值:{ number } { /* 派发更新 */ } <button onClick={()=>dispatchNumbner({ name:'add' })} >增加</button> <button onClick={()=>dispatchNumbner({ name:'sub' })} >减少</button> <button onClick={()=>dispatchNumbner({ name:'reset' ,payload:666 })} >赋值</button> { /* 把dispatch 和 state 传递给子组件 */ } <MyChildren dispatch={ dispatchNumbner } State={{ number }} /> </div> } |
3、useSyncExternalStore
useSyncExternalStore
是一个让你订阅外部 store 的 React Hook。
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
参数
-
subscribe
:一个函数,接收一个单独的callback
参数并把它订阅到 store 上。当 store 发生改变,它应当调用被提供的callback
。这会导致组件重新渲染。subscribe
函数会返回清除订阅的函数。 -
getSnapshot
:一个函数,返回组件需要的 store 中的数据快照。在 store 不变的情况下,重复调用getSnapshot
必须返回同一个值。如果 store 改变,并且返回值也不同了(用 Object.is 比较),React 就会重新渲染组件。 -
可选
getServerSnapshot
:一个函数,返回 store 中数据的初始快照。它只会在服务端渲染时,以及在客户端进行服务端渲染内容的 hydration 时被用到。快照在服务端与客户端之间必须相同,它通常是从服务端序列化并传到客户端的。如果你忽略此参数,在服务端渲染这个组件会抛出一个错误。
返回值
该 store 的当前快照,可以在你的渲染逻辑中使用。
useSyncExternalStore 基础用法:
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 是否发生变化,如果变化,触发更新。
4、useTransition
useTransition 执行返回一个数组。数组有两个状态值:
- 第一个是,当处于过渡状态的标志——isPending。
- 第二个是一个方法,可以理解为上述的 startTransition。可以把里面的更新任务变成过渡任务。
import { useTransition } from 'react'
/* 使用 */
const [ isPending , startTransition ] = useTransition ()
useTransition 基础用法:
除了上述切换 tab 场景外,还有很多场景非常适合 useTransition 产生的过渡任务,比如输入内容,实时搜索并展示数据,这本质上也是有两个优先级的任务,第一个任务就是受控表单的实时响应;第二个就是输入内容改变,数据展示的变化。那么接下来我们写一个 demo 来看一下 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 控制渲染的长列表数据 (在现实场景下长列表可能是一些数据量大的可视化图表)。
二、hooks 之执行副作用
1、useEffect
useEffect 基础介绍:
useEffect(()=>{
return destory
},dep)
useEffect 第一个参数 callback, 返回的 destory , destory 作为下一次callback执行之前调用,用于清除上一次 callback 产生的副作用。
第二个参数作为依赖项,是一个数组,可以有多个依赖项,依赖项改变,执行上一次callback 返回的 destory ,和执行新的 effect 第一个参数 callback 。
对于 useEffect 执行, React 处理逻辑是采用异步调用 ,对于每一个 effect 的 callback, 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 ] :any= 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 中做的功能如下:
- ① 请求数据。
- ② 设置定时器,延时器等。
- ③ 操作 dom , 在 React Native 中可以通过 ref 获取元素位置信息等内容。
- ④ 注册事件监听器, 事件绑定,在 React Native 中可以注册 NativeEventEmitter 。
- ⑤ 还可以清除定时器,延时器,解绑事件监听器等。
2、useLayoutEffect
useLayoutEffect 基础介绍:
useLayoutEffect 和 useEffect 不同的地方是采用了同步执行,那么和useEffect有什么区别呢?
① 首先 useLayoutEffect 是在 DOM 更新之后,浏览器绘制之前,这样可以方便修改 DOM,获取 DOM 信息,这样浏览器只会绘制一次,如果修改 DOM 布局放在 useEffect ,那 useEffect 执行是在浏览器绘制视图之后,接下来又改 DOM ,就可能会导致浏览器再次回流和重绘。而且由于两次绘制,视图上可能会造成闪现突兀的效果。
② useLayoutEffect callback 中代码执行会阻塞浏览器绘制。
useEffect 基础用法:
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> ) } |
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 <div className="css-in-js" > hello , useInsertionEffect </div> } |
三、hooks 之状态获取与传递
1、useContext
useContext 基础介绍
可以使用 useContext ,来获取父级组件传递过来的 context 值,这个当前值就是最近的父级组件 Provider 设置的 value 值,useContext 参数一般是由 createContext 方式创建的 ,也可以父级上下文 context 传递的 ( 参数为 context )。useContext 可以代替 context.Consumer 来获取 Provider 中保存的 value 值。
const contextValue = useContext(context)
useContext 接受一个参数,一般都是 context 对象,返回值为 context 对象内部保存的 value 值。
useContext 基础用法:
/* 用useContext方式 */ const DemoContext = ()=> { const value:any = 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> } |
2、useRef
useRef 基础介绍:
useRef 可以用来获取元素,缓存状态,接受一个状态 initState 作为初始值,返回一个 ref 对象 cur, cur 上有一个 current 属性就是 ref 对象需要获取的内容。
const cur = React.useRef(initState)
console.log(cur.current)
useRef 基础用法:
useRef 获取 DOM 元素,在 React Native 中虽然没有 DOM 元素,但是也能够获取组件的节点信息( fiber 信息 )。
const DemoUseRef = ()=>{ const dom= useRef(null) const handerSubmit = ()=>{ /* <div >表单组件</div> dom 节点 */ console.log(dom.current) } return <div> {/* ref 标记当前dom节点 */} <div ref={dom} >表单组件</div> <button onClick={()=>handerSubmit()} >提交</button> </div> } |
如上通过 useRef 来获取 DOM 节点。
useRef 保存状态, 可以利用 useRef 返回的 ref 对象来保存状态,只要当前组件不被销毁,那么状态就会一直存在。
const status = useRef(false) /* 改变状态 */ const handleChangeStatus = () => { status.current = true } |
四、hooks 之状态派生与保存
1、useMemo
useMemo 可以在函数组件 render 上下文中同步执行一个函数逻辑,这个函数的返回值可以作为一个新的状态缓存起来。那么这个 hooks 的作用就显而易见了:
场景一:在一些场景下,需要在函数组件中进行大量的逻辑计算,那么我们不期望每一次函数组件渲染都执行这些复杂的计算逻辑,所以就需要在 useMemo 的回调函数中执行这些逻辑,然后把得到的产物(计算结果)缓存起来就可以了。
场景二:React 在整个更新流程中,diff 起到了决定性的作用,比如 Context 中的 provider 通过 diff value 来判断是否更新
useMemo 基础介绍:
const cacheSomething = useMemo(create,deps)
- ① create:第一个参数为一个函数,函数的返回值作为缓存值,如上 demo 中把 Children 对应的 element 对象,缓存起来。
- ② deps: 第二个参数为一个数组,存放当前 useMemo 的依赖项,在函数组件下一次执行的时候,会对比 deps 依赖项里面的状态,是否有改变,如果有改变重新执行 create ,得到新的缓存值。
- ③ cacheSomething:返回值,执行 create 的返回值。如果 deps 中有依赖项改变,返回的重新执行 create 产生的值,否则取上一次缓存值。
缓存计算结果:
function Scope(){ const style = useMemo(()=>{ let computedStyle = {} // 经过大量的计算 return computedStyle },[]) return <div style={style} ></div> } |
缓存组件,减少子组件 rerender 次数:
function Scope ({ children }){ const renderChild = useMemo(()=>{ children() },[ children ]) return <div>{ renderChild } </div> } |
2、useCallback
useMemo 和 useCallback 接收的参数都是一样,都是在其依赖项发生变化后才执行,都是返回缓存的值,区别在于 useMemo 返回的是函数运行的结果,useCallback 返回的是函数,这个回调函数是经过处理后的也就是说父组件传递一个函数给子组件的时候,由于是无状态组件每一次都会重新生成新的 props 函数,这样就使得每一次传递给子组件的函数都发生了变化,这时候就会触发子组件的更新,这些更新是没有必要的,此时我们就可以通过 usecallback 来处理此函数,然后作为 props 传递给子组件。
useCallback 基础用法:
/* 用react.memo */ const DemoChildren = React.memo((props)=>{ /* 只有初始化的时候打印了 子组件更新 */ console.log('子组件更新') useEffect(()=>{ props.getInfo('子组件') },[]) return <div>子组件</div> }) const DemoUseCallback=({ id })=>{ const [number, setNumber] = useState(1) /* 此时usecallback的第一参数 (sonName)=>{ console.log(sonName) } 经过处理赋值给 getInfo */ const getInfo = useCallback((sonName)=>{ console.log(sonName) },[id]) return <div> {/* 点击按钮触发父组件更新 ,但是子组件没有更新 */} <button onClick={ ()=>setNumber(number+1) } >增加</button> <DemoChildren getInfo={getInfo} /> </div> } |