React Hooks 深入系列 —— 設計模式

本文是 React Hooks 深入系列的後續。此篇詳細介紹了 Hooks 相對 class 的優勢所在, 並介紹了相關 api 的設計思想, 同時對 Hooks 如何對齊 class 的生命週期鉤子作了闡述。

<!--more-->

React Logo 與 Hooks

React 的 logo 是一個原子圖案, 原子組成了物質的表現。類似的, React 就像原子般構成了頁面的表現; 而 Hooks 就如夸克, 其更接近 React 本質的樣子, 但是直到 4 年後的今天才被真正設計出來。 —— Dan in React Conf(2018)

why Hooks?

一: 多個組件間邏輯複用: 在 Class 中使用 React 不能將帶有 state 的邏輯給單獨抽離成 function, 其只能通過嵌套組件的方式來解決多個組件間邏輯複用的問題, 基於嵌套組件的思想存在 HOCrender props 兩種設計模式。但是這兩種設計模式是否存在缺陷呢?

  • 嵌套地獄, 當嵌套層級過多後, 數據源的追溯會變得十分困難, 導致定位 bug 不容易; (hoc、render props)
  • 性能, 需要額外的組件實例存在額外的開銷; (hoc、render props)
  • 命名重複性, 在一個組件中同時使用多個 hoc, 不排除這些 hoc 裏的方法存在命名衝突的問題; (hoc)

二: 單個組件中的邏輯複用: Class 中的生命週期 componentDidMountcomponentDidUpdate 甚至 componentWillUnMount 中的大多數邏輯基本是類似的, 必須拆散在不同生命週期中維護相同的邏輯對使用者是不友好的, 這樣也造成了組件的代碼量增加。

三: Class 的其它一些問題: 在 React 使用 Class 需要書寫大量樣板, 用戶通常會對 Class 中 Constructor 的 bind 以及 this 的使用感到困惑; 當結合 class 與 TypeScript 一起使用時, 需要對 defaultValue 做額外聲明處理; 此外 React Team 表示 Class 在機器編譯優化方面也不是很理想。

useState 返回的值爲什麼是數組而非對象?

原因是數組的解構比對象更加方便, 可以觀察以下兩種數據結構解構的差異。

返回數組時, 可以直接解構成任意名字。

[name, setName] = useState('路飛')
[age, setAge] = useState(12)

返回對象時, 卻需要多一層的命名。

{value: name, setValue: setName} = useState('路飛')
{value: name, setValue: setName} = useState(12)

Hooks 傳遞的設計

Hooks 是否可以設計成在組件中通過函數傳參來使用? 比如進行如下調用?

const SomeContext = require('./SomeContext)

function Example({ someProp }, hooks) {
  const contextValue = hooks.useContext(SomeContext)
  return <div>{someProp}{contextValue}</div>
}

使用傳遞的劣勢是會出現冗餘的傳遞。(可以聯想 context 解決了什麼)

Hooks 與 Class 中調用 setState 有不同的表現差異麼?

Hooks 中的 setState 與 Class 中最大區別在於 Hooks 不會對多次 setState 進行合併操作。如果要執行合併操作, 可執行如下操作:

setState(prevState => {
  return { ...prevState, ...updateValues }
})

此外可以對 class 與 Hooks 之間 setState 是異步還是同步的表現進行對比, 可以先對以下 4 種情形 render 輸出的個數進行觀察分析:

是否能使用 React Hooks 替代 Redux

在 React 16.8 版本之後, 針對不是特別複雜的業務場景, 可以使用 React 提供的 useContextuseReducer 實現自定義簡化版的 redux, 可見 todoList 中的運用。核心代碼如下:

import React, { createContext, useContext, useReducer } from "react"

// 創建 StoreContext
const StoreContext = createContext()

// 構建 Provider 容器層
export const StoreProvider = ({reducer, initialState, children}) => {
  return (
    <StoreContext.Provider value={useReducer(reducer, initialState)}>
      {children}
    </StoreContext.Provider>
  )
}

// 在子組件中調用 useStoreContext, 從而取得 Provider 中的 value
export const useStoreContext = () => useContext(StoreContext)

但是針對特別複雜的場景目前不建議使用此模式, 因爲 context 的機制會有性能問題。具體原因可見 react-redux v7 回退到訂閱的原因

Hooks 中如何獲取先前的 props 以及 state

React 官方在未來很可能會提供一個 usePrevious 的 hooks 來獲取之前的 props 以及 state。

usePrevious 的核心思想是用 ref 來存儲先前的值。

function usePrevous(value) {
  const ref = useRef()
  useEffect(() => {
    ref.current = value
  })
  return ref.current
}

Hooks 中如何調用實例上的方法

在 Hooks 中使用 useRef() 等價於在 Class 中使用 this.something。

/* in a function */
const X = useRef()
X.current // can read or write

/* in a Class */
this.X    // can read or write
Is there something like instance variables

Hooks 中 getDerivedStateFromProps 的替代方案

React 暗器百解 中提到了 getDerivedStateFromProps 是一種反模式, 但是極少數情況還是用得到該鉤子, Hooks 沒有該 api, 那其如何達到 getDerivedStateFromProps 的效果呢?

function ScrollView({row}) {
  const [isScrollingDown, setISScrollingDown] = setState(false)
  const [prevRow, setPrevRow] = setState(null)

  // 核心是創建一個 prevRow state 與父組件傳進來的 row 進行比較
  if (row !== prevRow) {
    setISScrollingDown(prevRow !== null && row > prevRow)
    setPrevRow(row)
  }

  return `Scrolling down ${isScrollingDown}`
}

Hooks 中 forceUpdate 的替代方案

可以使用 useReducer 來 hack forceUpdate, 但是儘量避免 forceUpdate 的使用。

const [ignored, forceUpdate] = useReduce(x => x + 1, 0)

function handleClick() {
  forceUpdate()
}

Hooks 中 shouldComponentUpdate 的替代方案

在 Hooks 中可以使用 useMemo 來作爲 shouldComponentUpdate 的替代方案, 但 useMemo 只對 props 進行淺比較。

React.useMemo((props) => {
  // your component
})

useMemo 與 useCallback 的區別

useMemo(() => <component />) 等價於 useCallback(<component />)
  • useCallback: 一般用於緩存函數
  • useMemo: 一般用於緩存組件

依賴列表中移除函數是否是安全的?

通常來說依賴列表中移除函數是不安全的。觀察如下 demo

const { useState, useEffect } = React

function Example({ someProp }) {
  function doSomething() {
    console.log(someProp) // 這裏只輸出 1, 點擊按鈕的 2 並沒有輸出。
  }

  useEffect(
    () => {
      doSomething()
    },
    [] // 🔴 這是不安全的, 因爲在 doSomething 函數中使用了 someProps 屬性
  )

  return <div>example</div>
}

export default function() {
  const [value, setValue] = useState(1)
  return (
    <>
      <Example someProp={value} />
      <Button onClick={() => setValue(2)}>button</Button>
    </>
  )
}

在該 demo 中, 點擊 button 按鈕, 並沒有打印出 2。解決上述問題有兩種方法。

方法一: 將函數放入 useEffect 中, 同時將相關屬性放入依賴項中。因爲在依賴中改變的相關屬性一目瞭然, 所以這也是首推的做法。

function Example({ someProp }) {
  useEffect(
    () => {
      function doSomething() {
        console.log(someProp)
      }
      doSomething()
    },
    [someProps] // 相關屬性改變一目瞭然
  )

  return <div>example</div>
}

方法二: 把函數加入依賴列表中

function Example({ someProp }) {
  function doSomething() {
    console.log(someProp)
  }

  useEffect(
    () => {
      doSomething()
    },
    [doSomething]
  )

  return <div>example</div>
}

方案二基本上不會單獨使用, 它一般結合 useCallback 一起使用來處理某些函數計算量較大的函數。

function Example({ someProp }) {
  const doSomething = useCallback(() => {
    console.log(someProp)
  }, [someProp])

  useEffect(
    doSomething(),
    [doSomething]
  )

  return <div>example</div>
}

如何避免重複創建昂貴的對象

  • 方法一: 使用 useState 的懶初始化, 用法如下
const [value, setValue] = useState(() => createExpensiveObj)
lazy-initial-state;
  • 方法二: 使用自定義 useRef 函數
function Image(props) {
  const ref = useRef(null)

  function getExpensiveObj() {
    if (ref.current === null) {
      ref.current = ExpensiveObj
    }

    return ref.current
  }

  // if need ExpensiveObj, call getExpensiveObj()
}

相關資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章