useEffect Hook 是如何工作的(前端需要懂的知識點)

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

想象一下:你有一個非常好用的函數組件,然後有一天,咱們需要向它添加一個生命週期方法。

呃…

剛開始咱們可能會想怎麼能解決這個問題,然後最後變成,通常的做法是將它轉換成一個類。但有時候咱們就是要用函數方式,怎麼破?useEffect hook 出現就是為了解決這種情況。

使用useEffect,可以直接在函數組件內處理生命週期事件。 如果你熟悉 React class 的生命週期函數,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 這三個函數的組合。來看看例子:

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>Re-render/<button>
<button>Show/Hide LifecycleDemo/<button>
{mounted && <lifecycledemo>}
>
);
}
ReactDOM.render(, document.querySelector('#root'));

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


useEffect Hook 是如何工作的(前端需要懂的知識點)


現在,點擊Re-render按鈕。每次點擊,它都會打render!,還會打印umounting,這似乎是奇怪的。


useEffect Hook 是如何工作的(前端需要懂的知識點)


為啥每次渲染都會打印 '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 不依賴於 props 或 state 中的任何值,所以它永遠都不需要重複執行。這並不屬於特殊情況 —— 它依然遵循依賴數組的工作方式。

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

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

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

只在掛載的時候執行

在這個例子中,一起來看下如何使用useEffect和useRef 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 (
ref={inputRef}
value={value}
onChange={e => setValue(e.target.value)}
/>
);
}
ReactDOM.render(, document.querySelector("#root"));

在頂部,我們使用useRef創建一個空的ref。 將它傳遞給input的ref 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 (

    {posts.map(post => (
  • {post.title}

  • ))}

);
}
ReactDOM.render(
<reddit>,
document.querySelector("#root")
);

注意到咱們沒有將第二個參數傳遞給useEffect,這是不好的,不要這樣做。

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

為了解決這個問題,我們需要傳遞一個數組作為第二個參數,數組內容又是啥呢。

useEffect所依賴的唯一變量是setPosts。因此,咱們應該在這裡傳遞數組[setPosts]。因為setPosts是useState返回的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 (

    {posts.map(post => (
  • {post.title}

  • ))}

);
}
ReactDOM.render(
<reddit>,
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 (
<>

<reddit>
>
);
}
ReactDOM.render(, 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的值。

與 componentDidMount、componentDidUpdate 不同的是,在瀏覽器完成佈局與繪製之後,傳給useEffect 的函數會延遲調用。這使得它適用於許多常見的副作用場景,比如如設置訂閱和事件處理等情況,因此不應在函數中執行阻塞瀏覽器更新屏幕的操作。

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

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

原文:https://daveceddia.com/useeffect-hook-examples/

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


分享到:


相關文章: