日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

React?Hook之使用Effect?Hook的方法_React

作者:zju_cbw ? 更新時間: 2022-05-18 編程語言

Effect Hook?可以讓你在函數組件中執行副作用操作

import React, { 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 (
    

You clicked {count} times

); }

這段代碼基于上一章節中的計數器示例進行修改,我們為計數器增加了一個小功能:將?document?的?title?設置為包含了點擊次數的消息。

數據獲取,設置訂閱以及手動更改 React 組件中的 DOM 都屬于副作用。不管你知不知道這些操作,或是“副作用”這個名字,應該都在組件中使用過它們。

提示

如果你熟悉 React?class?的生命周期函數,你可以把?useEffect?Hook 看做?componentDidMount,componentDidUpdate?和?componentWillUnmount?這三個函數的組合。

在 React 組件中有兩種常見副作用操作:需要清除的和不需要清除的。我們來更仔細地看一下他們之間的區別。

無需清除的 effect

有時候,我們只想在 React 更新 DOM 之后運行一些額外的代碼。比如發送網絡請求,手動變更 DOM,記錄日志,這些都是常見的無需清除的操作。因為我們在執行完這些操作之后,就可以忽略他們了。讓我們對比一下使用?class?和 Hook 都是怎么實現這些副作用的。

使用 class 的示例

在 React 的?class?組件中,render?函數是不應該有任何副作用的。一般來說,在這里執行操作太早了,我們基本上都希望在 React 更新 DOM 之后才執行我們的操作。

這就是為什么在 React class 中,我們把副作用操作放到?componentDidMount?和?componentDidUpdate?函數中?;氐绞纠?,這是一個 React 計數器的?class?組件。它在 React 對 DOM 進行操作之后,立即更新了?document?的?title?屬性

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 (
      

You clicked {this.state.count} times

); } }

注意,在這個 class 中,我們需要在兩個生命周期函數中編寫重復的代碼。

這是因為很多情況下,我們希望在組件加載和更新時執行同樣的操作。從概念上說,我們希望它在每次渲染之后執行 —— 但 React 的?class?組件沒有提供這樣的方法。即使我們提取出一個方法,我們還是要在兩個地方調用它。

現在讓我們來看看如何使用?useEffect?執行相同的操作。

使用 Hook 的示例

我們在本章節開始時已經看到了這個示例,但讓我們再仔細觀察它:

import React, { useState, useEffect } from 'react';
function Example() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
  return (
    

You clicked {count} times

); }
  • useEffect 做了什么?通過使用這個 Hook,你可以告訴 React 組件需要在渲染后執行某些操作。React 會保存你傳遞的函數(我們將它稱之為?effect),并且在執行 DOM 更新之后調用它。在這個?effect?中,我們設置了?document?的?title?屬性,不過我們也可以執行數據獲取或調用其他命令式的 API。
  • 為什么在組件內部調用 useEffect??將?useEffect?放在組件內部讓我們可以在?effect?中直接訪問?count state?變量(或其他?props)。我們不需要特殊的 API 來讀取它 —— 它已經保存在函數作用域中。Hook 使用了 JavaScript 的閉包機制,而不用在 JavaScript 已經提供了解決方案的情況下,還引入特定的 React API。
  • useEffect 會在每次渲染后都執行嗎??是的,默認情況下,它在第一次渲染之后和每次更新之后都會執行。(我們稍后會談到如何控制它。)你可能會更容易接受?effect?發生在“渲染之后”這種概念,不用再去考慮“掛載”還是“更新”。React 保證了每次運行?effect?的同時,DOM 都已經更新完畢。

詳細說明

現在我們已經對?effect?有了大致了解,下面這些代碼應該不難看懂了:

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

我們聲明了?count state?變量,并告訴 React 我們需要使用?effect。緊接著傳遞函數給?useEffect?Hook。此函數就是我們的?effect。然后使用?document.title?瀏覽器 API 設置?document?的?title。我們可以在?effect?中獲取到最新的?count?值,因為他在函數的作用域內。當 React 渲染組件時,會保存已使用的?effect,并在更新完 DOM 后執行它。這個過程在每次渲染時都會發生,包括首次渲染。

經驗豐富的 JavaScript 開發人員可能會注意到,傳遞給?useEffect?的函數在每次渲染中都會有所不同,這是刻意為之的。事實上這正是我們可以在?effect?中獲取最新的?count?的值,而不用擔心其過期的原因。每次我們重新渲染,都會生成新的?effect,替換掉之前的。某種意義上講,effect?更像是渲染結果的一部分 —— 每個?effect?“屬于”一次特定的渲染。我們將在本章節后續部分更清楚地了解這樣做的意義。

提示

與?componentDidMount?或?componentDidUpdate?不同,使用?useEffect?調度的?effect?不會阻塞瀏覽器更新屏幕,這讓你的應用看起來響應更快。大多數情況下,effect?不需要同步地執行。在個別情況下(例如測量布局),有單獨的?useLayoutEffect?Hook 供你使用,其 API 與?useEffect?相同。

需要清除的 effect

之前,我們研究了如何使用不需要清除的副作用,還有一些副作用是需要清除的。例如訂閱外部數據源。這種情況下,清除工作是非常重要的,可以防止引起內存泄露!現在讓我們來比較一下如何用 Class 和 Hook 來實現。

使用 Class 的示例

在 React class 中,你通常會在?componentDidMount?中設置訂閱,并在?componentWillUnmount?中清除它。例如,假設我們有一個?ChatAPI?模塊,它允許我們訂閱好友的在線狀態。以下是我們如何使用 class 訂閱和顯示該狀態:

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

你會注意到?componentDidMount?和?componentWillUnmount?之間相互對應。使用生命周期函數迫使我們拆分這些邏輯代碼,即使這兩部分代碼都作用于相同的副作用。

注意眼尖的讀者可能已經注意到了,這個示例還需要編寫?componentDidUpdate?方法才能保證完全正確。我們先暫時忽略這一點,本章節中后續部分會介紹它。

使用 Hook 的示例

如何使用 Hook 編寫這個組件。

你可能認為需要單獨的?effect?來執行清除操作。但由于添加和刪除訂閱的代碼的緊密性,所以?useEffect?的設計是在同一個地方執行。如果你的?effect?返回一個函數,React 將會在執行清除操作時調用它:

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

為什么要在 effect 中返回一個函數??這是?effect?可選的清除機制。每個?effect?都可以返回一個清除函數。如此可以將添加和移除訂閱的邏輯放在一起。它們都屬于?effect?的一部分。

React 何時清除 effect??React 會在組件卸載的時候執行清除操作。正如之前學到的,effect?在每次渲染的時候都會執行。這就是為什么 React 會在執行當前?effect?之前對上一個?effect?進行清除。

注意

并不是必須為?effect?中返回的函數命名。這里我們將其命名為?cleanup?是為了表明此函數的目的,但其實也可以返回一個箭頭函數或者給起一個別的名字。

小結

了解了?useEffect?可以在組件渲染后實現各種不同的副作用。有些副作用可能需要清除,所以需要返回一個函數:

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

其他的?effect?可能不必清除,所以不需要返回。

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

effect?Hook 使用同一個 API 來滿足這兩種情況。

使用 Effect 的提示

在本節中將繼續深入了解?useEffect?的某些特性,有經驗的 React 使用者可能會對此感興趣。你不一定要在現在了解他們,你可以隨時查看此頁面以了解有關 Effect Hook 的更多詳細信息。

提示:使用多個 Effect 實現關注點分離

使用 Hook 其中一個目的就是要解決?class?中生命周期函數經常包含不相關的邏輯,但又把相關邏輯分離到了幾個不同方法中的問題。下述代碼是將前述示例中的計數器和好友在線狀態指示器邏輯組合在一起的組件:

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?的邏輯是如何被分割到?componentDidMount?和?componentDidUpdate?中的,訂閱邏輯又是如何被分割到?componentDidMount?和?componentWillUnmount?中的。而且?componentDidMount?中同時包含了兩個不同功能的代碼。

那么 Hook 如何解決這個問題呢?就像你可以使用多個?state?的 Hook 一樣,你也可以使用多個?effect。這會將不相關邏輯分離到不同的?effect?中:

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // ...
}

Hook 允許我們按照代碼的用途分離他們,?而不是像生命周期函數那樣。React 將按照?effect?聲明的順序依次調用組件中的每一個?effect。

解釋:為什么每次更新的時候都要運行 Effect

如果你已經習慣了使用?class,那么你或許會疑惑為什么?effect?的清除階段在每次重新渲染時都會執行,而不是只在卸載組件的時候執行一次。讓我們看一個實際的例子,看看為什么這個設計可以幫助我們創建 bug 更少的組件。

在本章節開始時,我們介紹了一個用于顯示好友是否在線的?FriendStatus?組件。從?class?中?props?讀取?friend.id,然后在組件掛載后訂閱好友的狀態,并在卸載組件的時候取消訂閱:

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

但是當組件已經顯示在屏幕上時,friend prop 發生變化時會發生什么??我們的組件將繼續展示原來的好友狀態。這是一個 bug。而且我們還會因為取消訂閱時使用錯誤的好友 ID 導致內存泄露或崩潰的問題。

在?class?組件中,我們需要添加?componentDidUpdate?來解決這個問題:

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  componentDidUpdate(prevProps) {
    // 取消訂閱之前的 friend.id
    ChatAPI.unsubscribeFromFriendStatus(
      prevProps.friend.id,
      this.handleStatusChange
    );
    // 訂閱新的 friend.id
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

忘記正確地處理?componentDidUpdate?是 React 應用中常見的 bug 來源。

現在看一下使用 Hook 的版本:

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

它并不會受到此 bug 影響。(雖然我們沒有對它做任何改動。)

并不需要特定的代碼來處理更新邏輯,因為?useEffect?默認就會處理。它會在調用一個新的?effect?之前對前一個?effect?進行清理。為了說明這一點,下面按時間列出一個可能會產生的訂閱和取消訂閱操作調用序列:

// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     // 運行第一個 effect
// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // 清除上一個 effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     // 運行下一個 effect
// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // 清除上一個 effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     // 運行下一個 effect
// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // 清除最后一個 effect

此默認行為保證了一致性,避免了在?class?組件中因為沒有處理更新邏輯而導致常見的 bug。

提示:通過跳過 Effect 進行性能優化

在某些情況下,每次渲染后都執行清理或者執行?effect?可能會導致性能問題。在?class?組件中,我們可以通過在?componentDidUpdate?中添加對?prevProps?或?prevState?的比較邏輯解決:

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

這是很常見的需求,所以它被內置到了?useEffect?的 Hook API 中。如果某些特定值在兩次重渲染之間沒有發生變化,你可以通知 React 跳過對?effect?的調用,只要傳遞數組作為?useEffect?的第二個可選參數即可:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 僅在 count 更改時更新

上面這個示例中,我們傳入?[count]?作為第二個參數。這個參數是什么作用呢?如果?count?的值是?5,而且我們的組件重渲染的時候?count?還是等于?5React?將對前一次渲染的?[5]?和后一次渲染的?[5]?進行比較。因為數組中的所有元素都是相等的?(5 === 5),React 會跳過這個 effect,這就實現了性能的優化。

當渲染時,如果?count?的值更新成了?6,React 將會把前一次渲染時的數組?[5]?和這次渲染的數組?[6]?中的元素進行對比。這次因為?5 !== 6,React 就會再次調用?effect。如果數組中有多個元素,即使只有一個元素發生變化,React 也會執行?effect

對于有清除操作的?effect?同樣適用:

useEffect(() => {
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }
  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // 僅在 props.friend.id 發生變化時,重新訂閱

未來版本,可能會在構建時自動添加第二個參數。

注意:

如果你要使用此優化方式,請確保數組中包含了所有外部作用域中會隨時間變化并且在?effect?中使用的變量,否則你的代碼會引用到先前渲染中的舊變量。參閱文檔,了解更多關于如何處理函數以及數組頻繁變化時的措施內容。

如果想執行只運行一次的 effect(僅在組件掛載和卸載時執行),可以傳遞一個空數組([])作為第二個參數。這就告訴 React 你的?effect?不依賴于?props?或?state?中的任何值,所以它永遠都不需要重復執行。這并不屬于特殊情況 —— 它依然遵循依賴數組的工作方式。

如果你傳入了一個空數組([]),effect?內部的?props?和?state?就會一直擁有其初始值。盡管傳入?[]?作為第二個參數更接近大家更熟悉的?componentDidMount?和?componentWillUnmount?思維模式,但我們有更好的方式來避免過于頻繁的重復調用?effect。除此之外,請記得 React 會等待瀏覽器完成畫面渲染之后才會延遲調用?useEffect,因此會使得額外操作很方便。

我們推薦啟用?eslint-plugin-react-hooks?中的?exhaustive-deps?規則。此規則會在添加錯誤依賴時發出警告并給出修復建議。

總結

原文鏈接:https://blog.csdn.net/weixin_44491423/article/details/123508777

欄目分類
最近更新