useEffect Hook 是如何工做的

做者:Dave Ceddiahtml

譯者:前端小智前端

來源:daveceddia.react


阿里雲最近在作活動,低至2折,有興趣能夠看看promotion.aliyun.com/ntms/yunpar…git


爲了保證的可讀性,本文采用意譯而非直譯。github

想象一下:你有一個很是好用的函數組件,而後有一天,我們須要向它添加一個生命週期方法。json

呃...數組

剛開始我們可能會想怎麼能解決這個問題,而後最後變成,一般的作法是將它轉換成一個類。但有時候我們就是要用函數方式,怎麼破? useEffect hook 出現就是爲了解決這種狀況。瀏覽器

使用useEffect,能夠直接在函數組件內處理生命週期事件。 若是你熟悉 React class 的生命週期函數,你能夠把 useEffect Hook 看作 componentDidMountcomponentDidUpdatecomponentWillUnmount 這三個函數的組合。來看看例子:安全

import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';

function LifecycleDemo() {
  useEffect(() => {
    // 默認狀況下,每次渲染後都會調用該函數
    console.log('render!');

    // 若是要實現 componentWillUnmount,
    // 在末尾處返回一個函數
    // React 在該函數組件卸載前調用該方法
    // 其命名爲 cleanup 是爲了代表此函數的目的,
    // 但其實也能夠返回一個箭頭函數或者給起一個別的名字。
    return function cleanup () {
        console.log('unmounting...');
    }
  })  
  return "I'm a lifecycle demo";
}

function App() {
  // 創建一個狀態,爲了方便
  // 觸發從新渲染的方法。
  const [random, setRandom] = useState(Math.random());

  // 創建一個狀態來切換 LifecycleDemo 的顯示和隱藏
  const [mounted, setMounted] = useState(true);

  // 這個函數改變 random,並觸發從新渲染
  // 在控制檯會看到 render 被打印
  const reRender = () => setRandom(Math.random());

  // 該函數將卸載並從新掛載 LifecycleDemo
  // 在控制檯能夠看到  unmounting 被打印
  const toggle = () => setMounted(!mounted);

  return (
    <>
      <button onClick={reRender}>Re-render</button>
      <button onClick={toggle}>Show/Hide LifecycleDemo</button>
      {mounted && <LifecycleDemo/>}
    </>
  );
}

ReactDOM.render(<App/>, document.querySelector('#root'));
複製代碼

CodeSandbox中嘗試一下。dom

單擊「Show/Hide」按鈕,看看控制檯,它在消失以前打印「unmounting...」,並在它再次出現時打印 「render!」。

如今,點擊Re-render按鈕。每次點擊,它都會打render!,還會打印umounting,這彷佛是奇怪的。

爲啥每次渲染都會打印 'unmounting'。

我們能夠有選擇性地從useEffect返回的cleanup函數只在組件卸載時調用。React 會在組件卸載的時候執行清除操做。正如以前學到的,effect 在每次渲染的時候都會執行。這就是爲何 React 會在執行當前 effect 以前對上一個 effect 進行清除。這實際上比componentWillUnmount生命週期更強大,由於若是須要的話,它容許我們在每次渲染以前和以後執行反作用。

不徹底的生命週期

useEffect在每次渲染後運行(默認狀況下),而且能夠選擇在再次運行以前自行清理。

與其將useEffect看做一個函數來完成3個獨立生命週期的工做,不如將它簡單地看做是在渲染以後執行反作用的一種方式,包括在每次渲染以前和卸載以前我們但願執行的須要清理的東西。

阻止每次從新渲染都會執行 useEffect

若是但願 effect 較少運行,能夠提供第二個參數 - 值數組。 將它們視爲該effect的依賴關係。 若是其中一個依賴項自上次更改後,effect將再次運行。

const [value, setValue] = useState('initial');

useEffect(() => {
  // 僅在 value 更改時更新
  console.log(value);
}, [value]) 
複製代碼

上面這個示例中,我們傳入 [value] 做爲第二個參數。這個參數是什麼做用呢?若是value的值是 5,並且我們的組件重渲染的時候 value 仍是等於 5,React 將對前一次渲染的 [5] 和後一次渲染的 [5] 進行比較。由於數組中的全部元素都是相等的(5 === 5),React 會跳過這個 effect,這就實現了性能的優化。

僅在掛載和卸載的時候執行

若是想執行只運行一次的 effect(僅在組件掛載和卸載時執行),能夠傳遞一個空數組([])做爲第二個參數。這就告訴 React 你的 effect 不依賴於 propsstate 中的任何值,因此它永遠都不須要重複執行。這並不屬於特殊狀況 —— 它依然遵循依賴數組的工做方式。

useEffect(() => {
  console.log('mounted');
  return () => console.log('unmounting...');
}, []) 
複製代碼

這樣只會在組件初次渲染的時候打印 mounted,在組件卸載後打印: unmounting

不過,這隱藏了一個問題:傳遞空數組容易出現bug。若是我們添加了依賴項,那麼很容易忘記向其中添加項,若是錯過了一個依賴項,那麼該值將在下一次運行useEffect時失效,而且可能會致使一些奇怪的問題。

只在掛載的時候執行

在這個例子中,一塊兒來看下如何使用useEffectuseRef hook 將input控件聚焦在第一次渲染上。

import React, { useEffect, useState, useRef } from "react";
import ReactDOM from "react-dom";

function App() {
  // 存儲對 input 的DOM節點的引用
  const inputRef = useRef();

  // 將輸入值存儲在狀態中
  const [value, setValue] = useState("");

  useEffect(
    () => {
      // 這在第一次渲染以後運行
      console.log("render");
      // inputRef.current.focus();
    },
	// effect 依賴  inputRef
    [inputRef]
  );

  return (
    <input
      ref={inputRef}
      value={value}
      onChange={e => setValue(e.target.value)}
    />
  );
}

ReactDOM.render(<App />, document.querySelector("#root"));
複製代碼

在頂部,咱們使用useRef建立一個空的ref。 將它傳遞給inputref prop ,在渲染DOM 時設置它。 並且,重要的是,useRef返回的值在渲染之間是穩定的 - 它不會改變。

所以,即便我們將[inputRef]做爲useEffect的第二個參數傳遞,它實際上只在初始掛載時運行一次。這基本上是 componentDidMount 效果了。

使用 useEffect 獲取數據

再來看看另外一個常見的用例:獲取數據並顯示它。在類組件中,無們經過能夠將此代碼放在componentDidMount方法中。在 hook 中可使用 useEffect hook 來實現,固然還須要用useState來存儲數據。

下面是一個組件,它從Reddit獲取帖子並顯示它們

import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";

function Reddit() {
  const [posts, setPosts] = useState([]);

  useEffect(async () => {
    const res = await fetch(
      "https://www.reddit.com/r/reactjs.json"
    );

    const json = await res.json();

    setPosts(json.data.children.map(c => c.data));
  }); // 這裏沒有傳入第二個參數,你猜猜會發生什麼?

  // Render as usual
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

ReactDOM.render(
  <Reddit />,
  document.querySelector("#root")
);
複製代碼

注意到我們沒有將第二個參數傳遞給useEffect,這是很差的,不要這樣作。

不傳遞第二個參數會致使每次渲染都會運行useEffect。而後,當它運行時,它獲取數據並更新狀態。而後,一旦狀態更新,組件將從新呈現,這將再次觸發useEffect,這就是問題所在。

爲了解決這個問題,咱們須要傳遞一個數組做爲第二個參數,數組內容又是啥呢。

useEffect所依賴的惟一變量是setPosts。所以,我們應該在這裏傳遞數組[setPosts]。由於setPostsuseState返回的setter,因此不會在每次渲染時從新建立它,所以effect只會運行一次。

當數據改變時從新獲取

虛接着擴展一下示例,以涵蓋另外一個常見問題:如何在某些內容發生更改時從新獲取數據,例如用戶ID,名稱等。

首先,我們更改Reddit組件以接受subreddit做爲一個prop,並基於該subreddit獲取數據,只有當 prop 更改時才從新運行effect.

// 從props中解構`subreddit`:
function Reddit({ subreddit }) {
  const [posts, setPosts] = useState([]);

  useEffect(async () => {
    const res = await fetch(
      `https://www.reddit.com/r/${subreddit}.json`
    );

    const json = await res.json();
    setPosts(json.data.children.map(c => c.data));

    // 當`subreddit`改變時從新運行useEffect:
  }, [subreddit, setPosts]);

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

ReactDOM.render(
  <Reddit subreddit='reactjs' />,
  document.querySelector("#root")
);
複製代碼

這仍然是硬編碼的,可是如今我們能夠經過包裝Reddit組件來定製它,該組件容許我們更改subreddit

function App() {
  const [inputValue, setValue] = useState("reactjs");
  const [subreddit, setSubreddit] = useState(inputValue);

  // Update the subreddit when the user presses enter
  const handleSubmit = e => {
    e.preventDefault();
    setSubreddit(inputValue);
  };

  return (
    <>
      <form onSubmit={handleSubmit}>
        <input
          value={inputValue}
          onChange={e => setValue(e.target.value)}
        />
      </form>
      <Reddit subreddit={subreddit} />
    </>
  );
}

ReactDOM.render(<App />, document.querySelector("#root"));
複製代碼

在 CodeSandbox 試試這個示例。

這個應用程序在這裏保留了兩個狀態:當前的輸入值和當前的subreddit。提交表單將提交subreddit,這會致使Reddit從新獲取數據。

順便說一下:輸入的時候要當心,由於沒有錯誤處理,因此當你輸入的subreddit不存在,應用程序將會爆炸,實現錯誤處理就做爲大家的練習。

各位能夠只使用一個狀態來存儲輸入,而後將相同的值發送到Reddit,可是Reddit組件會在每次按鍵時獲取數據。

頂部的useState看起來有點奇怪,尤爲是第二行:

const [inputValue, setValue] = useState("reactjs");
const [subreddit, setSubreddit] = useState(inputValue);
複製代碼

咱們把reactjs的初值傳遞給第一個狀態,這是有意義的,這個值永遠不會改變。

那麼第二行呢,若是初始狀態改變了呢,如當你輸入box時候。

記住useState是有狀態的。它只使用初始狀態一次,即第一次渲染,以後它就被忽略了。因此傳遞一個瞬態值是安全的,好比一個可能改變或其餘變量的 prop

許許多多的用途

使用useEffect 就像瑞士軍刀。它能夠用於不少事情,從設置訂閱到建立和清理計時器,再到更改ref的值。

componentDidMountcomponentDidUpdate 不一樣的是,在瀏覽器完成佈局與繪製以後,傳給 useEffect 的函數會延遲調用。這使得它適用於許多常見的反作用場景,好比如設置訂閱和事件處理等狀況,所以不該在函數中執行阻塞瀏覽器更新屏幕的操做。

然而,並不是全部 effect 均可以被延遲執行。例如,在瀏覽器執行下一次繪製前,用戶可見的 DOM 變動就必須同步執行,這樣用戶纔不會感受到視覺上的不一致。(概念上相似於被動監聽事件和主動監聽事件的區別。)React 爲此提供了一個額外的 useLayoutEffect Hook 來處理這類 effect。它和 useEffect 的結構相同,區別只是調用時機不一樣。

雖然 useEffect 會在瀏覽器繪製後延遲執行,但會保證在任何新的渲染前執行。React 將在組件更新前刷新上一輪渲染的 effect

原文:daveceddia.com/useeffect-h…

代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug

交流(歡迎加入羣,羣工做日都會發紅包,互動討論技術)

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

github.com/qq449245884…

我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!

關注公衆號,後臺回覆福利,便可看到福利,你懂的。

相關文章
相關標籤/搜索