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系列介紹, 請前往此處查看

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容

  • 作為一個合格的開發者,不要只滿足于編寫了可以運行的代碼。而要了解代碼背后的工作原理;不要只滿足于自己的程序...
    六個周閱讀 8,475評論 1 33
  • 之前我們介紹了使用hooks的原因,在開始介紹api之前,現在我們先來整體的預覽下這些api。從上篇的介紹可以知道...
    xiaohesong閱讀 60,954評論 11 17
  • Hooks是新的功能提案,出現在v16.7.0-alpha版本中,使用Hooks可以讓你在不適用類的情況下使用狀態...
    xiaohesong閱讀 1,535評論 0 3
  • It's a common pattern in React to wrap a component in an ...
    jplyue閱讀 3,291評論 0 2
  • “你什么時候需要孤獨?”這是我qq漂流瓶中一個陌生人問的問題。 想了一會,我回答他周六上午。 我經常喜歡在周六的上...
    墨客于筆閱讀 342評論 2 5