React Hooks 深刻不淺出

這個標題可能不太好,但此文章確實不是一篇使用教程,並且也不會覆蓋太多點,建議時間充裕的仍是應該完整地看下 官網文檔javascript

React Hooks 對於部分人來講可能仍是陌生的,但仍是阻止不了它成爲了當前 React 社區裏「最」熱門的一個詞彙。html

一開始瞭解到這個仍是 Dan Abramov 在十月底的時候發了一個推,是一篇文章 Making Sense of React Hooks,建議沒看過的先看下。看完第一感覺就是:React 本就應該是這樣的啊!java

看完這篇文章,但願你能夠從總體上對 Hooks 有個認識,並對其設計哲學有一些理解,但願看的過程不要急,跟着個人思路走。react

若是你想本身跟着文章一塊兒練手,須要把 reactreact-dom 更新到 16.7.0-alpha 及以上,若是配置了 ESLint,記得添加對應的 Plugin

插曲

長期以來不少人會把 Stateless ComponentFunctional Component 混爲一談,我會試着跟他們解釋這不是一回事(不是一個維度),但當時 Functional Component 裏確實沒法使用 state,我不管怎麼解釋都會顯得很無力。難道這冥冥之中都預示着會有相似 React Hooks 的東西出現?npm

React Hooks 的本質

稍微複雜點的項目確定是充斥着大量的 React 生命週期函數(注意,即便你使用了狀態管理庫也避免不了這個),每一個生命週期裏幾乎都承擔着某個業務邏輯的一部分,或者說某個業務邏輯是分散在各個生命週期裏的。編程

而 Hooks 的出現本質是把這種面向生命週期編程變成了面向業務邏輯編程,你不用再去關心本不應關心的生命週期。json

一個 Hooks 演變

咱們先假想一個常見的需求,一個 Modal 裏須要展現一些信息,這些信息須要經過 API 獲取且跟 Modal 強業務相關,要求咱們:redux

  • 由於業務簡單,沒有引入額外狀態管理庫
  • 由於業務強相關,並不想把數據跟組件分開放
  • API 數據會隨機變更,所以須要每次打開 Modal 才獲取最新數據
  • 爲了後期優化,不能夠有額外的組件建立和銷燬

咱們可能的實現以下:api

代碼完整演示地址app

class RandomUserModal extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      user: {},
      loading: false,
    };
    this.fetchData = this.fetchData.bind(this);
  }

  componentDidMount() {
    if (this.props.visible) {
      this.fetchData();
    }
  }

  componentDidUpdate(prevProps) {
    if (!prevProps.visible && this.props.visible) {
      this.fetchData();
    }
  }

  fetchData() {
    this.setState({ loading: true });
    fetch('https://randomuser.me/api/')
      .then(res => res.json())
      .then(json => this.setState({
        user: json.results[0],
        loading: false,
      }));
  }

  render() {
    const user = this.state.user;
    return (
      <ReactModal
        isOpen={this.props.visible}
      >
        <button onClick={this.props.handleCloseModal}>Close Modal</button>
        {this.state.loading ?
          <div>loading...</div>
          :
          <ul>
            <li>Name: {`${(user.name || {}).first} ${(user.name || {}).last}`}</li>
            <li>Gender: {user.gender}</li>
            <li>Phone: {user.phone}</li>
          </ul>
        }
      </ReactModal>
    )
  }
}

咱們抽象了一個包含業務邏輯的 RandomUserModal,該 Modal 的展現與否由父組件控制,所以會傳入參數 visiblehandleCloseModal(用於 Modal 關閉本身)。

爲了實如今 Modal 打開的時候才進行數據獲取,咱們須要同時在 componentDidMountcomponentDidUpdate 兩個生命週期裏實現數據獲取的邏輯,並且 constructor 裏的一些初始化操做也少不了。

其實咱們的要求很簡單:在合適的時候經過 API 獲取新的信息,這就是咱們抽象出來的一個業務邏輯,爲了這個業務邏輯能在 React 里正確工做,咱們須要將其按照 React 組件生命週期進行拆解。這種拆解除了代碼冗餘,還很難複用

下面咱們看看採用 Hooks 改造後會是什麼樣:

完整演示地址

function RandomUserModal(props) {
  const [user, setUser] = React.useState({});
  const [loading, setLoading] = React.useState(false);

  React.useEffect(() => {
    if (!props.visible) return;
    setLoading(true);
    fetch('https://randomuser.me/api/').then(res => res.json()).then(json => {
      setUser(json.results[0]);
      setLoading(false);
    });
  }, [props.visible]);
  
  return (
    // View 部分幾乎與上面相同
  );
}

很明顯地能夠看到咱們把 Class 形式變成了 Function 形式,使用了兩個 State Hook 進行數據管理(類比 constructor),以前 cDMcDU 兩個生命週期裏乾的事咱們直接在一個 Effect Hook 裏作了(若是有讀取或修改 DOM 的需求能夠看 這裏)。作了這些,最大的優點是代碼精簡,業務邏輯變的緊湊,代碼行數也從 50+ 行減小到 30+ 行。

Hooks 的強大之處還不只僅是這個,最重要的是這些業務邏輯能夠隨意地的的抽離出去,跟普通的函數沒什麼區別(僅僅是看起來沒區別),因而就變成了能夠複用的自定義 Hook。具體能夠看下面的進一步改造:

完整演示地址

// 自定義 Hook
function useFetchUser(visible) {
  const [user, setUser] = React.useState({});
  const [loading, setLoading] = React.useState(false);
  
  React.useEffect(() => {
    if (!visible) return;
    setLoading(true);
    fetch('https://randomuser.me/api/').then(res => res.json()).then(json => {
      setUser(json.results[0]);
      setLoading(false);
    });
  }, [visible]);
  return { user, loading };
}

function RandomUserModal(props) {
  const { user, loading } = useFetchUser(props.visible);
  
  return (
    // 與上面相同
  );
}

這裏的 useFetchUser 爲自定義 Hook,它的地位跟自帶的 useState 等比也沒什麼區別,你能夠在其它組件裏使用,甚至在這個組件裏使用兩次,它們會自然地隔離開。

業務邏輯複用

這裏說的業務邏輯複用主要是須要跨生命週期的業務邏輯。單單按照組件堆積的形式組織代碼雖然也能夠達到各類複用的目的,可是會致使組件很是複雜,數據流也會很亂。組件堆積適合 UI 佈局,可是不適合邏輯組織。爲了解決這些問題,在 React 發展過程當中,產生了不少解決方案,我認知裏常見的有如下幾種:

Mixins

壞處遠遠大於帶來的好處,由於如今已經再也不支持,很少說,能夠看看這篇文章:Mixins Considered Harmful

Class Inheritance

官方 很不推薦此作法,實際上我也沒真的看到有人這麼作。

High-Order Components (HOC)

React 高階組件 在封裝業務組件上簡直是屢試不爽,它的實現是把本身做爲一個函數,接受一個組件,再返回一個組件,這樣它能夠統一處理掉一些業務邏輯並達到複用目的。

比較常見的一個就是 react-redux 裏的 connect 函數:

(圖片來自 這裏

可是它也被不少人吐槽嵌套問題:

(圖片來自 這裏

Render Props

Render Props 其實很常見,好比 React Context API

class App extends React.Component {
  render() {
    return (
      <ThemeProvider>
        <ThemeContext.Consumer>
          {val => <div>{val}</div>}
        </ThemeContext.Consumer>
      </ThemeProvider>
    )
  }
}

它的實現思路很簡單,把原來該放「組件」的地方,換成了回調,這樣當前組件裏就能夠拿到子組件的狀態並使用。

可是,一樣這會產生 Wrapper Hell 問題:

(圖片來自 這裏

Hooks

Hooks 本質上面說了,是把面向生命週期編程變成了面向業務邏輯編程,寫法上帶來的優化只是順帶的。

這裏,作一個類比,await/async 本質是把 JS 裏異步編程思惟變成了同步思惟,寫法上表現出來的特色就是原來的 Callback Hell 被打平了。

總結對比:

  • await/async 把 Callback Hell 幹掉了,異步編程思惟變成了同步編程思惟
  • Hooks 把 Wrapper Hell 幹掉了,面向生命週期編程變成了面向業務邏輯編程

這裏不得不客觀地說,HOC 和 Render Props 仍是有存在的必要,一方面是支持 React Class,另外一方面,它們不光適用於純邏輯封裝,不少時候也適合邏輯 + 組件的封裝場景,雖然此時使用 Hooks 也能夠,可是會顯得囉嗦點。另外,上面詬病的最大的問題 Wrapper Hell,我我的以爲使用 Fragment 也能夠基本解決。

狀態盒子

首先,React Hooks 的設計是反直覺的,爲何這樣說呢?能夠先試着問本身:爲何 Hooks 只能在其它 Hooks 的函數或者 React Function 組件裏?

在咱們的認知裏,React 社區一直推崇函數式、純函數等思想,引入 Hooks 概念後的 Functional Component 變的再也不純了,useXxx 與其說是一條執行語句,不如說是一個聲明。聲明這裏放了一個「狀態盒子」,盒子有輸入和輸出,剩下的內部實現就一無所知,重要的是,盒子是有記憶的,下次執行到此位置時,它有以前上下文信息。

類比「代碼」和「程序」的區別,前者是死的,後者是活的。表達式 c = a + b 表示把 ab 累加後的值賦值給 c,可是若是寫成 c := a + b 就表示 c 的值由 ab 相加獲得。看起來表述差很少,但實際上,後者隱藏着一個時間的維度,它表示的是一種聯繫,而不僅僅是個運算。這在 RxJS 等庫中被大量使用。

這種聲明目前是經過很弱的 use 前綴標識的(可是設計上會簡潔不少),爲了避免弄錯每一個盒子和狀態的對應關係,書寫的時候 Hooks 須要 use 開頭且放在頂層做用域,即不能夠包裹 if/switch/when/try 等。若是你按文章開頭引入了那個 ESLint Plugin 就不用擔憂會弄錯了。

總結

這篇文章可能並無一個很條理的目錄結構,大可能是一些我的理解和相關思考。所以,這不能替代你去看真正的文檔瞭解更多。若是你看完後仍是以爲廢話太多,不知所云,那我但願你至少能夠在下面幾點上跟做者達成共鳴:

  • Hooks 本質是把面向生命週期編程變成了面向業務邏輯編程
  • Hooks 使用上是一個邏輯狀態盒子,輸入輸出表示的是一種聯繫;
  • Hooks 是 React 的將來,但仍是沒法徹底替代原始的 Class。

參考

文章可隨意轉載,但請保留此 原文連接
很是歡迎有激情的你加入 ES2049 Studio,簡歷請發送至 caijun.hcj(at)alibaba-inc.com 。
相關文章
相關標籤/搜索