React爲何須要Hook

自從React 16.8發佈Hook以後,筆者已經在實際項目中使用Hook快一年了,雖然Hook在使用中存在着一些坑,可是總的來講它是一個很好的功能,特別是在減小模板代碼和提升代碼複用率這些方面特別有用。爲了讓更多的人瞭解和使用Hook,我決定寫一系列和Hook相關的文章,本篇文章就是這個系列的第一篇,主要和你們聊一下React爲何須要Hookjavascript

Hook解決的問題

Component非UI邏輯複用困難

對於React或者其它的基於Component的框架來講,頁面是由一個個UI組件構成的。獨立的組件能夠在同一個項目中甚至不一樣項目中進行復用,這十分有利於前端開發效率的提升。但是除了UI層面上的複用,一些狀態相關(stateful)或者反作用相關(side effect)的非UI邏輯在不一樣組件之間複用起來卻十分困難。對於React來講,你可使用高階組件(High-order Component)或者renderProps的方法來複用這些邏輯,但是這兩種方法都不是很好,存在各類各樣的問題。若是你以前沒有複用過這些非UI邏輯的話,咱們能夠先來看一個高階組件的例子。html

假如你在開發一個社交App的我的詳情頁,在這個頁面中你須要獲取並展現當前用戶的在線狀態,因而你寫了一個叫作UserDetail的組件:前端

class UserDetail extends React.Component {
  state = {
    isOnline: false
  }

  handleUserStatusUpdate = (isOnline) => {
    this.setState({ isOnline })
  }

  componentDidMount() {
    // 組件掛載的時候訂閱用戶的在線狀態
    userService.subscribeUserStatus(this.props.userId, this.handleUserStatusUpdate)
  }

  componentDidUpdate(prevProps) {
    // 用戶信息發生了變化
    if (prevProps.userId != this.props.userId) {
      // 取消上一個用戶的狀態訂閱
      userService.unSubscribeUserStatus(this.props.userId, this.handleUserStatusUpdate)
      // 訂閱下一個用戶的狀態
      userService.subscribeUserStatus(this.props.userId, this.handleUserStatusUpdate)
    }
  }

  componentWillUnmount() {
    // 組件卸載的時候取消狀態訂閱
    userService.unSubscribeUserStatus(this.props.userId, this.handleUserStatusUpdate)
  }

  render() {
    return (
      <UserStatus isOnline={this.state.isOnline}> ) } } 複製代碼

從上面的代碼能夠看出其實在UserDetail組件裏面維護用戶狀態信息並非一件簡單的事情,咱們既要在組件掛載和卸載的時候訂閱和取消訂閱用戶的在線狀態,並且還要在用戶id發生變化的時候更新訂閱內容。所以若是另一個組件也須要用到用戶在線狀態信息的話,做爲一個優秀如你的程序員確定不想簡單地對這部分邏輯進行復制和粘貼,由於重複的代碼邏輯十分不利於代碼的維護和重構。接着讓咱們看一下如何使用高階組件的方法來複用這部分邏輯:java

// withUserStatus.jsx
const withUserStatus = (DecoratedComponent) => {
  class WrapperComponent extends React.Component {
   state = {
      isOnline: false
    }

    handleUserStatusUpdate = (isOnline) => {
      this.setState({ isOnline })
    }

    componentDidMount() {
      // 組件掛載的時候訂閱用戶的在線狀態
      userService.subscribeUserStatus(this.props.userId, this.handleUserStatusUpdate)
    }

    componentDidUpdate(prevProps) {
      // 用戶信息發生了變化
      if (prevProps.userId != this.props.userId) {
        // 取消上一個用戶的狀態訂閱
        userService.unSubscribeUserStatus(this.props.userId, this.handleUserStatusUpdate)
        // 訂閱下一個用戶的狀態
        userService.subscribeUserStatus(this.props.userId, this.handleUserStatusUpdate)
      }
    }

    componentWillUnmount() {
      // 組件卸載的時候取消狀態訂閱
      userService.unSubscribeUserStatus(this.props.userId, this.handleUserStatusUpdate)
    }

    render() {
      return <DecoratedComponent isOnline={this.stateIsOnline} {...this.props} /> } } return WrapperComponent } 複製代碼

在上面的代碼中咱們定義了用來獲取用戶在線狀態的高階組件,它維護了當前用戶的在線狀態信息並把它做爲參數傳遞給被裝飾的組件。接着咱們就可使用這個高階組件來重構UserDetail組件的代碼了:react

import withUserStatus from 'somewhere'

class UserDetail {
  render() {
    return <UserStatus isOnline={this.props.isOnline}> } } export default withUserStatus(UserDetail) 複製代碼

咱們能夠看到使用了withUserStatus高階組件後,UserDetail組件的代碼一會兒變得少了不少,如今它只須要從父級組件中獲取到isOnline參數進行展現就好。並且這個高階組件能夠套用在其它任何須要獲取用戶在線狀態信息的組件上,你不再須要在前端維護同樣的代碼了。git

這裏要注意的是上面的高階組件封裝的邏輯和UI展現沒有太大關係,它維護的是用戶在線狀態信息的獲取和更新這些和外面世界交互的side effect,以及用戶狀態的存儲這些和組件狀態相關的邏輯。雖然看起來彷佛代碼很優雅,不過使用高階組件來封裝組件的這些邏輯其實會有如下的問題:程序員

  • 高階組件的開發對開發者不友好:開發者(特別是初級開發者)須要花費一段時間才能搞懂其中的原理而且適應它的寫法。若是你使用高階組件已經好久了,你看到這個說法可能會有些不覺得然。但是我相信你在最開始接觸高階組件的時候確定也花了一段時間才能搞懂它的原理,並且從上面的例子來看高階組件實際上是十分笨重的。試想一下,某天你的項目來了一個React新手,估計他也得花費一段時間才能理解你寫的那些高階組件代碼吧。
  • 高階組件之間組合性差:使用太高階組件的同窗必定試過因爲要爲組件添加不一樣的功能,咱們要爲同一個組件嵌套多個高階組件,例如這樣的代碼:withAuth(withRouter(withUserStatus(UserDetail)))。這種嵌套寫法的高階組件可能會致使不少問題,其中一個就是props丟失的問題,例如withAuth傳遞給UserDetail的某個prop可能在withUserStatus組件裏面丟失或者被覆蓋了。若是你使用的高階組件都是本身寫的話還好,由於調試和修改起來都比較簡單,若是你使用的是第三方的庫的話就很頭痛了。
  • 容易發生wrapper hell:這個問題在上面嵌套多重高階組件的時候就會出現,具體會形成咱們在React Devtools查看和調試某個組件的時候十分困難。咱們能夠看幅圖片來感覺一下:
    這真是高階組件一時爽,出問題就火葬場的感受有沒有。

和高階組件相似,renderProps也會存在一樣的問題。基於這些緣由,React須要一個新的用來複用組件之間非UI邏輯的方法,因此Hook就這麼誕生了。總的來講,Hook相對於高階組件和renderProps在複用代碼邏輯方面有如下的優點:github

  • 寫法簡單:每個Hook都是一個函數,所以它的寫法十分簡單,並且開發者更容易理解。
  • 組合簡單:Hook組合起來十分簡單,組件只須要同時使用多個hook就可使用到它們全部的功能。
  • 容易擴展:Hook具備很高的可擴展性,你能夠經過自定義Hook來擴展某個Hook的功能。
  • 沒有wrapper hell:Hook不會改變組件的層級結構,也就不會有wrapper hell問題的產生。

除了用來替代難用的HOC和renderProps來解決組件非UI邏輯複用的問題以外,其實Hook還解決了如下這些問題。編程

組件的生命週期函數不適合side effect邏輯的管理

在上面UserDetail組件中咱們將獲取用戶的在線狀態這個side effect的相關邏輯分散到了componentDidMountcomponentWillUnmountcomponentDidUpdate三個生命週期函數中,這些互相關聯的邏輯被分散到不一樣的函數中會致使bug的發生和產生數據不一致的狀況。除了這個,咱們還可能會在組件的同一個生命週期函數放置不少互不關聯的side effect邏輯。舉個例子,若是咱們想在用戶查看某個用戶的詳情頁面的時候將瀏覽器當前標籤頁的title改成當前用戶名的話,就須要在組件的componentDidMount生命週期函數裏面添加document.title = this.props.userName這段代碼,但是這段代碼和以前訂閱用戶狀態的邏輯是互不關聯的,並且隨着組件的功能變得愈來愈複雜,這些不關聯而又放在一塊兒的代碼只會變得愈來愈多,因而你的組件逐漸變得難以測試。因而可知Class Component的生命週期函數並不適合用來管理組件的side effect邏輯。瀏覽器

那麼這個問題Hook又是如何解決的呢?因爲每一個Hook都是一個函數,因此你能夠將和某個side effect相關的邏輯都放在同一個函數(Hook)裏面(useEffect Hook)。這種作法有不少好處,首先關聯的代碼都放在一塊兒,能夠十分方便代碼的維護,其次實現了某個side effect的Hook還能夠被不一樣的組件進行復用來提升開發效率。舉個例子,咱們就能夠將改變標籤頁title的邏輯封裝在一個自定的Hook中,若是其它組件有相同邏輯的話就可使用這個Hook了:

// 自定義Hook
function useTabTitle(title) {
  React.useEffect(() => {
    document.title = title
  }, [title])
}

// UserDetail中使用useTabTitle Hook
function UserDetail = (props) => {
  useTabTitle(props.userName)
  ...
}
複製代碼

這個複用side effect的功能實際上是一個十分強大的功能,你能夠檢查一下你如今寫的項目代碼,確定有不少組件的side effect是能夠封裝成Hook的。封裝成Hook的side effect不只僅能夠在某一個項目中使用,還能夠在不一樣項目中複用,這對咱們的開發效率確定會有很大的提高。

不友好的Class Component

其實Class Component除了生命週期函數不適合side effect的管理以外,還有一些其它的問題。

首先Class Component對開發者不友好。若是你要使用Class Component首先你得理解JS裏面的this是怎麼使用的,它的使用方法其實和其餘語言有很大的區別。因爲JS自己的緣由,在Class Component中你要手動爲註冊的event listener綁定this,否則就會報this is undefined的錯誤,早期的React玩家確定體驗過每一個事件監聽函數都要手動綁定this的酸爽感受,乏味並且容易引起bug,這個問題直到class properties出來以後纔有所改善。

class UserDetail extends React.Component {
  constructor(props) {
    super(props)
    this.handlerUserStatusUpdate = this.handleUserStatusUpdate.bind(this)
    ...
  }
}
複製代碼

除了對開發者不友好,Class Component對機器也很不友好。例如Class Component的生命週期函數很難被minified。其次,Class Component的存在可能會阻礙React後面的發展。舉個例子,隨着新的理念 - Compiler as Framework的興起,一些諸如Svelte, AngularGlimmer的框架將框架的概念放到了編譯時以去除production code裏面的runtime代碼來加快應用的首屏加載速度,這個方案已經開始被逐漸採納了,並且將來有可能會成爲潮流。若是你們不是很瞭解Compiler as Framework理念的話,能夠看個人另一篇文章:Svelte 3 初學者徹底指南。React已經存在了5年,它若是想要繼續存在多五年的話也要跟上這個潮流,出於這個緣由,React團隊和Prepack團隊進行了一些和Compiler as Framework相關的嘗試,並且就目前實驗的結果來講這個思路有很大的想象空間。不過在這個過程當中React的開發者也發現了一個嚴重的問題,那就是開發者可能會以一種很是規的模式來使用Class Component,而這些模式會下降這個方案帶來的優化效果。

所以React要想獲得進一步的發展的話,就必須讓開發者更多地使用Function Component而不是Class Component。而開發者偏向於使用Class Component而不是Function Component的一個主要緣由是Function Component沒有狀態管理和生命週期函數等功能。Hook出來後這個問題就不存在了,由於開發者可使用useState Hook來在Function Component使用state以及useEffect Hook來實現一些和生命週期函數相似的功能。最重要的是,React將全部複雜的實現都封裝在框架裏面了,開發者無需學習函數式編程和響應式編程的概念也能夠很好地使用Hook來進行開發。

總結

本篇文章我主要論述了React爲啥要有Hook,總的來講是如下三個緣由:

  • Component非UI邏輯複用困難。
  • 組件的生命週期函數不適合side effect邏輯的管理。
  • 不友好的Class Component。

若是你有其餘的補充或者以爲我有什麼地方說得不對的話能夠在評論區和我一塊兒探討,在後面一篇文章中我將會爲你們深刻介紹一些經常使用的Hook。

參考文獻

我的技術動態

文章始發於個人我的博客

歡迎關注公衆號進擊的大蔥一塊兒學習成長

相關文章
相關標籤/搜索