關於為什么使用React新特性Hook的一些實踐與淺見


前言

關於Hook的定義官方文檔是這么說的:

Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。

簡單來說,就是在使用函數式組件時能用上state,還有一些生命周期函數等其他的特性。

如果想了解Hook怎么用,官方文檔和阮一峰的React Hooks 入門教程都講得很清楚了,我建議直接看官方文檔和阮大神的文章即可。

本篇博客只講為什么要用React的Hook新特性,以及它解決了什么問題。

為什么使用Hook?

讓我們先看看別人怎么說。

阮大神的文章中給了一個示例代碼:

import React, { Component } from "react";

export default class Button extends Component {
  constructor() {
    super();
    this.state = { buttonText: "Click me, please" };
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() {
    this.setState(() => {
      return { buttonText: "Thanks, been clicked!" };
    });
  }
  render() {
    const { buttonText } = this.state;
    return <button onClick={this.handleClick}>{buttonText}</button>;
  }
}

並且提出:

這個組件類僅僅是一個按鈕,但可以看到,它的代碼已經很"重"了。
真實的 React App 由多個類按照層級,一層層構成,復雜度成倍增長。
再加入 Redux,就變得更復雜。

實際上,上面這個代碼的“重”有部分來源於寫法問題,他可能並沒有“重”,讓我們看看下面這種class寫法:

import React, { Component } from "react";

export default class Button extends Component {
  state = {
    buttonText: "Click me, please"
  }
  handleClick = () => {
    this.setState(() => {
      return { buttonText: "Thanks, been clicked!" };
    });
  }
  render() {
    const { buttonText } = this.state;
    return <button onClick={this.handleClick}>{buttonText}</button>;
  }
}

然后再對比下使用了Hook的函數式組件:

import React, { useState } from "react";

export default function Button() {
  const [buttonText, setButtonText] = useState("Click me,   please");

  function handleClick() {
    return setButtonText("Thanks, been clicked!");
  }

  return <button onClick={handleClick}>{buttonText}</button>;
}

即使是我們簡化過的class寫法,比起Hook的看起來好像也確實“重”了點。

Hook的語法確實簡練了一些,但是這個理由並不是那么充分。

阮大神同時列舉了Redux 的作者 Dan Abramov 總結了組件類的幾個缺點:

  • 大型組件很難拆分和重構,也很難測試。
  • 業務邏輯分散在組件的各個方法之中,導致重復邏輯或關聯邏輯。(這里我認為阮大神寫的可能有點問題,應該是是各個生命周期方法更為准確)
  • 組件類引入了復雜的編程模式,比如 render props 和高階組件。

這三點都是事實,於是有了函數化的組件,但之前的函數化組件沒有state和生命周期,有了Hook那么就可以解決這個痛點。

而且Hook並不只是這么簡單,通過自定義Hook,我們可以將原有組件的邏輯提取出來實現復用。

用useEffect解決生命周期導致的重復邏輯或關聯邏輯

上面舉的幾個缺點,第一點和第三點你可能很容易理解,第二點就不容易理解了,所以我們需要深入到具體的代碼中去理解這句話。

我們看看下面這段代碼:

import React, { Component } from "react";

export default class Match extends Component {
  state={
    matchInfo:''
  }
  componentDidMount() {
    this.getMatchInfo(this.props.matchId)
  }
  componentDidUpdate(prevProps) {
    if (prevProps.matchId !== this.props.matchId) {
      this.getMatchInfo(this.props.matchId)
    }
  }
  getMatchInfo = (matchId) => {
    // 請求后台接口獲取賽事信息
    // ...
    this.setState({
      matchInfo:serverResult // serverResult是后台接口的返回值
    })
  }

  render() {
    const { matchInfo } = this.state
    return <div>{matchInfo}</div>;
  }
}

這樣的代碼在我們的業務中經常會出現,通過修改傳入賽事組件的ID,去改變這個賽事組件的信息。

在上面的代碼中,受生命周期影響,我們需要在加載完畢和Id更新時都寫上重復的邏輯和關聯邏輯。

所以現在你應該比較好理解這句話:業務邏輯分散在組件的各個生命周期方法之中,導致重復邏輯或關聯邏輯

為了解決這一點,React提供了useEffect這個鈎子。

但是在講這個之前,我們需要先了解到React帶來的一個新的思想:同步。

我們在上面的代碼中所做的實際上就是在把組件內的狀態和組件外的狀態進行同步。

所以在使用Hook之前,我們需要先摒棄生命周期的思想,而用同步的思想去思考這個問題。

現在再讓我們看看改造后的代碼:

import React, { Component } from "react";

export default function Match({matchId}) {
  const [ matchInfo, setMatchInfo ] = React.useState('')
  React.useEffect(() => {
    // 請求后台接口獲取賽事信息
    // ...
    setMatchInfo(serverResult) // serverResult是后台接口的返回值
  }, [matchId])
  
  return <div>{matchInfo}</div>;
}

看到這個代碼,再對比上面的代碼,你心中第一反應應該就是:簡單。

React.useEffect接受兩個參數,第一個參數是Effect函數,第二個參數是一個數組。

組件加載的時候,執行Effect函數。

組件更新會去判斷數組中的各個值是否變動,如果不變,那么不會執行Effect函數。

而如果不傳第二個參數,那么無論加載還是更新,都會執行Effect函數。

順便提一句,這里有組件加載和更新的生命周期的概念了,那么也應該是有組件卸載的概念的:

import React, { Component } from "react";

export default function Match({matchId}) {
  const [ matchInfo, setMatchInfo ] = React.useState('')
  React.useEffect(() => {
    // 請求后台接口獲取賽事信息
    // ...
    setMatchInfo(serverResult) // serverResult是后台接口的返回值

    return ()=>{
      // 組件卸載后的執行代碼
    }
  }, [matchId])
  
  return <div>{matchInfo}</div>;
}
}

這個常用於事件綁定解綁之類的。

用自定義Hook解決高階組件

React的高階組件是用來提煉重復邏輯的組件工廠,簡單一點來說就是個函數,輸入參數為組件A,輸出的是帶有某邏輯的組件A+。

回想一下上面的Match組件,假如這個組件是頁面A的首頁頭部用來展示賽事信息,然后現在頁面B的側邊欄也需要展示賽事信息。

問題就在於頁面A的這塊UI需要用div,而頁面B側邊欄的這塊UI需要用到span。

保證今天早點下班的做法是復制A頁面的代碼到頁面B,然后改下render的UI即可。

保證以后早點下班的做法是使用高階組件,請看下面的代碼:

import React from "react";

function hocMatch(Component) {
  return class Match React.Component {
    componentDidMount() {
      this.getMatchInfo(this.props.matchId)
    }
    componentDidUpdate(prevProps) {
      if (prevProps.matchId !== this.props.matchId) {
        this.getMatchInfo(this.props.matchId)
      }
    }
    getMatchInfo = (matchId) => {
      // 請求后台接口獲取賽事信息
    }
    render () {
      return (
        <Component {...this.props} />
      )
    }
  }
}

const MatchDiv=hocMatch(DivUIComponent)
const MatchSpan=hocMatch(SpanUIComponent)

<MatchDiv matchId={1} matchInfo={matchInfo} />
<MatchSpan matchId={1} matchInfo={matchInfo} />

但是實際上有的時候我們的高階組件可能會更復雜,比如react-redux的connect,這就是高階組件的復雜化使用方式。

又比如:

hocPage(
  hocMatch(
    hocDiv(DivComponent)
  )
)

毫無疑問高階組件能讓我們復用很多邏輯,但是過於復雜的高階組件會讓之后的維護者望而卻步。

而Hook的玩法是使用自定義Hook去提煉這些邏輯,首先看看我們之前使用了Hook的函數式組件:

import React, { Component } from "react";

export default function Match({matchId}) {
  const [ matchInfo, setMatchInfo ] = React.useState('')
  React.useEffect(() => {
    // 請求后台接口獲取賽事信息
    // ...
    setMatchInfo(serverResult) // serverResult是后台接口的返回值
  }, [matchId])
  
  return <div>{matchInfo}</div>;
}

然后,自定義Hook:

function useMatch(matchId){
  const [ matchInfo, setMatchInfo ] = React.useState('')
  React.useEffect(() => {
    // 請求后台接口獲取賽事信息
    // ...
    setMatchInfo(serverResult) // serverResult是后台接口的返回值
  }, [matchId])
  return [matchInfo]
}

接下來,修改原來的Match組件

export default function Match({matchId}) {
  const [matchInfo]=useMatch(matchId)
  return <div>{matchInfo}</div>;
}

相比高階組件,自定義Hook更加簡單,也更加容易理解。

現在我們再來處理以下這種情況:

hocPage(
  hocMatch(
    hocDiv(DivComponent)
  )
)

我們的代碼將不會出現這種不斷嵌套情況,而是會變成下面這種:

export default function PageA({matchId}) {
  const [pageInfo]=usePage(pageId)
  const [matchInfo]=useMatch(matchId)
  const [divInfo]=useDiv(divId)

  return <ul>
    <li>{pageInfo}</li>
    <li>{matchInfo}</li>
    <li>{divInfo}</li>
  </ul>
}

是否需要改造舊的class組件?

現在我們了解到了Hook的好,所以就需要去改造舊的class組件。

官方推薦不需要專門為了hook去改造class組件,並且保證將繼續更新class相關功能。

實際上我們也沒有必要專門去改造舊項目中的class組件,因為工作量並不小。

但是我們完全可以在新的項目或者新的組件中去使用它。

總結

Hook是對函數式組件的一次增強,使得函數式組件可以做到class組件的state和生命周期。

Hook的語法更加簡練易懂,消除了class的生命周期方法導致的重復邏輯代碼,解決了高階組件難以理解和使用困難的問題。

然而Hook並沒有讓函數式組件能做到class組件做不到的事情,它只是讓很多事情變得更加簡單而已。

class組件並不會消失,但hook化的函數式組件將是趨勢。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM