用了這麼長時間的 React useEffect hook,你用對了嗎?

useEffect 是 React Hooks 的核心,要保證理解它的運行機制和正確的使用方法才能避免這樣那樣的坑。在以前的工作中,因爲它我碰到過無數個坑,比如拿到的值是舊的,該執行的時候不執行,不該執行的時候執行了……

所以爲了避免如上尷尬、節省絞盡腦汁找 bug 的時間、保護咱們的髮際線,一定要認真學會如何正確使用 useEffect hook。

什麼是 Effect

俗話說,知已知彼,百戰不殆,我們先了解下神馬是 Effect。其實大家在開發過程中或多或少的都接觸過 side effect 的概念,即副作用 —— 對於數據抓取,註冊監聽事件,修改 DOM 元素等馬後炮式的操作都屬於副作用,因爲我們渲染出來的頁面是靜態的,任何在之後的操作都會對它產生影響,所以才稱之爲副作用。而 useEffect 則是專門用來編寫副作用代碼的,這也是 React 的核心所在。

與生命週期的關係

目前市面上的文章,包括官方文檔都讓我們把 useEffect 想象成 componentDidMountcomponentDidUpdatecomponentWillUnmount 三個生命週期的結合體。其實並不然,如果非要把 useEffect 的運行機制往生命週期上靠,會造成一些邏輯上的困惑,進而產生 bug。我們所要做的,就是把 useEffect 當成一個全新的特性,專門爲函數式組件服務的,這樣用起來纔不會迷茫。下面我們通過實例來演示它的各種用法。

運行時機

useEffect 必然會在 render 的時候執行一次,其他的運行時機取決於以下情況:

  • 有沒有第二個參數。useEffect hook 接受兩個參數,第一個是要執行的代碼,第二個是一個數組,指定一組依賴的變量,其中任何一個變量發生變化時,此 effect 都會重新執行一次。
  • 有沒有返回值。 useEffect 的執行代碼中可以返回一個函數,在每一次新的 render 進行前或者組件 unmount 之時,都會執行此函數,進行清理工作。

我們先看一個簡單的例子,想看完整代碼和隨意把玩的,請點擊下邊按鈕

Edit without the second parameter

我們首先看最頂層 <App /> 的代碼:

function App() {
  const [showList, setShowList] = useState(false);
  const [postCount, setPostCount] = useState(5);

  return (
    <div className="App">
      <button onClick={() => setShowList(!showList)}>
        {showList ? "隱藏" : "顯示"}
      </button>
      <button onClick={() => setPostCount(previousCount => previousCount + 1)}>
        增加數量
      </button>
      {showList && <PostList count={postCount} />}
    </div>
  );
}

此組件用來顯示一系列的文章列表,以及控制文章列表是否顯示的按鈕和控制顯示多少條文章的按鈕。我們用 showList state 來控制 <PostList /> 的顯示與否。這是爲了讓 <PostList /> 組件 unmount 再 render ,以證明每次它 render 和 unmount 的時候,useEffect hook 都會跑一次。 <PostList /> 組件的代碼如下:

function PostList({ count = 5 }) {
  useEffect(() => {
    let p = document.createElement("p");
    p.innerHTML = `當前文章數量:${count}`;
    document.body.append(p);
  });

  return (
    <ul>
      {new Array(count).fill("文章標題").map((value, index) => {
        return (
          <li key={index}>
            {value}
            {index + 1}
          </li>
        );
      })}
    </ul>
  );
}

該組件展示了一個 <ul> 列表,爲了簡單起見,生成了一些無聊的文章標題。我們重點來看一下 useEffect 所做的操作:

  • 創建一個 p 元素
  • 設置 p 的文本爲當前文章的數量
  • 追加 pbody 的最後

在這裏,此 effect 並沒有返回任何值,也沒有給它傳遞任何一個參數,那會是什麼樣的效果呢?
沒返回值

答案是,此 effect 會在每次 countshowList 改變時每點擊一次 顯示增加數量 按鈕,我們新追加的 p 都會再追加一次。這也是造成內容泄露的坑,如果我們在這裏添加了太多耗內存的東西而沒有清理,不用多久瀏覽器就崩潰了~ 解決方法很簡單,給 useEffect 添加一個返回值,並在裏邊刪除我們追加的 p 元素即可:

useEffect(() => {
  let p = document.createElement("p");
  p.innerHTML = `當前文章數量:${count}`;
  document.body.append(p);

  return () => {
    p.remove();
  };
});

這樣我們在點擊按鈕的時候,確保只有一個 p 在當前頁面上。看,這樣寫起來是不是比分散在 componentDidMountcomponentWillUnmount 中方便多了?我們可以方便的在同一個作用域中方便的拿到 p 的引用,直接刪除它即可。

有返回值

類實際工作的例子 - 抓取數據

爲了繼續深入 useEffect hook,我仿照實際工作遇到的情況,編寫了一個例子,這裏我們用 useEffect 進行數據抓取,同樣的顯示博客文章列表,完整代碼請點擊下方按鈕查看:

Edit async loading

在本例中,<PostList />組件的代碼做了一些修改,首先我們定義兩個新的 state:

  • posts。保存遠程加載的文章列表
  • loading。記錄 ajax 請求狀態
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(false);

我們一開始覺得 fetch 是異步操作,那麼得給 useEffect hook 傳遞個 async 的函數吧?錯,傳遞給 useEffect 的函數不能是 async 的, 因爲 async 的本質是返回一個 Promise,而 useEffect 唯一接收的返回值是個函數。使用 async 會收到以下異常:

Warning: An effect function must not return anything besides a function, which is used for clean-up.

// 錯誤
useEffect(async () => {
  // const response = await fetch("https://jsonplaceholder.typicode.com/posts");
});

正確的寫法是,把抓取數據的邏輯定義到一個單獨的函數中,然後在 useEffect 中調用它:

useEffect(() => {
  // const response = await fetch("https://jsonplaceholder.typicode.com/posts");

  const loadPosts = async () => {
    setLoading(true);
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/posts?_limit=${count}`
    );
    const data = await response.json();
    setPosts(data);
    setLoading(false);
  };

  loadPosts();
}, [count]);

它所做的操作是,在請求數據前,把 loading 狀態設置爲 true,然後根據 count 的值去取對應數量的文章列表,把返回值更新到 posts state 中,再把 loading 設置爲 false。最後根據 loading 的狀態,我們顯示 加載中文章列表

if (loading) return <div>loading...</div>;

return (
  <ul>
    {posts.slice(0, count).map((post, index) => {
      return <li key={post.id}>{post.title}</li>;
    })}
  </ul>
);

加載數據

第二個參數

上邊的例子中我們給 useEffect 傳遞了第二個參數,並把 count 作爲依賴的值,每當 count 變化時,此 effect 都會重新執行一次,去加載新的數據。另外,如果我們隱藏列表,再點擊 顯示 按鈕時,effect 也會再跑一次,因爲點擊隱藏時,<PostList /> 組件被 unmount ,然後再次顯示時會重新 render ,我們可以根據 loading... 這個標誌就可以看出來了。

如果我們去掉第二個參數,那麼就會陷入死循環的坑,爲什麼呢?因爲 effect 執行時,會更新 postsloading 這兩個 state,而 state 變化時,組件又會重新 render 一次,根據 useEffect 在每次 render 必執行一次的定律不難得出結論。

那麼如果我們給它一個空數組呢?那就無論怎麼點擊增加數量,此 effect 都不會重新執行,導致永遠只加載默認 5 篇文章。

加載數據-無依賴數組

添加其他屬性

我們可以再試試添加一個其他屬性來測試 useEffect 依賴數組的特性。在 <App /> 組件中我們添加一個佈局狀態 vertical 和修改佈局的按鈕,用來控制 <PostList> 組件的橫向、縱向佈局:

// APP
function App() {
  // 其它代碼省略
  const [vertical, setVertical] = useState(true);

  return (
    <div className="App">
      {/* 其它代碼省略 */}
      <button onClick={() => setVertical(prev => !prev)}>更改佈局</button>
      {showList && <PostList count={postCount} vertical={vertical} />}
    </div>
  );
}

// PostList
function PostList({ count = 5, vertical = false }) {
  // 其它代碼省略
  useEffect(() => {
    // const response = await fetch("https://jsonplaceholder.typicode.com/posts");

    const loadPosts = async () => {
      setLoading(true);
      const response = await fetch(
        `https://jsonplaceholder.typicode.com/posts?_limit=${count}`
      );
      const data = await response.json();
      setPosts(data);
      setLoading(false);
    };

    loadPosts();
  }, [count, vertical]); // 在這裏添加 vertical 作爲依賴

  // 其它代碼省略
}

在這裏我們給第二個參數添加了 vertical 依賴,這樣每次點擊 更改佈局 按鈕時,文章列表都會加載一次,這種適合在佈局改變時需要重新請求數據的情況:
加載數據-依賴layout

如果不需要重新加載數據,只需要把 vertical 從依賴數組裏去掉就可以了。

加載數據-不依賴layout

劃重點

看看大家對頻繁使用的 useEffect 的用法用對了沒有?來標一下重點:

  1. 它可不完全是 componentDidMountcomponentDidUpdatecomponentWillUnmount 三個生命週期的結合體哦(人家有自己的想法)。
  2. 會在每次 render 的時候必定執行一次。
  3. 如果返回了函數,那麼在下一次 render 之前或組件 unmount 之前必定會運行一次返回函數的代碼。
  4. 如果指定了依賴數組,且不爲空,則當數組裏的每個元素髮生變化時,都會重新運行一次。
  5. 如果數組爲空,則只在第一次 render 時執行一次,如果有返回值,則同 3。
  6. 如果在 useEffect 中更新了 state,且沒有指定依賴數組,或 state 存在於依賴數組中,就會造成死循環。

大家掌握了嗎?有什麼問題歡迎評論或私信我!如果覺得文章有幫助請關注博主我哦,感謝,比心。

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