結合React的Effect Hook分析組件副作用的清除

一個訂閱好友在線的組件

我們在DidMount的時候通過ID訂閱了好友的在線狀態
並且爲了防止內存泄漏,我們需要在WillUnmount清除訂閱

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

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

優化訂閱好友在線的組件

爲了解決props更新導致的BUG我們需要在DidUpdate中進行修改訂閱


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

引入Hooks代碼會變得十分簡單

Effect Hook

useEffect() 可以讓你在函數組件中執行副作用操作
默認情況下,它在第一次渲染之後和每次更新之後都會執行。

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

這是React官網最基礎的Hooks應用

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

需要清除的 effect

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

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

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

現在假設props.friend.id在更新100->200->300
我們並不需要特定的代碼來處理更新邏輯,因爲 useEffect 默認就會處理。它會在調用一個新的 effect 之前對前一個 effect 進行清理。這樣基於不會產生之前因爲Props改變而產生的BUG

// 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

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

參考文獻

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章