[React Hooks 翻譯] 4-8 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 (
    <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div>
  );
}
複製代碼
  • 什麼是反作用:數據獲取,設置訂閱以及手動更改React組件中的DOM都是反作用
  • 能夠將useEffect Hook視爲componentDidMount,componentDidUpdate和componentWillUnmount的組合。

不清理的反作用

有時,咱們但願在React更新DOM以後運行一些額外的操做。如:html

  1. 網絡請求
  2. 手動修改DOM
  3. 日誌記錄

這些操做不須要清理,也就是說能夠運行它們並當即忘記它們。下面咱們分別看看class和Hook是如何處理的react

使用Class

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>
    );
  }
}
複製代碼

注意,咱們在componentDidMount和componentDidUpdate的時候執行了一樣的代碼。下面看看Hooks怎麼處理的git

使用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作了什麼?github

    • useEffect告訴React組件須要在渲染後執行某些操做,React將記住useEffect傳遞的函數(也就是反作用函數),並在執行DOM更新後調用。
    • 本例中咱們設置了文檔標題,咱們也能夠執行獲取數據或調用其餘API
  • 爲何要在組件內部調用useEffect?npm

    • 能夠直接訪問state
    • 不須要特殊的API來讀取state,state已經在函數做用域內了。
  • useEffect每次render後都執行嗎?數組

    • 是的
    • 默認狀況下,它在第一次渲染以後和每次更新以後運行。 (咱們稍後將討論如何自定義它。)
    • 比起「mount」和"update",可能考慮"render"以後執行某些操做更容易。React確保是在DOM更新以後執行反作用

細節解釋

如今咱們對effect有了必定的瞭解,下面的代碼應該很容易懂了瀏覽器

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

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

也許你會發現每次render傳給useEffect的函數都不一樣,這是有意爲之的。實際上,這就是爲何咱們即便在useEffect內部讀取state也不用擔憂state過時。每次re-render,咱們都會安排一個不一樣的effect去取代以前的那個effect。在某種程度上,這使得effect更像是render的結果的一部分——每一個effect「屬於」特定的render。網絡

須要清理的反作用

使用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的代碼需「相互鏡像」。生命週期方法迫使咱們拆分相互關聯的邏輯函數

使用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);
    // 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何時會清理反作用?
    • 組件卸載時
    • **執行下一次反作用以前。**反作用在每次render的時候都會執行,React在下次運行反作用以前還清除前一次render的反作用。咱們將在後面討論爲何這麼作,以及發生性能問題以後如何跳過清除行爲

Note性能

反作用函數不必定要返回具名函數。

使用Effect須知

使用多個Effect進行關注點分離

以前在使用Hooks的動機那一章就有提到,使用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
    });
  }
  // ...
複製代碼

使用Hooks如何解決這個問題呢?

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);
    };
  });
  // ...
}
複製代碼
  • Hooks讓咱們根據邏輯拆分代碼,而不是根據生命週期方法名稱來拆分代碼。
  • React將按照指定的順序組件使用的每一個effect。

解釋:爲何每次更新都要運行effect?

若是你習慣使用class組件,你可能會很疑惑爲何不是組件卸載的時候執行清理反作用的工做,而是在每次re-render的時候都要執行。下面咱們就看看爲何

前面咱們介紹了一個示例FriendStatus組件,該組件顯示朋友是否在線。咱們的類從this.props讀取friend.id,在組件掛載以後訂閱朋友狀態,並在卸載前取消訂閱。

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

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
複製代碼

**可是若是在該組件還在顯示的狀態下,friend屬性改變了怎麼辦?**組件顯示的將是原來那個friend的在線狀態。這是一個bug。而後後面取消訂閱調用又會使用錯誤的friend ID,還會在卸載時致使內存泄漏或崩潰。

在class組件中,咱們須要添加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
    );
  }
複製代碼

忘記正確處理componentDidUpdate經常致使bug。

如今考慮使用Hooks實現這個組件

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
// Run first effect
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     

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

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

// Unmount
// Clean up last effect
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); 
複製代碼

默認狀況下這種作法確保了邏輯連貫性,而且防止了經常在class組件裏出現的由於忘寫update邏輯而致使的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]); // Only re-run the effect if count changes
複製代碼

在上面的例子中,咱們將[count]做爲第二個參數傳遞。

這是什麼意思?

  • 若是count是5,而後組件從新渲染以後count仍是5,React就會比較前一次渲染的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]); // Only re-subscribe if props.friend.id changes
複製代碼

未來,第二個參數可能會被構建時轉換自動添加。

Note

  • 若是你適用了這個優化,請確保數組包含了組件做用域內(例如props和state)effect用到的、隨時間變化的全部值。不然代碼可能引用了上一次render的那個過時的變量。Learn more about how to deal with functions and what to do when the array changes too often
  • 若是僅執行effect並清理一次(在mount和unmount上),能夠傳遞一個空數組([])做爲第二個參數。
    • 這告訴React你的效果不依賴於來自props或state,因此它永遠不須要從新運行。這不做爲一種特殊狀況處理 - 它直接遵循依賴項數組的工做方式。
    • 若是傳遞一個空數組([]),effect中的props和state將始終具備其初始值。
    • 雖然傳遞[]做爲第二個參數更接近componentDidMount和componentWillUnmount,可是有更好的解決方案來避免常常從新本身執行effect( better solutions
  • 不要忘記React延遲執行行useEffect直到瀏覽器繪製完成,因此作額外的工做並非什麼問題。
  • 咱們推薦使用 exhaustive-deps 規則(這是 eslint-plugin-react-hooks 的一部分)。它會在錯誤地指定依賴項時發出警告並建議修復。

下一篇

下面咱們將瞭解鉤子規則 - 它們對於使鉤子工做相當重要。

相關文章
相關標籤/搜索