React 16.7.0-alpha hooks 之 effect

轉載於免費視頻網站:www.rails365.net/articles/re…html

Effect Hook可使得你在函數組件中執行一些帶有反作用的方法。react

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計數器的例子,可是如今添加了新的功能: 咱們將文檔標題設置爲自定義消息,包括點擊次數。git

數據獲取,設置訂閱以及手動更改React組件中的DOM都是反作用的示例。不管你是否習慣於將這些操做稱爲「反作用」(或僅僅是「效果」),但你以前可能已經在組件中執行了這些操做。github

提示: 若是你熟悉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的任何值,因此它永遠不須要從新運行。這不做爲特殊狀況處理 - 它直接遵循輸入數組的工做方式。雖然傳遞[]更接近熟悉的componentDidMountcomponentWillUnmount生命週期,但咱們建議不要將它做爲一種習慣,由於它常常會致使錯誤,除非你明確你本身在作什麼, 如上所述。 不要忘記React推遲運行useEffect直到瀏覽器繪製完成後,因此作額外的工做不是問題。

相關文章
相關標籤/搜索