前言
本文全面介绍了 React Hooks 的所有 API 概念、用法、丰富的 demo 以及部分底层原理。
实际上,React 官网已经列出了所有的 hooks,但比较零散、缺乏 demo。建议阅读本文的同时,结合官网一同学习。
React 最早使用 React.createElement 创建组件,之后演变成 ClassComponent,最后有了 hooks。每一次演变都是一次性能、开发体验以及能力的进化。
Hook 简介
Hook 是 React 16.8 (2019.02.06)的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
去 class 化,以 use 开头的函数式组件 API。
Hook 解决的 ClassComponent 问题
组件之间很难复用状态逻辑(只能用 HOC,或者 render props),会导致组件树层级很深 –> 使用自定义 HOOK(代码复用)
复杂组件变得难以理解,生命周期钩子逻辑耦合、需引入状态管理 –> 使用 Effect Hook(代码管理)
class 类组件很难理解,比如方法需要 bind,this 指向不明确 –> 函数式组件(摆脱难用的 class)
使用注意
不能将 hooks 放在循环、条件语句或者嵌套方法内。react 是根据 hooks 出现顺序来记录对应状态的,执行以上操作会打乱 hooks 顺序
只在 function 组件和自定义 hooks 中使用 hooks。
命名规范:
useState 返回数组的第二项以 set 开头(仅作为约定)。
自定义 hooks 以 use 开头(可被 lint 校验)。
React 中提供的 hooks:
useState:setState
useReducer:setState,同时 useState 也是该方法的封装
useRef: ref
useImperativeHandle: 给 ref 分配特定的属性
useContext: context,需配合 createContext 使用
useMemo: 可以对 setState 的优化
useCallback: useMemo 的变形,对函数进行优化
useEffect: 类似 componentDidMount/Update, componentWillUnmount,当效果为 componentDidMount/Update 时,总是在整个更新周期的最后(页面渲染完成后)才执行
useLayoutEffect: 用法与 useEffect 相同,区别在于该方法的回调会在数据更新完成后,页面渲染之前进行,该方法会阻碍页面的渲染
useDebugValue:用于在 React 开发者工具中显示自定义 hook 的标签
1. State Hooks
1.1 useState
function Counter({ initialCount }) {
const [state, setState] = useState(initialState)
return (
<>
Count: {count}
<button onClick={() => setCount(0)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
</>
)
}
useState 有一个参数,该参数可传如任意类型的值或者返回任意类型值的函数。
useState 返回值为一个数组,数组的第一个参数为我们需要使用的 state,第二个参数为一个 setter 函数,可传任意类型的变量,或者一个接收 state 旧值的函数,其返回值作为 state 新值。
注意: set 方法不会像类组件的 setState 一样做 merge,所以建议:
如果数据结构简单,可以将变量根据数据结构需要放在不同的 useState 中,避免放入一个对象中大量使用类似 {…state, value} 形势。
如果数据结构复杂,建议使用 useReducer 管理组件的 state。
1.2 useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
useReducer 接收三个参数,第一个参数为一个 reducer 函数,第二个参数是 reducer 的初始值,第三个参数为可选参数,值为一个函数,可以用来惰性提供初始状态。
这意味着我们可以使用一个 init 函数来计算初始状态/值,而不是显式的提供值。如果初始值可能会不一样,这会很方便,最后会用计算的值来代替初始值。
reducer 接受两个参数一个是 state 另一个是 action ,用法原理和 redux 中的 reducer 一致。
useReducer 返回一个数组,数组中包含一个 state 和 dispath,state 是返回状态中的值,而 dispatch 是一个可以发布事件来更新 state 的函数。
function init(initialCount) {
return { count: initialCount }
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
case 'reset':
return init(action.payload)
default:
throw new Error()
}
}
function Counter({ initialCount }) {
const [state, dispatch] = useReducer(reducer, initialCount, init)
return (
<>
Count: {state.count}
<button
onClick={() => dispatch({ type: 'reset', payload: initialCount })}
>
Reset
</button>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</>
)
}
function render() {
ReactDOM.render(<Counter initialCount={0} />, document.getElementById('root'))
}
同时,useReucer 也是 useState 的内部实现.
useState 和 useReucer 的实现原理:
let memoizedState
function useReducer(reducer, initialArg, init) {
let initState = 0
if (typeof init === 'function') {
initState = init(initialArg)
} else {
initState = initialArg
}
function dispatch(action) {
memoizedState = reducer(memoizedState, action)
// React 的渲染
// render()
}
memoizedState = memoizedState || initState
return [memoizedState, dispatch]
}
function useState(initState) {
return useReducer((oldState, newState) => {
if (typeof newState === 'function') {
return newState(oldState)
}
return newState
}, initState)
}
useState 的替代方案。
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。
跳过 dispatch
如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用 Object.is 比较算法 来比较 state。)
需要注意的是,React 可能仍需要在跳过渲染前再次渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。
2. Effect Hooks
2.1 useEffect
useEffect(effect, array);
useEffect 接收两个参数,没有返回值:
第一个参数为 effect 函数,该函数将在 componentDidMmount 时触发和 componentDidUpdate 时有条件触发(该添加为 useEffect 的第二个数组参数)。
同时该 effect 函数可以返回一个函数(returnFunction),returnFunction 将会在 componentWillUnmount 时触发和在 componentDidUpdate 时先于 effect 有条件触发(先执行 returnFuncton 再执行 effect,比如需要做定时器的清除)。
注意: 与 componentDidMount 和 componentDidUpdate 不同之处是,effect 函数触发时间为在浏览器完成渲染之后。 如果需要在渲染之前触发,需要使用 useLayoutEffect。
第二个参数 array 作为有条件触发情况时的条件限制:
如果不传,则每次 componentDidUpdate 时都会先触发 returnFunction(如果存在),再触发 effect。
如果为空数组 [],componentDidUpdate 时不会触发 returnFunction 和 effect。
如果只需要在指定变量变更时触发 returnFunction 和 effect,将该变量放入数组。
2.2 useLayoutEffect
useLayoutEffect(effect, array);
与 useEffect 使用方法一样,只是执行回调函数的时机有着略微区别,运行时机更像是 componentDidMount 和 componentDidUpdate。但是要注意的是,该方法是同步方法,在浏览器 paint 之前执行,会阻碍浏览器 paint,只有当我们需要进行 DOM 的操作时才使用该函数(比如设定 DOM 布局尺寸,这样可以防抖动)。
useEffect 与 useLayoutEffect:
正常情况用默认的 useEffect 钩子就够了,这可以保证状态变更不阻塞渲染过程,但如果 effect 更新(清理)中涉及 DOM 更新操作,用 useEffect 就会有意想不到的效果,这时我们最好使用 useLayoutEffect 。
比如逐帧动画 requestAnimationFrame ,需要保证同步变更。这也符合作者说到的 useEffect 的时期是非常晚,可以保证页面是稳定下来再做事情。
钩子的执行顺序:useLayoutEffect > requestAnimationFrame > useEffect
3. Context Hooks
设计目的: context 设计目的是为共享那些被认为对于一个组件树而言是“全局”的数据。
使用场景: context 通过组件树提供了一个传递数据的方法,从而避免了在每一个层级手动的传递 props 属性。
3.1 createContext
const { Provider, Consumer } = React.createContext(
defaultValue,
calculateChangedBits
)
该方法创建一对 { Provider, Consumer }。当 React 渲染 context 组件 Consumer 时,它将从组件树的上层中最接近的匹配的 Provider 读取当前的 context 值。Consumer 是 Provider 提供数据的使用者。
如果上层的组件树没有一个匹配的 Provider,而此时你需要渲染一个 Consumer 组件,那么你可以用到 defaultValue 。
3.1.1 Provider
React 组件允许 Consumers 订阅 context 的改变 。而 Provider 就是发布这种状态的组件,该组件 接收一个 value 属性 传递给 Provider 的后代 Consumers。 一个 Provider 可以联系到多个 Consumers。Providers 可以被嵌套以覆盖组件树内更深层次的值。
export const ProviderComponent = props => {
return <Provider value={}>{props.children}</Provider>
}
关于 calculateChangedBits:
在 createContext() 函数中的第二个参数为 calculateChangedBits,它是一个接受 newValue 与 oldValue 的函数,返回值作为 changedBits,在 Provider 中,当 changedBits = 0,将不再触发更新。而在 Consumer 中有一个不稳定的 props,unstable_observedBits,若 Provider 的 changedBits & observedBits = 0,也将不触发更新。
calculateChangedBits 涉及位运算,用来判断比较 context 是否更新,我们可以用它来自定义更新细粒度从而避免不必要的更新。
const Context = React.createContext({ foo: 0, bar: 0 }, (a, b) => {
let result = 0
if (a.foo !== b.foo) {
result |= 0b01
}
if (a.bar !== b.bar) {
result |= 0b10
}
return result
})
calculateChangedBits observedBits
这两个 api 是一对,现在这个东西可能不太稳定,慎用。
3.1.2 Consumer
<Consumer>
{value => /_ render something based on the context value _/}
</Consumer>
一个可以订阅 context 变化的 React 组件。当 context 值发生改变,即 Provider 的值发生改变时, 作为 Provider 后代的所有 Consumers 都会重新渲染。
从 Provider 到其后代的 Consumers 传播不受 shouldComponentUpdate 方法的约束,因此即使祖先组件退出更新时,后代 Consumer 也会被更新。
接收一个 函数作为子节点,该函数接收当前 context 的值并返回一个 React 节点。传递给函数的 value 将等于组件树中上层 context 的最近的 Provider 的 value 属性。如果 context 没有 Provider ,那么 value 参数将等于被传递给 createContext() 的 defaultValue 。
// 创建一个 theme Context, 默认 theme 的值为 light
const ThemeContext = React.createContext('light')
function ThemedButton(props) {
// ThemedButton 组件从 context 接收 theme
return (
<ThemeContext.Consumer>
{theme => <Button {...props} theme={theme} />}
</ThemeContext.Consumer>
)
}
// 中间组件
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
)
}
class App extends React.Component {
render() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
)
}
}
3.2 useContext
const Context = React.createContext('light')
// Provider
class Provider extends Component {
render() {
return (
<Context.Provider value={'dark'}>
<DeepTree />
</Context.Provider>
)
}
}
// Consumer
function Consumer(props) {
const context = useContext(Context)
return <div>{context} // dark</div>
}
使用效果和 Consumer 类似,但是是函数式的使用方式,仍然需要与 Provider 配合使用。
该函数接收一个 Context 类型的参数(就是包裹了 Provider 和 Consumer 的那个对象),返回 Provider 中的 value 属性对象的值。
3.3 配合 useReducer 使用
// index.jsx
import React from 'react'
import ShowArea from './ShowArea'
import Buttons from './Buttons'
import { Color } from './Color'
function Demo() {
return (
<div>
<Color>
<ShowArea />
<Buttons />
</Color>
</div>
)
}
export default Demo
// Color.jsx
import React, { createContext, useReducer } from 'react'
export const ColorContext = createContext()
export const UPDATE_COLOR = 'UPDATE_COLOR'
function reducer(state, action) {
switch (action.type) {
case UPDATE_COLOR:
return action.color
default:
return state
}
}
export const Color = props => {
const [color, dispatch] = useReducer(reducer, 'blue')
return (
<ColorContext.Provider value={{ color, dispatch }}>
{props.children}
</ColorContext.Provider>
)
}
// Button.jsx
import React, { useContext } from 'react'
import { ColorContext, UPDATE_COLOR } from './Color'
function Buttons() {
const { dispatch } = useContext(ColorContext)
return (
<div>
<button
onClick={() => {
dispatch({ type: UPDATE_COLOR, color: 'red' })
}}
>
red
</button>
<button
onClick={() => {
dispatch({ type: UPDATE_COLOR, color: 'yellow' })
}}
>
yellow
</button>
</div>
)
}
export default Buttons
// ShowArea.jsx
import React, { useContext } from 'react'
import { ColorContext } from './Color'
function ShowArea() {
const { color } = useContext(ColorContext)
return <div style={{ color }}>color:{color}</div>
}
export default ShowArea
4. Ref Hooks
4.1 useRef
4.1.1 组件引用
createRef 使用方法和 useRef 一致,返回的是一个 ref 对象,该对象下面有一个 current 属性指向被引用对象的实例,一般用于操作 dom:
import { React, createRef, useRef } from 'react'
const FocusInput = () => {
const inputElement = createRef()
// const inputElement = useRef()
const handleFocusInput = () => {
inputElement.current.focus()
}
return (
<>
<input type="text" ref={inputElement} />
<button onClick={handleFocusInput}>Focus Input</button>
</>
)
}
export default FocusInput
但是,这两者对应 ref 的引用其实是有着本质区别的:createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用。
const App = () => {
const [renderIndex, setRenderIndex] = React.useState(1)
const refFromUseRef = React.useRef()
const refFromCreateRef = React.createRef()
if (!refFromUseRef.current) {
refFromUseRef.current = renderIndex
}
if (!refFromCreateRef.current) {
refFromCreateRef.current = renderIndex
}
return (
<>
<p>Current render index: {renderIndex}</p>
<p>
<b>refFromUseRef</b> value: {refFromUseRef.current}
</p>
<p>
<b>refFromCreateRef</b> value:{refFromCreateRef.current}
</p>
<button onClick={() => setRenderIndex(prev => prev + 1)}>
Cause re-render
</button>
</>
)
}
因为一直都存在 refFromUseRef.current,所以并不会改变值。
4.1.2 替代 this
那么,为什么要赋予 useRef 这种特性,在什么场景下我们需要这种特性呢?
一个经典案例:
import React, { useRef, useState } from 'react'
function App() {
const [count, setCount] = useState(0)
function handleAlertClick() {
setTimeout(() => {
alert(`Yout clicked on ${count}`)
}, 3000)
}
return (
<div>
<p>You click {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<button onClick={handleAlertClick}>Show alert</button>
</div>
)
}
export default App
当我们更新状态的时候, React 会重新渲染组件, 每一次渲染都会拿到独立的 count 状态, 并重新渲染一个 handleAlertClick 函数. 每一个 handleAlertClick 里面都有它自己的 count。
你会发现,count 的值并不能够实时的显示更新的数据,这个是由于 JS 中一值就存在的闭包机制导致的,当点击显示弹窗的按钮时,此时的 count 的值已经确定,并且传入到了 alert 方法的回调中,形成闭包,后续值的改变不会影响到定时器的触发。
而如果在类组件中,如果我们使用的是 this.state.count,得到的结果又会是实时的,因为它们都是指向的同一个引用对象。
在函数组件中,我们可以使用 useRef 来实现实时得到新的值,这就是 useRef 的另外一种用法,它还相当于 this , 可以存放任何变量。useRef 可以很好的解决闭包带来的不方便性。
import React, { useRef, useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const lastestCount = useRef()
lastestCount.current = count
function handleAlertClick() {
setTimeout(() => {
alert(`You clicked on ${lastestCount.current}`) // 实时的结果
}, 3000)
}
return (
<div>
<p>Yout click {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<button onClick={handleAlertClick}>Show alert</button>
</div>
)
}
export default App
要值得注意的是,如果我们在 useRef 中传入参数(一般 useRef 中传值就用在这里),使用下面这种方法来访问值,结果又会不同:
import React, { useRef, useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const lastestCount = useRef(count) // 直接传入 count
function handleAlertClick() {
setTimeout(() => {
alert(`You clicked on ${lastestCount.current}`)
}, 3000)
}
return (
<div>
<p>Yout click {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<button onClick={handleAlertClick}>Show alert</button>
</div>
)
}
export default App
点击的时候我们会发现弹出来的值永远是 0,正如我们所说,useRef 返回的都是相同的引用,参数在第一个传入进去的时候已经赋值给了 current 属性,返回了一个实例回来,后续因为已经有了实例了,所以会直接将原来的实例返回,传入的参数也就不再起作用了。
4.2 forwardRef
基本用法:
forwardRef((props, ref) => {
// dosomething
return (
<div ref={ref}></div>
)
})
forwardRef 准确来说不是 hooks 中的内容,但是如果我们要使用 useImperativeHandle,就需要使用它来进行搭配。
该方法的作用是:引用父组件的 ref 实例,成为子组件的一个参数,可以引用父组件的 ref 绑定到子组件自身的节点上。
该方法可以看做是一个高阶组件,本身 props 只带有 children 这个参数,它能将从父组件拿到的 ref 和 props 传入给子组件,由子组件来调用父组件传入的 ref。
传入的组件会接收到两个参数,一个是父组件传递的 props,另一个就是 ref 的引用。
// 我们可以使用三层组件嵌套,把传入 forwardRef 的函数看成传值的中间层
function InputWithLabel(props) {
// 这里的 myRef 为通过外部打入的父级 ref 节点
const { label, myRef } = props
const [value, setValue] = useState("")
const handleChange = e => {
const value = e.target.value
setValue(value)
}
return (
<div>
<span>{label}:</span>
<input type="text" ref={myRef} value={value} onChange={handleChange} />
</div>
)
}
// 这里用 forwardRef 来承接得到父级传入的 ref 节点,并将其以参数的形式传给子节点
const RefInput = React.forwardRef((props, ref) => (
<InputWithLabel {...props} myRef={ref} />
))
// 调用该 RefInput 的过程
function App() {
// 通过 useRef hook 获得相应的 ref 节点
const myRef = useRef(null)
const handleFocus = () => {
const node = myRef.current
console.log(node)
node.focus()
}
return (
<div className="App">
<RefInput label={"姓名"} ref={myRef} />
<button onClick={handleFocus}>focus</button>
</div>
)
}
4.3 useImperativeHandle
基本用法:
useImperativeHandle(ref, () => ({
a:1,
b:2,
c:3
}))
官方建议 useImperativeHandle 和 forwardRef 同时使用,减少暴露给父组件的属性,避免使用 ref 这样的命令式代码。
useImperativeHandle 有三个参数:
第一个参数,接收一个通过 forwardRef 引用父组件的 ref 实例
第二个参数一个回调函数,返回一个对象,对象里面存储需要暴露给父组件的属性或方法
第三个参数为一个可选参数,该参数是一个依赖项数组,就像 useEffect 那样
function Example(props, ref) {
const inputRef = useRef()
useImperativeHandle(ref, () => ({
// 父组件可以通过 this.xxx.current.focus 的方式使用子组件传递出去的 focus 方法
focus: () => {
inputRef.current.focus()
}
}))
return <input ref={inputRef} />
}
export default forwardRef(Example)
class App extends Component {
constructor(props){
super(props)
this.inputRef = createRef()
}
render() {
return (
<>
<Example ref={this.inputRef}/>
<button onClick={() => {this.inputRef.current.focus()}}>Click</button>
</>
)
}
}
5. 性能优化
5.1 memo
我们都知道,对于类组件来说,有 PureComponent 可以通过判断父组件传入的 props 是否进行改变来优化渲染性能。所以,在函数式组件中,React 也有一个类似 PureComponent 功能的高阶组件 memo,效果同 PureComponent,都会判断父组件传入的 props 是否发生改变来重新渲染当前组件。
使用方法很简单:
import React, { memo } from 'react'
function Demo(props){
return (
<div>{props.name}</div>
)
}
export default memo(Demo)
5.2 useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
useMemo 是 React 推出用于优化函数式组件性能的 hooks,它可以传入两个参数:
第一个参数为一个工厂函数,返回一个缓存的值,也就是仅当重新渲染且数组中的值发生改变时,回调函数才会重新计算缓存数据,这可以使得我们避免在每次重新渲染时都进行复杂的数据计算。
第二个参数为一个依赖项数组,只有依赖项中的数据发生改变时才重新计算值,用法同 useEffect 的依赖项数组。
import React, { useState, useMemo } from 'react'
function Child({ color }) {
// color 值不发生改变不会打印 console,但是依旧会触发重新渲染,如果连这个函数都不执行,在最外层加上 memo
const actionColor = useMemo(() => {
console.log('color update')
return color
}, [color])
return <div style={{ color: actionColor }}>{actionColor}</div>
}
function MemoCount() {
const [count, setCount] = useState(0)
const [color, setColor] = useState('blue')
return (
<div>
<button
onClick={() => {
setCount(count + 1)
}} >
Update Count
</button>
<button
onClick={() => {
setColor('green')
}} >
Update Color
</button>
<div>{count}</div>
<Child color={color} />
</div>
)
}
export default MemoCount
上面的例子其实并不是 useMemo 最常用的场景,就像之前说的,在 props 发生改变的时候才会触发被 memo 的组件的重新渲染,但是如果只是 props 的引用对象发生改变,实际的值并没有发生改变,组件还是会被重新渲染。就像下面这样:
import React, { useState, memo } from 'react'
const Child = memo(({ config }) => {
console.log(config)
return <div style={{ color:config.color }}>{config.text}</div>
})
function MemoCount() {
const [count, setCount] = useState(0)
const [color, setColor] = useState('blue')
const config = {
color,
text:color
}
return (
<div>
<button
onClick={() => {
setCount(count + 1)
}} >
Update Count
</button>
<button
onClick={() => {
setColor('green')
}} >
Update Color
</button>
<div>{count}</div>
<Child config={config} />
</div>
)
}
export default MemoCount
当我们改变 count 值的时候,我们发现这其实和 config 对象是无关的,但是 Child 组件依旧会重新渲染,因为由于父组件的重新渲染,config 被重新赋值了新的对象,虽然新的对象里面的值都是相同的,但由于是引用类型对象,所以依旧会改变值,要改变这种状况,我们需要:
// 使用 useMemo
import React, { useState,useMemo, memo } from 'react'
const Child = memo(({ config }) => {
console.log(config)
return <div style={{ color:config.color }}>{config.text}</div>
})
function MemoCount() {
const [count, setCount] = useState(0)
const [color, setColor] = useState('blue')
// 只会根据 color 的改变来返回不同的对象,否则都会返回同一个引用对象
const config = useMemo(()=>({
color,
text:color
}),[color])
return (
<div>
<button
onClick={() => {
setCount(count + 1)
}}
>
Update Count
</button>
<button
onClick={() => {
setColor('green')
}}
>
Update Color
</button>
<div>{count}</div>
<Child config={config} />
</div>
)
}
export default MemoCount
这样,当 count 的值发生改变时,子组件就不会再重新渲染了。
5.3 useCallback
const memoizedCallback = useCallback(
() => {
doSomething(a, b)
},
[a, b],
)
useCallback 的用法和 useMemo 类似,是专门用来缓存函数的 hooks,也是接收两个参数,同时,我们第一个参数传入额回调函数就是要缓存的函数。
注意:第二个参数目前只用于指定需要判断是否变化的参数,并不会作为形参传入回调函数。建议回调函数中使用到的变量都应该在数组中列出。
要在回调函数中传入参数,我们可以使用高阶函数的方法,useCallback 会帮我们缓存这个高阶函数,如上所示。
当然,同样可以在 callback 中写入形参:
const memoizedCallback = useCallback(
(a, b) => {
doSomething(a, b)
},
[],
)
// memoizedCallback 其实就是传入的回调函数
可以看出,都是当依赖项方式改变时,才触发回调函数。因此,我们可以认为:useCallback(fn, inputs) 等同于 useMemo(() => fn, inputs)
// useCallback 的实现原理
let memoizedState = null
function useCallback(callback, inputs) {
const nextInputs =
inputs !== undefined && inputs !== null ? inputs : [callback]
const prevState = memoizedState;
if (prevState !== null) {
const prevInputs = prevState[1]
if (areHookInputsEqual(nextInputs, prevInputs)) {
return prevState[0]
}
}
memoizedState = [callback, nextInputs]
return callback
}
// useMemo 的实现原理
function useMemo(callback, inputs){
return useCallback(callbak(),inputs)
}
更多情况,useCallback 一般用于在 React 中给事件绑定函数并需要传入参数的时候:
// 下面的情况可以保证组件重新渲染得到的方法都是同一个对象,避免在传给 onClick 的时候每次都传不同的函数引用
import React, { useState, useCallback } from 'react'
function MemoCount() {
const [count, setCount] = useState(0)
memoSetCount = useCallback(()=>{
setCount(count + 1)
},[])
return (
<div>
<button
onClick={memoSetCount}
>
Update Count
</button>
<div>{color}</div>
</div>
)
}
export default MemoCount
6. Debug
6.1 useDebugValue
useDebugValue(value)
// or
useDebugValue(date, date => date.toDateString());
useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。
useDebugValue 接收两个参数,根据传入参数数量的不同有不同的使用方式:
直接传 debug 值
function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); // ... // 在开发者工具中的这个 Hook 旁边显示标签 // e.g. "FriendStatus: Online" useDebugValue(isOnline ? 'Online' : 'Offline'); return isOnline; }
提示
我们不推荐你向每个自定义 Hook 添加 debug 值。当它作为共享库的一部分时才最有价值。延迟格式化 debug 值
const date = new Date() useDebugValue(date, date => date.toDateString())
在某些情况下,格式化值的显示可能是一项开销很大的操作。除非需要检查 Hook,否则没有必要这么做。
7. 自定义 Hooks
自定义 Hook 是一个函数,其名称以 use 开头,函数内部可以调用其他的 Hook
// myhooks.js
// 下面自定义了一个获取窗口长宽值的 hooks
import React, { useState, useEffect, useCallback } from 'react'
function useWinSize() {
const [size, setSize] = useState({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
})
const onResize = useCallback(() => {
setSize({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
})
}, [])
useEffect(() => {
window.addEventListener('resize', onResize)
return () => {
window.removeEventListener('reisze', onResize)
}
}, [onResize])
return size
}
export const useWinSize
// index.js
import { useWinSize } from './myhooks'
function MyHooksComponent() {
const size = useWinSize()
return (
<div>
页面 Size:{size.width}x{size.height}
</div>
)
}
export default MyHooksComponent
8. 复杂页面 hooks 代码组织
简单状态管理:useState
复杂状态管理:useReducer
祖先组件操作子孙组件:useRef & useImperativeHandle
祖先组件传值给子孙组件:useContext
子组件渲染性能优化:useMemo & useCallback
依赖副作用管理:useEffect & useLayoutEffect
复用复杂逻辑:自定义 hooks
调试 hooks:useDebugValue