一文学会React Hooks

前言


本文全面介绍了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>
    </>
  )
}

|

image

因为一直都存在 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
image

当我们更新状态的时候, 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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270