React Hooks 入門記錄

Actoress發表於2020-04-04

什麼是 Hooks

以往,React 元件都是通過 class 的形式來編寫,只有無狀態元件才可以用函式來編寫。Hooks 就允許我們在函式元件中使用預定義函式,來標記狀態和元件生命週期,這使得所有元件都可以使用函式來編寫。

類元件的劣勢

    1. 狀態邏輯難複用 缺少複用機制 渲染屬性和高階元件導致層級冗餘
    1. 趨向複雜難以維護 生命週期函式混雜不相干邏輯 想幹邏輯分散在不同生命週期
    1. this指向困擾 行內函數過度建立新控制程式碼 類成員函式不能保證this

優化類元件的三大問題

  • 函式元件無 this 問題
  • 自定義 Hook 方便複用狀態邏輯
  • 副作用的關注點分離

Hooks 使用法則

官方文件: https://reactjs.org/docs/hooks-rules.html

  • 僅在頂層呼叫 Hooks 函式

整個 Hooks 函式都依賴呼叫順序,這樣 React 才能在不同的渲染週期中把相同的邏輯關聯起來,一旦 Hooks 函式不在頂層呼叫,那麼很有可能在元件的不同渲染週期中,他們的呼叫順序發生了變化,進而導致變數混亂等錯誤,為了儘量規避這些問題,所以儘量把 Hooks 放在最頂層。

  • 僅在函式元件及 函式元件 中呼叫 Hooks 函式,不能放在普通函式及類元件中

Hooks 函式必須是use開頭

Hooks 常見問題

生命週期函式如何對映到 Hooks ?

getDerivedStateFromError() 這個處理錯誤的函式,暫時無法在 Hooks 中實現,由此看出 Hooks 暫時無法代替類元件

function App () {
  useEffect(() => {
    // componentDidMount
    return () => {
      // componentWillUnmount
    }
  }, [])
  let renderCouter = useRef(0) // 通過 ref 來保持渲染計數
  renderCouter.current++

  useEffect(() => {
    if (renderCouter > 1) {
      // componentDidUpdate
    }
  })
}
複製程式碼

類例項成員變數如何對映到 Hooks ?

使用 useRef(0),同步到 current 中

Hooks 中如何獲取歷史 props 和 state

使用 useRef(0),同步到 current 中

如何強制更新一個 Hooks 元件

建立一個不參與渲染的 state 的值,通過修改這個 state 的值來實現強制渲染

function Counter () {
  const [updater, setUpdater] = useState(0)
  function forceUpdate () {
    setUpdater(updater => updater + 1)
  }
}
複製程式碼

使用 eslint-plugin-react-hooks 防止程式碼出錯

1.安裝

npm install eslint-plugin-react-hooks -D
複製程式碼

2.在package.json 中配置

// package.json
...
"eslintConfig": {
    "extends": "react-app",
    "plugins": [
        "react-hooks"
    ],
    "rules": {
        "react-hooks/rules-of-hooks": "error"
    }
},
...
複製程式碼

簡單使用 useState

使用傳統類元件

import React, { Component } from 'react'
class ClassComponent extends Component {
  state = {
    count: 0
  }
  render () {
    const { count } = this.state
    return (
      <button onClick={() => {this.setState({ count: count + 1 })}}>
        ClassComponent Click Count ({count})
      </button>
    )
  }
}
複製程式碼

使用 Hooks

import React, { Component, useState } from 'react'
function HooksComponent () {
  // useState 傳入 count 的預設值
  // count 獲取值
  // setCount 設定值
  const [count, setCount] = useState(0)
  return (
    <button onClick={ () => { setCount(count + 1)} }>
        HooksComponent Click Count ({count})
    </button>
  )
}
複製程式碼

通過 props 設定預設值

import React, { Component, useState } from 'react'
function HooksComponetDefault (props) {
  // 使用 props 設定預設值,useState 傳入的函式只會被呼叫一次
  const [count, setCount] = useState(() => {
    return props.defaultCount || 0
  })
  return (
    <button onClick={ () => { setCount(count + 1)} }>
        HooksComponent Click Count ({count})
    </button>
  )
}
複製程式碼

使用 Effect Hooks

常見副作用:

  • 繫結事件
  • 網路請求
  • 訪問DoM

常見的副作用時機:

  • Mount之後(componentDidMount)
  • Update之後(componentDidUpdate)
  • Unmount之前(componentWillUnmount)

以上的時機在傳統類元件中,是在生命週期中解決,在 hooks 中的解決方案:useEffect()

關於 useEffect()

userEffect()標準上是在元件渲染(render)之後呼叫,並且根據自定義狀態來決定是否呼叫,函式元件第一次渲染後的呼叫,就相當於componentDidMount(),後面的呼叫都相當於componentDidUpdate()

userEffect()可以返回一個回撥函式,這個回撥函式的執行時機跟userEffect()執行時機掛鉤。這個回撥函式的主要作用是 清除上一次副作用所遺留下來的狀態

useEffect() 的引數

【第一個引數】第一個引數為一個函式,返回值為回撥函式。回撥函式在檢視被銷燬的時候觸發,1.元件重渲染,2.元件被解除安裝

【第二個引數】如果不填,則每次渲染後都會執行。第二個引數為一個陣列,只有陣列的每一項都不變的情況下,userEffect()才不會執行,因此,傳一個空陣列的話,該 useEffect 就只會在第一次呼叫一次

簡單使用

function HooksCompinentUseEffect () {
  const [ count, setCount ] = useState(0)
  const [ size, setSize ] = useState({
    width: document.documentElement.clientWidth,
    height: document.documentElement.clientHeight
  })
  const onWindowResize = () => {
    setSize({
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight
    })
  }

  // useEffect 第一個引數為一個函式
  // 這裡類似 componentDidount 和 componentDidUpdate
  useEffect(() => {
    document.title = count
  })

  // useEffect 第一個引數為一個函式,第二個引數為一個陣列,只有陣列的每一項都不變的情況下,userEffect()才不會執行
  // 這裡類似 componentDidMount 和 componentWillUnmount
  useEffect(() => {
    window.addEventListener('resize', onWindowResize, false)
    // 回撥函式在檢視被銷燬的時候觸發,1.元件重渲染,2.元件被解除安裝
    return () => {
      window.removeEventListener('resize', onWindowResize, false)
    }
  },  [])
  return (
    <div>
      <button onClick={() => { setCount(count + 1)} }>
        HooksCompinentUseEffect Click Count ({count})
      </button>
      W: {size.width} | H: {size.height}
    </div> 
  )
}
複製程式碼

Hooks環境下的Context

不要濫用 context,因為會破壞元件的獨立性

import React, { useState, createContext, useContext } from 'react'
// 使用 context
const CountContext = createContext()
function HooksContextProvider () {
  const [ count, setCount ] = useState(0)
  return (
    <div>
      <button onClick={() => { setCount(count + 1) }}>HooksContextProvider {count}</button>
      {/* value 中設定的是需要共享的值 */}
      <CountContext.Provider value={count}>
        <HooksContextConsumer />
      </CountContext.Provider>
    </div>
  )
}
// 使用 useContext 呼叫,引數為建立的 Provider
function HooksContextConsumer () {
  const count = useContext(CountContext)
  return (
    <div>
      HooksContextConsumer: <b>{count}</b>
    </div>
  )
}
複製程式碼

使用 useMemo 和 memo

useMemo 和 memo 的區別,memo針對的是一個元件的渲染是否重複執行,useMemo定義了一段函式是否重複執行。本質都是利用同樣的演算法,來判斷依賴是否發生改變,進而決定是否觸發特定邏輯,同樣都用來做效能優化。

useMemo的引數

第一個引數為要執行的邏輯函式

第二個引數為這個函式鎖依賴的變數組成的陣列,如果不傳則 useMemo 的邏輯每次都執行,如果傳入空陣列就執行執行一次

與 useEffect 的差異

兩個函式的呼叫時機不同,useEffect() 執行的是副作用,所以一定是在渲染完成之後執行,而useMemo() 是需要有返回值,返回值直接參與渲染,因為 useMemo() 是在渲染期間完成的。兩者存在一前一後的區別

useMemo 簡單使用

import React, { useState, useMemo } from 'react'
// 使用 memo
function HookMemoParent () {
  const [ count, setCount ] = useState(0)
  // 第一個引數為要執行的邏輯函式
  // 第二個引數為這個函式鎖依賴的變數組成的陣列,如果不傳則 useMemo 的邏輯每次都執行,如果傳入空陣列就執行執行一次
  // 當第二個引數發生變化時,就會觸發邏輯,跟陣列是什麼值無關
  const doubleCount = useMemo(() => {
    return count * 2
  }, [count])

  // 根據第二個引數,count < 3 時,保持不變,不會重新計算
  // 當陣列中的 bool 值發生改變時,就會重新渲染,false => true => false,所以 boolCount 會重新渲染兩次
  const boolCount = useMemo(() => {
    return count * 3
  }, [count === 3])

  return (
    <div>
      <button onClick={() => {setCount(count + 1)}}>HookMemoParent: {count},doubleCount: {doubleCount}, boolCount: {boolCount}</button>
      <HooksMemoChild count={doubleCount}></HooksMemoChild>
    </div>
  )
}
function HooksMemoChild (props) {
  return (
    <div>{props.count}</div>
  )
}
複製程式碼

useMemo 和 useCallback

當需要用 useMemo 返回一個函式時,可以使用 useCallback 代替,可以替代上一層函式。使用 useCallback 不能解決阻止建立新的函式,因為每次元件的函式執行都會建立新的函式,但是建立的這個函式不一定能夠被返回,很可能會被直接棄用。useCallback解決的是傳入子元件的函式引數過渡變化,導致子元件過渡渲染的問題。實際上 useCallback 只是 useMemo 的一種簡寫。

【特別提醒】使用 useMemo 和 useCallback 時,當依賴變化時,useMemo 和 useCallback 一定重新執行。但是,當依賴沒變化時,不能保證它們一定不執行,也可能重新執行,這是考慮記憶體優化的結果,react 官方的文件中也沒有打包票一定不執行。所以,useMemo 和 useCallback 可以作為一種錦上添花的方案,不可以過渡依賴它們是否重新執行。

useCallback的引數

第一個引數為要執行的邏輯函式

第二個引數為這個函式鎖依賴的變數組成的陣列,如果不傳則 useMemo 的邏輯每次都執行,如果傳入空陣列就執行執行一次

useMemo(() => {
  return () => { console.log("click") }
}, [])
// 上下兩者等價
useCallback(() => {
  console.log("click")
}, [])
複製程式碼

使用 useRef

  • 獲取子元件或者DOM節點的控制程式碼
  • 獲取跨越渲染週期的任意資料

useRef 和 useState 區別

state 的賦值會觸發元件重渲染,但是 ref 不會

【特別用法】

如果在元件中,需要使用上次渲染的資料,可以使用useRef,同步到 current 中

// useRef
function HooksUseRef () {
  const [ count, setCount ] = useState(0)
  // 宣告ref
  const countRef = useRef()
  // 將定時器例項,通步在useRef中,這樣就可以防止每次渲染時重複呼叫
  const timer = useRef()

  // 給子元件傳遞的事件
  const bindChildClick = useCallback(() => {
    console.log('click')
    // 通過 current 獲取 DOM 節點
    console.log(countRef.current)
    // 通過 ref 呼叫子元件的方法
    countRef.current.speak()
  }, [countRef])

  // 使 count 自增
  useEffect(() => {
    timer.current = setInterval(() => {
      setCount(count => count + 1)
    }, 1000)
    return () => {
      cleanup
    }
  }, [])
  // 監測 count 大於10,停止自增
  useEffect(() => {
    if (count >= 10) {
      clearInterval(timer.current)
    }
  })

  return (
    <div>
      <button onClick={() => {setCount(count + 1)}}>HooksUseRef: {count}</button>
      {/* 給類元件的ref屬性賦值 */}
      <HooksUseRefClassChild ref={countRef} count={count} onClick={bindChildClick}></HooksUseRefClassChild>
    </div>
  )
}
// 類元件獲取 ref
class HooksUseRefClassChild extends PureComponent {
  speak () {
    console.log('this is child function: ', this.props.count)
  }
  render () {
    const { props } = this
    return <b onClick={props.onClick}> HooksUseRefClassChild: {props.count}</b>
  }
}
複製程式碼

自定義 Hooks 函式

// 自定義 hooks 函式,一定要使用 use 開頭
function useCount (defaultCount) {
  const [count, setCount] = useState(defaultCount)
  const timer = useRef()
  // 使 count 自增
  useEffect(() => {
    timer.current = setInterval(() => {
      setCount(count => count + 1)
    }, 1000)
    return () => {
      cleanup
    }
  }, [])
  // 監測 count 大於10,停止自增
  useEffect(() => {
    if (count >= 10) {
      clearInterval(timer.current)
    }
  })
  // 這裡的返回值可以自定義,我參考 useState 的返回
  return [count, setCount]
}
// 自定義 hooks 函式,返回 JSX
function useCountJSX (defaultCount) {
  return (
    <b>{defaultCount}</b>
  )
}
function HooksSelfFunciton () {
  const [count, setCount] = useCount(0) // 模仿原生的 useState
  const CountJSX = useCountJSX(count) // 直接返回 JSX
  return (
    <div>
      <button onClick={() => {setCount(count + 1)}}>useCount: {count}</button>
      useCountJSX: {CountJSX}
    </div>
  )
}
複製程式碼

相關文章