1. 程式人生 > >react 16.7 hooks - effect 詳解

react 16.7 hooks - effect 詳解

Effect Hook可以使得你在函式元件中執行一些帶有副作用的方法。

import { useState, useEffect } from 'react';

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

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title =
`You clicked ${count} times`; }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }

上面這段程式碼是基於上個state hook計數器的例子,但是現在添加了新的功能: 我們將文件標題設定為自定義訊息,包括點選次數。

資料獲取,設定訂閱以及手動更改React元件中的DOM都是副作用的示例。無論你是否習慣於將這些操作稱為“副作用”(或僅僅是“效果”),但你之前可能已經在元件中執行了這些操作。

提示: 如果你熟悉React類生命週期方法,則可以將useEffect Hook視為componentDidMountcomponentDidUpdatecomponentWillUnmount的組合。

React元件中有兩種常見的副作用:那些不需要清理的副作用,以及那些需要清理的副作用。讓我們更詳細地看一下這種區別。

無需清理的副作用

有時,我們希望在**React更新DOM之後執行一些額外的程式碼。** 網路請求,手動改變DOM

和日誌記錄是不需要清理的效果(副作用,簡稱’效果’)的常見示例。我們這樣說是因為我們可以執行它們並立即忘記它們。讓我們比較一下classhooks如何讓我們表達這樣的副作用。

使用class的例子

React類元件中,render方法本身不應該導致副作用。這太早了 - 我們通常希望在React更新DOM之後執行我們的效果。

這就是為什麼在React類中,我們將副作用放入componentDidMountcomponentDidUpdate中。回到我們的示例,這裡是一個React計數器類元件,它在ReactDOM進行更改後立即更新文件標題:

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>
    );
  }
}

請注意我們如何在類中複製這兩個生命週期方法之間的程式碼。

這是因為在許多情況下,我們希望執行相同的副作用,無論元件是剛安裝還是已更新。從概念上講,我們希望它在每次渲染之後發生 - 但是React類元件沒有這樣的方法(render方法應該避免副作用)。我們可以提取一個單獨的方法,但我們仍然需要在兩個地方呼叫它。

現在讓我們看看我們如何使用useEffect Hook做同樣的事情。

使用Hooks的例子

import { 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有什麼作用? 通過使用這個Hook,你告訴React你的元件需要在渲染後執行某些操作。React將記住你傳遞的函式(我們將其稱為“效果”),並在執行DOM更新後稍後呼叫它。在這個效果中,我們設定文件標題,但我們也可以執行資料提取或呼叫其他命令式API

為什麼在元件內呼叫useEffect 在元件中使用useEffect讓我們可以直接從效果中訪問狀態變數(如count或任何道具)。我們不需要特殊的API來讀取它 - 它已經在函式範圍內了。Hooks擁抱JavaScript閉包,並避免在JavaScript已經提供解決方案的情況下引入特定於ReactAPI

每次渲染後useEffect都會執行嗎? 是的。預設情況下,它在第一次渲染之後和每次更新之後執行。 (我們稍後會討論如何自定義它。)你可能會發現更容易認為效果發生在“渲染之後”,而不是考慮“掛載”和“更新”。React保證DOM在執行‘效果’時已更新。

詳細說明

現在我們對這個hook更加的瞭解了,那讓我們再看看下面的例子:

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

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

我們聲明瞭count狀態變數,然後告訴React我們需要使用效果。我們將一個函式傳遞給useEffect Hook,這個函式就是效果(副作用)。在我們的效果中,我們使用document.title瀏覽器API設定文件標題。我們可以讀取效果中的最新count,因為它在我們的函式範圍內。當React渲染我們的元件時,它會記住我們使用的效果,然後在更新DOM後執行我們的效果。每次渲染都會發生這種情況,包括第一次渲染。

有經驗的JavaScript開發人員可能會注意到,傳遞給useEffect的函式在每次渲染時都會有所不同。這是有意的。事實上,這就是讓我們從效果中讀取計數值而不用擔心它沒有改變的原因。每次我們重新渲染時,我們都會安排一個不同的效果,取代之前的效果。在某種程度上,這使得效果更像是渲染結果的一部分 - 每個效果“屬於”特定渲染。我們將在本頁後面更清楚地看到為什麼這有用。

注意:componentDidMountcomponentDidUpdate不同,使用useEffect的效果不會阻止瀏覽器更新螢幕。這使應用感覺更具響應性。大多數效果不需要同步發生。在他們這樣做的不常見情況下(例如測量佈局),有一個單獨的useLayoutEffect Hook,其APIuseEffect相同。

需要清理的副作用

之前,我們研究瞭如何表達不需要任何清理的副作用。但是,有些效果需要清理。例如,我們可能希望設定對某些外部資料來源的訂閱。在這種情況下,清理是非常重要的,這樣我們就不會引入記憶體洩漏!讓我們比較一下我們如何使用類和Hooks來實現它。

使用class的例子

React類中,通常會在componentDidMount中設定訂閱,並在componentWillUnmount中清除它。例如,假設我們有一個ChatAPI模組,可以讓我們訂閱朋友的線上狀態。以下是我們如何使用類訂閱和顯示該狀態:

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';
  }
}

請注意componentDidMountcomponentWillUnmount如何相互作用。生命週期方法迫使我們拆分這個邏輯,即使它們中的概念程式碼都與相同的效果有關。

注意: 眼尖的你可能會注意到這個例子還需要一個componentDidUpdate方法才能完全正確。我們暫時忽略這一點,但會在本頁的後面部分再回過頭來討論它。

使用hooks的例子

你可能認為我們需要單獨的效果來執行清理。但是新增和刪除訂閱的程式碼是如此緊密相關,以至於useEffect旨在將它保持在一起。如果你的效果返回一個函式,React將在清理時執行它:

import { useState, useEffect } from 'react';

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

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

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

為什麼我們從效果中返回一個函式? 這是效果的可選清理機制。每個效果都可能返回一個在它之後清理的函式。這使我們可以保持新增和刪除彼此接近的訂閱的邏輯。

React什麼時候清理效果? 當元件解除安裝時,React執行清理。但是,正如我們之前所瞭解的那樣,效果會針對每個渲染執行而不僅僅是一次。這就是React在下次執行效果之前還清除前一渲染效果的原因。我們將討論為什麼這有助於避免錯誤以及如何在以後發生效能問題時選擇退出此行為

注意 我們不必從效果中返回命名函式。我們在這裡只是為了說明才加的命名,但你可以返回箭頭函式。

概括

我們已經瞭解到useEffect讓我們在元件渲染後表達不同型別的副作用。某些效果可能需要清理,因此它們返回一個函式:

useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
});

其他效果可能沒有清理階段,也不會返回任何內容。比如:

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

如果你覺得你對Effect Hook的工作方式有了很好的把握,或者你感到不知所措,那麼現在就可以跳轉到關於Hooks規則。

使用效果的提示

我們將繼續深入瞭解使用React使用者可能會產生好奇心的useEffect的某些方面。

提示:使用多重效果分離問題

這是一個組合了前面示例中的計數器和朋友狀態指示器邏輯的元件:

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

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

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

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

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

請注意設定document.title的邏輯如何在componentDidMountcomponentDidUpdate之間拆分。訂閱邏輯也在componentDidMountcomponentWillUnmount之間傳播。componentDidMount包含兩個任務的程式碼。

那麼,Hooks如何解決這個問題呢?就像你可以多次使用狀態掛鉤一樣,你也可以使用多種效果。這讓我們將不相關的邏輯分成不同的效果:

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }
  // ...
}

Hooks允許我們根據它正在做的事情而不是生命週期方法名稱來拆分程式碼。 React將按照指定的順序應用元件使用的每個效果。

說明:為什麼效果在每個更新上執行

如果你習慣了類,你可能想知道為什麼每次重新渲染後效果的清理階段都會發生,而不是在解除安裝過程中只發生一次。讓我們看一個實際的例子,看看為什麼這個設計可以幫助我們建立更少bug的元件。

在上面介紹了一個示例FriendStatus元件,該元件顯示朋友是否線上。我們的類從this.props讀取friend.id,在元件掛載後訂閱朋友狀態,並在解除安裝期間取消訂閱:

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

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

但是如果friend prop在元件出現在螢幕上時發生了變化,會發生什麼? 我們的元件將繼續顯示不同朋友的線上狀態。這是一個錯誤。解除安裝時我們還會導致記憶體洩漏或崩潰,因為取消訂閱會使用錯誤的朋友ID。

在類元件中,我們需要新增componentDidUpdate來處理這種情況:

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

  componentDidUpdate(prevProps) {
    // Unsubscribe from the previous friend.id
    ChatAPI.unsubscribeFromFriendStatus(
      prevProps.friend.id,
      this.handleStatusChange
    );
    // Subscribe to the next friend.id
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

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

忘記正確處理componentDidUpdateReact應用程式中常見的bug漏洞。

現在考慮使用Hooks的這個元件的版本:

function FriendStatus(props) {
  // ...
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

它不會受到這個bug的影響。 (但我們也沒有對它做任何改動。)

沒有用於處理更新的特殊程式碼,因為預設情況下useEffect會處理它們。它會在應用下一個效果之前清除之前的效果。為了說明這一點,這裡是一個訂閱和取消訂閱呼叫的序列,該元件可以隨著時間的推移產生:

// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     // Run first effect

// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     // Run next effect

// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     // Run next effect

// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // Clean up last effect

此行為預設確保一致性,並防止由於缺少更新邏輯而導致類元件中常見的錯誤。

提示:通過跳過效果優化效能

在某些情況下,在每次渲染後清理或應用效果可能會產生效能問題。在類元件中,我們可以通過在componentDidUpdate中編寫與prevPropsprevState的額外比較來解決這個問題:

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

這個要求很常見,它被內建到useEffect Hook API中。如果在重新渲染之間沒有更改某些值,則可以告訴React跳過應用效果。為此,將陣列作為可選的第二個引數傳遞給useEffect

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 當count改變的時候回再次執行這個效果

在上面的例子中,我們傳遞[count]作為第二個引數。這是什麼意思?如果count為5,然後我們的元件重新渲染,count仍然等於5,則React將比較前一個渲染的[5]和下一個渲染的[5]。因為陣列中的所有項都是相同的(5 === 5),所以React會跳過這個效果。這是我們的優化。

當我們使用count更新為6渲染時,React會將前一渲染中[5]陣列中的專案與下一渲染中[6]陣列中的專案進行比較。這次,React將重新執行效果,因為5!== 6如果陣列中有多個專案,React將重新執行效果,即使其中只有一個不同。

這也適用於具有清理階段的效果:

useEffect(() => {
  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // Only re-subscribe if props.friend.id changes

將來, 第二個引數可能會通過構建時轉換自動新增。

注意 如果使用此優化,請確保該陣列包含外部作用域中隨時間變化且效果使用的任何值,換句話說就是要在這個效果函式裡有意義。否則,程式碼將引用先前渲染中的舊值。我們還將討論Hooks API參考中的其他優化選項。

如果要執行效果並僅將其清理一次(在裝載和解除安裝時),則可以將空陣列([])作為第二個引數傳遞。 這告訴React你的效果不依賴於來自propsstate的任何值,所以它永遠不需要重新執行。這不作為特殊情況處理 - 它直接遵循輸入陣列的工作方式。雖然傳遞[]更接近熟悉的componentDidMount和componentWillUnmount心理模型,但我們建議不要將它作為一種習慣,因為它經常會導致錯誤,如上所述。 不要忘記React推遲執行useEffect直到瀏覽器繪製完成後,所以做額外的工作不是問題。

更多的關於hook系列介紹, 請前往此處檢視