React Hooks 解析(上):基礎

歡迎關注我的公衆號睿Talk,獲取我最新的文章:
clipboard.png

一、前言

React Hooks 是從 v16.8 引入的又一開創性的新特性,我深深的爲 React 團隊天馬行空的創造力和精益求精的鑽研精神所折服。剛開始瞭解這特性的時候,真的有一種豁然開朗,發現新大陸的感覺。本文除了關注具體的用法外,還會分析背後的邏輯和使用時候的一些注意點,力求做到既知其然也知其所以然。

二、Hooks 的由來

Hooks的出現是爲了解決 React 長久以來存在的一些問題:

  • 帶組件狀態的邏輯很難重用

爲了解決這個問題,需要引入render propshigher-order components這樣的設計模式,如react-redux提供的connect方法。這種方案不夠直觀,而且需要改變組件的層級結構,極端情況下會有多個wrapper嵌套調用的情況。

Hooks可以在不改變組件層級關係的前提下,方便的重用帶狀態的邏輯。

  • 複雜組件難於理解

大量的業務邏輯需要放在componentDidMountcomponentDidUpdate等生命週期函數中,而且往往一個生命週期函數中會包含多個不相關的業務邏輯,如日誌記錄和數據請求會同時放在componentDidMount中。而相關的業務邏輯又需要放在不同的生命週期函數中,如組件掛載的時候訂閱時間,組件卸載的時候取消訂閱,就需要同時在componentDidMountcomponentWillUnmount中寫相關邏輯。

Hooks可以封裝相關聯的業務邏輯,讓代碼結構更加清晰。

  • 難於理解的 Class 組件

JS 中的this關鍵字讓不少人吃過苦頭,它的取值與其它面嚮對象語言都不一樣,是在運行時決定的。爲了解決這一痛點,纔會有剪頭函數的this綁定特性。另外 React 中還有Class ComponentFunction Component的概念,什麼時候應該用什麼組件也會是一件糾結的事情。代碼優化方面,對Class Component進行預編譯和壓縮會比普通函數困難得多,而且還容易出問題。

Hooks可以在不使用 Class 的前提下,應用 React 的各種特性。

三、什麼是 Hooks

Hooks are functions that let you “hook into” React state and lifecycle features from function components

上面是官方解釋。從中可以看出 Hooks 是函數,有多個種類,每個 Hook 都爲Function Component提供使用 React 狀態和生命週期特性的通道。Hooks 不能在Class Component中使用。

React 提供了一些預定義好的 Hooks 供我們使用,下面我們來詳細瞭解一下。

四、State Hook

先來看一個傳統的Class Component:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

使用 State Hook 來改寫會是這個樣子:

import React, { useState } from 'react';

function Example() {
  // 定義一個 State 變量,變量值可以通過 setCount 來改變
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

可以看到useState的入參只有一個,就是 state 的初始值。這個初始值可以是一個數字、字符串或對象,甚至可以是一個函數。當入參是一個函數的時候,這個函數只會在這個組件初始渲染的時候執行:

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

useState的返回值是一個數組,數組的第一個元素是 state 當前的值,第二個元素是改變 state 的方法。這兩個變量的命名不需要遵守什麼約定,可以自由發揮。要注意的是如果 state 是一個對象,setState 的時候不會像Class Component的 setState 那樣自動合併對象。要達到這種效果,可以這麼做:

setState(prevState => {
  // Object.assign 也可以
  return {...prevState, ...updatedValues};
});

從上面的代碼可以看出,setState 的參數除了數字、字符串或對象,還可以是函數。當需要根據之前的狀態來計算出當前狀態值的時候,就需要傳入函數了,這跟Class Component的 setState 有點像。

另外一個跟Class Component的 setState 很像的一點是,當新傳入的值跟之前的值一樣時(使用Object.is比較),不會觸發更新。

五、Effect Hook

解釋這個 Hook 之前先理解下什麼是副作用。網絡請求、訂閱某個模塊或者 DOM 操作都是副作用的例子,Effect Hook 是專門用來處理副作用的。Function Component的函數體中 React 是不建議寫副作用的代碼的,否則容易出 bug。

下面的Class Component例子中,副作用代碼寫在了componentDidMountcomponentDidUpdate中:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

可以看到componentDidMountcomponentDidUpdate中的代碼是一樣的。而使用 Effect Hook 來改寫就不會有這個問題:

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useEffect會在每次 DOM 渲染後執行,不會阻塞頁面渲染。它同時具備componentDidMountcomponentDidUpdatecomponentWillUnmount三個生命週期函數的執行時機。

此外還有一些副作用需要組件卸載的時候做一些額外的清理工作的,例如訂閱某個功能:

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

使用 Effect Hook 來改寫會是這個樣子:

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    
    // 返回一個函數來進行額外的清理工作:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

useEffect的返回值是一個函數的時候,React 會在下一次執行這個副作用之前執行一遍清理工作,整個組件的聲明週期流程可以這麼理解:

組件掛載 --> 執行副作用 --> 組件更新 --> 執行清理函數 --> 執行副作用 --> 組件更新 --> 執行清理函數 --> 組件卸載

上文提到useEffect會在每次渲染後執行,但有的情況下我們希望只有在 state 或 props 改變的情況下才執行。如果是Class Component,我們會這麼做:

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

使用 Hook 的時候,我們只需要傳入第二個參數:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 只有在 count 改變的時候才執行 Effect

第二個參數是一個數組,可以傳多個值,一般會將 Effect 用到的所有 props 和 state 都傳進去。

當副作用只需要在組件掛載的時候和卸載的時候執行,第二個參數可以傳一個空數組[],實現的效果有點類似componentDidMountcomponentWillUnmount的組合。

六、總結

本文介紹了在 React 之前版本中存在的一些問題,然後引入 Hooks 的解決方案,並詳細介紹了 2 個最重要的 Hooks:useStateuseEffect的用法及注意事項。本來想一篇寫完所有相關的內容,但發現坑有點深,只能分兩次填了:)

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