useEffect 是 React Hooks 的核心,要保證理解它的運行機制和正確的使用方法才能避免這樣那樣的坑。在以前的工作中,因爲它我碰到過無數個坑,比如拿到的值是舊的,該執行的時候不執行,不該執行的時候執行了……
所以爲了避免如上尷尬、節省絞盡腦汁找 bug 的時間、保護咱們的髮際線,一定要認真學會如何正確使用 useEffect hook。
什麼是 Effect
俗話說,知已知彼,百戰不殆,我們先了解下神馬是 Effect。其實大家在開發過程中或多或少的都接觸過 side effect 的概念,即副作用 —— 對於數據抓取,註冊監聽事件,修改 DOM 元素等馬後炮式的操作都屬於副作用,因爲我們渲染出來的頁面是靜態的,任何在之後的操作都會對它產生影響,所以才稱之爲副作用。而 useEffect
則是專門用來編寫副作用代碼的,這也是 React 的核心所在。
與生命週期的關係
目前市面上的文章,包括官方文檔都讓我們把 useEffect
想象成 componentDidMount
, componentDidUpdate
,componentWillUnmount
三個生命週期的結合體。其實並不然,如果非要把 useEffect
的運行機制往生命週期上靠,會造成一些邏輯上的困惑,進而產生 bug。我們所要做的,就是把 useEffect
當成一個全新的特性,專門爲函數式組件服務的,這樣用起來纔不會迷茫。下面我們通過實例來演示它的各種用法。
運行時機
useEffect
必然會在 render 的時候執行一次,其他的運行時機取決於以下情況:
- 有沒有第二個參數。
useEffect
hook 接受兩個參數,第一個是要執行的代碼,第二個是一個數組,指定一組依賴的變量,其中任何一個變量發生變化時,此 effect 都會重新執行一次。 - 有沒有返回值。
useEffect
的執行代碼中可以返回一個函數,在每一次新的 render 進行前或者組件 unmount 之時,都會執行此函數,進行清理工作。
我們先看一個簡單的例子,想看完整代碼和隨意把玩的,請點擊下邊按鈕
我們首先看最頂層 <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
的文本爲當前文章的數量 - 追加
p
到body
的最後
在這裏,此 effect 並沒有返回任何值,也沒有給它傳遞任何一個參數,那會是什麼樣的效果呢?
答案是,此 effect 會在每次 count
或 showList
改變時每點擊一次 顯示
或 增加數量
按鈕,我們新追加的 p
都會再追加一次。這也是造成內容泄露的坑,如果我們在這裏添加了太多耗內存的東西而沒有清理,不用多久瀏覽器就崩潰了~ 解決方法很簡單,給 useEffect
添加一個返回值,並在裏邊刪除我們追加的 p
元素即可:
useEffect(() => {
let p = document.createElement("p");
p.innerHTML = `當前文章數量:${count}`;
document.body.append(p);
return () => {
p.remove();
};
});
這樣我們在點擊按鈕的時候,確保只有一個 p
在當前頁面上。看,這樣寫起來是不是比分散在 componentDidMount
和 componentWillUnmount
中方便多了?我們可以方便的在同一個作用域中方便的拿到 p
的引用,直接刪除它即可。
類實際工作的例子 - 抓取數據
爲了繼續深入 useEffect
hook,我仿照實際工作遇到的情況,編寫了一個例子,這裏我們用 useEffect
進行數據抓取,同樣的顯示博客文章列表,完整代碼請點擊下方按鈕查看:
在本例中,<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 執行時,會更新 posts
和 loading
這兩個 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
依賴,這樣每次點擊 更改佈局
按鈕時,文章列表都會加載一次,這種適合在佈局改變時需要重新請求數據的情況:
如果不需要重新加載數據,只需要把 vertical
從依賴數組裏去掉就可以了。
劃重點
看看大家對頻繁使用的 useEffect
的用法用對了沒有?來標一下重點:
- 它可不完全是
componentDidMount
,componentDidUpdate
,componentWillUnmount
三個生命週期的結合體哦(人家有自己的想法)。 - 會在每次 render 的時候必定執行一次。
- 如果返回了函數,那麼在下一次 render 之前或組件 unmount 之前必定會運行一次返回函數的代碼。
- 如果指定了依賴數組,且不爲空,則當數組裏的每個元素髮生變化時,都會重新運行一次。
- 如果數組爲空,則只在第一次 render 時執行一次,如果有返回值,則同 3。
- 如果在
useEffect
中更新了 state,且沒有指定依賴數組,或 state 存在於依賴數組中,就會造成死循環。
大家掌握了嗎?有什麼問題歡迎評論或私信我!如果覺得文章有幫助請關注博主我哦,感謝,比心。