天天看點

React Hook 使用詳解

一:簡介

Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。
hook 引入時間 (react 版本更新大事記)
- 2013年05月29日     v0.3.0        Facebook 開源了React 
	…..
- 2014年10月28日     v0.12.0      使用BSD協定+附加專利授權協定- 2016年03月30日     v0.14.0      拆分成React 和 ReactDOM
- 2016年04月09日     v15.0.0	     挂載元件方式改動、SVG相容等
- 2017年09月25日     v15.6.2	     開源協定改為MIT
- 2017年09月26日     v16.0.0      引入Fiber
- 2019年02月06日     v16.8.0      Hook 引入
- 2020年10月20日     v17.0.0      Concurrent Mode、底層技術改造、解決一些曆史包袱
           
在Hook之前

函數元件

function Welcome(props) {
  return <h1>hello, {props.name}</h1>
}
           

類元件

React Hook 使用詳解
有了Hook後

函數元件能夠完成和類元件一樣的功能,有自己的狀态,生命周期以及狀态控制能力

let timer = null
export default function Clock() {

  const [date, setDate] = useState(new Date())

  useEffect(() => {
    timer = setInterval(() => {
      setDate(new Date())
    }, 1000)
    return () => {
      clearTimeout(timer)
    };
  }, [])

  return <div>
    <h1>Hello,world!</h1>
    <h2>It is {date.toLocaleTimeString()}.</h2>
  </div>
}
           

二:React 提供的内置Hook API清單

useState
useEffect

useContext
useRe
useCallback
useMemo

useReducer
useLayoutEffect

useImperativeHandle
useDebugValue
useTransition
useDeferredValue
           

三:Hook API詳解

  1. useState

    源碼定義

export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}
           

使用示例

const [data, setData] = useState(function () {
    return JSON.parse(bigData)
  })
           

如果傳入函數,函數隻在第一此初始化元件的時候執行,後面元件update的時候不會重複計算,适合對初始值需要進行大量計算的場景的優化的時候使用

  1. useEffect

源碼定義

export function useEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const dispatcher = resolveDispatcher();
  return dispatcher.useEffect(create, deps);
}
           

功能:

  • 處理副作用
  • 實作生命周期
  • 粒度可控

使用示例

React Hook 使用詳解

useEffect 作為副作用處理函數,可以模拟 componentDidMount 已經componentDidUpdate等生命周期,

  1. useLayoutEffect

源碼定義

export function useLayoutEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const dispatcher = resolveDispatcher();
  return dispatcher.useLayoutEffect(create, deps);
}
           

功能:

  • 處理副作用
  • 實作生命周期
  • 粒度可控

使用方法和useEffect一緻

差別:會阻塞浏覽器渲染,比useEffect先執行

使用場景:

不想讓使用者看到dom變化過程,副作用回調函數裡有dom更新操作适合使用

示例代碼

對比下面兩組代碼,一個是使用的useEffect ,一個是使用的useLayoutEffect

import { React } from '../adaptation'
// import { useEffect } from './react/packages/react'

const { useLayoutEffect, useEffect, useState } = React

export default function TestUseLayoutEffect(props) {
  const [count, setCount] = useState(0)
  
  const handleClick = () => {
    setCount(0)
  }

  useLayoutEffect(() => {
    if (count === 0) {
      // 耗時操作 start
      const arr = []
      for (let i = 0; i < 100000000; i++) {
        arr.push(i)
      }
      // 耗時操作 end
      setCount(Math.random())
    }
  }, [count])

  return <div >
    {count}
    <button onClick={handleClick}>改變</button>
  </div>
}
           
import { React } from '../adaptation'
// import { useEffect } from './react/packages/react'

const { useEffect, useState } = React

export default function TestUseEffect(props) {

  const [count, setCount] = useState(0)
  const handleClick = () => {
    setCount(0)
  }

  useEffect(() => {
    if (count === 0) {
      // 耗時操作 start
      const arr = []
      for (let i = 0; i < 100000000; i++) {
        arr.push(i)
      }
      // 耗時操作end
      setCount(Math.random())
    }
  }, [count])

  useEffect(() => {
    console.log('xxx123')
  })
  return <div >
    {count}
    <button onClick={handleClick}>改變</button>
  </div>
}
           
  1. useRef

源碼定義

export function useRef<T>(initialValue: T): {| current: T |} {
  const dispatcher = resolveDispatcher();
  return dispatcher.useRef(initialValue);
}
           

示例

const { useRef } = React

export default function TestRef() {
  const myRef = useRef(null)
  const handleClick = () => {
    console.log(myRef.current)
  }
  return <div>
    <button ref={myRef}  onClick={handleClick}>click</button>
  </div>
}
           
React Hook 使用詳解

useRef vs createRef()

功能類似

useRef 會在整個生命周期中保持引用修改useRef current的值不會觸發重新渲染

createRef 每次重新渲染值都會變化

  1. useMemo 和 useCallback

源碼定義

export function useCallback<T>(
  callback: T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useCallback(callback, deps);
}

export function useMemo<T>(
  create: () => T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useMemo(create, deps);
}
           

useMemo 避免更新時重複計算值

useCallback 避免重新定義函數

示例:

下面這個例子,無論更新多少次,隻要name不變,就不會執行genWelcomeStr方法

import { React } from '../adaptation'

const { useMemo } = React

export default function TestMemo(props) {
  const { name, count } = props

  const genWelcomeStr = () => {
    console.log('genWelcomeStr called')
    return 'Hello,' + name
  }
  const welcomeStr = useMemo(genWelcomeStr, [name])
  // const welcomeStr = genWelcomeStr()

  return <div>
    {count}
    {welcomeStr}
  </div>
}
           

下面兩組代碼,通過父子元件 useCallback 和useMemo的配合,隻要name不變,useCallback包裹的函數指向就不會變,子元件中通過useMemo來監聽傳入的方法,隻要方法指向不變就不會重複計算

import { React } from '../adaptation';
// import TestMemo from './TestComponents/TestMemo'
import TestUseCallback from './TestUseCallback';

const { useState, useCallback } = React

function TestUseCallbackInApp() {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('bob')
  
  const handleClick = (count) => {
    setCount(++count)
  }

  const handleGenStr = useCallback(() => {
    return 'name: ' + name
  }, [name])

  return <div className="App">
    <TestUseCallback handleGenStr={handleGenStr} />
    {count}
    <button onClick={() => handleClick(count)}>add</button>
    <button onClick={() => setName('jack' + Date.now())}>changeName</button>
  </div>
}

export default TestUseCallbackInApp;

           
import { React } from '../adaptation'

const { useMemo } = React

export default function TestUseCallback(props) {
  const { handleGenStr } = props

  const renderStr = useMemo(() => {
    console.log('handleGenStr called')
    return handleGenStr()
  }, [handleGenStr])

  console.log('child comp render')
  return <div >
    {renderStr}
  </div>
}
           

useMemo和useCallback 适合在大資料量等極端情況來做優化,否則普通資料量下,優化效果不大,甚至由于多了很多監聽邏輯,反而使代碼體積和性能變得更糟,甚至引起bug,底層引擎的速度可以抵消這一點性能差異。

  1. useReducer

    源碼定義

export function useReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useReducer(reducer, initialArg, init);
}
           

示例

React Hook 使用詳解
  1. useContext

源碼定義

export function useContext<T>(
  Context: ReactContext<T>,
  unstable_observedBits: number | boolean | void,
) {
  const dispatcher = resolveDispatcher();
  if (__DEV__) {
    if (unstable_observedBits !== undefined) {
      console.error(
        'useContext() second argument is reserved for future ' +
        'use in React. Passing it is not supported. ' +
        'You passed: %s.%s',
        unstable_observedBits,
        typeof unstable_observedBits === 'number' && Array.isArray(arguments[2])
          ? '\n\nDid you call array.map(useContext)? ' +
          'Calling Hooks inside a loop is not supported. ' +
          'Learn more at https://fb.me/rules-of-hooks'
          : '',
      );
    }

    // TODO: add a more generic warning for invalid values.
    if ((Context: any)._context !== undefined) {
      const realContext = (Context: any)._context;
      // Don't deduplicate because this legitimately causes bugs
      // and nobody should be using this in existing code.
      if (realContext.Consumer === Context) {
        console.error(
          'Calling useContext(Context.Consumer) is not supported, may cause bugs, and will be ' +
          'removed in a future major release. Did you mean to call useContext(Context) instead?',
        );
      } else if (realContext.Provider === Context) {
        console.error(
          'Calling useContext(Context.Provider) is not supported. ' +
          'Did you mean to call useContext(Context) instead?',
        );
      }
    }
  }
  return dispatcher.useContext(Context, unstable_observedBits);
}
           

示例

React Hook 使用詳解

解決函數元件狀态共享問題

避免了層層傳遞

  1. 其他
  • useDebugValue 給hook 加一個調試專用的名字
function useMyCount(num) {
  const [ count, setCount ] = useState(0);

  useDebugValue('myHook');

  const myCount = () => {
    setCount(count + 2);
  }

  return [ count, myCount ];
}

           
React Hook 使用詳解
  • useImperativeHandle 限制ref對應dom執行個體對外暴露的内容
import { React } from '../adaptation'

const { useRef,useImperativeHandle,forwardRef } = React

const TestImperative =  forwardRef((props, ref) => {
  const myRef = useRef(null)

  useImperativeHandle(ref, () => ({
    focus: () => {
      myRef.current.focus()
    }
  }))

  return <div>
    <input ref={myRef}/>
  </div>
})

export default TestImperative
           
import { React } from '../adaptation';
// import TestMemo from './TestComponents/TestMemo'
import TestImperative from './TestUseImperative';

const { useEffect, useRef } = React

function TestUseImperativeInApp() {
  const myRef = useRef(null)

  useEffect(() => {
    console.log(myRef)
    myRef.current.focus()
  }, [])

  return <div className="App">
    <TestImperative ref={myRef}/>
  </div>
}

export default TestUseImperativeInApp;

           

運作結果

React Hook 使用詳解
  • useTransition 元件延遲過渡鈎子
  • useDeferredValue 監聽狀态,延遲過度更新

四:常見問題

  1. 為什麼不能在條件、嵌套或者循環語句中定義hook

這是hook連結清單的基礎結構:

type Hooks = {
  memoizedState: any, 
  baseState: any, 
  baseUpdate: Update<any> | null
  queue: UpdateQueue<any> | null
  next: Hook | null, // link 到下一個 hooks,通過 next 串聯每一 hooks
}


           

僞代碼描述代碼和hook資料結構的對應關系:

React Hook 使用詳解

hook是初始化的時候按照連結清單的方式逐行設定next,如果中途插入了條件語句,第一次初始化生成的連結清單順序 跟更新時候的實際useState定義的順序可能會不一緻,導緻bug出現。

  1. 擁有Hook的函數元件是否可以完全替代Class元件?

    “官方推薦使用hook來編寫元件”

  2. hook 能模拟class 元件的所有生命周期嗎?參考下圖:
class元件 Hooks元件
constructor useState的時候
getDerivedStateFromProps useEffect 配置依賴的變量,會自定按照依賴的新舊對比控制更新
shouldComponentUpdate React.memo 緩存render的值,配置依賴
render 函數傳回的内容
componentDidMount useEffect 依賴配置成空數組
componentDidUpdate useEffect 通過useRect 緩存一個記錄第一次更新的變量來控制隻在更新的時候執行
componentWillUnmount useEffect第一個參數傳回一個函數,這個函數将在unmount的時候執行