輕鬆學會 React 鉤子:以 useEffect-- 爲例
作者: 阮一峯
五年多前,我寫過 React 系列教程。不用說,內容已經有些過時了。
我本來不想碰它們了,覺得框架一直在升級,教程寫出來就會過時。
但是,最近我逐漸體會到 React 鉤子(hooks)非常好用,重新認識了 React 這個框架,覺得應該補上關於鉤子的部分。
下面就來談談,怎樣正確理解鉤子,並且深入剖析最重要的鉤子之一的useEffect()
。內容會盡量通俗,讓不熟悉 React 的朋友也能看懂。歡迎大家參考我以前寫的《React 框架入門》和《React 最常用的四個鉤子》。
本文得到了 開課吧 的支持,結尾有 React 視頻學習資料。希望通過視頻來系統學習 React 的同學,可以關注。
一、React 的兩套 API
以前,React API 只有一套,現在有兩套:類(class)API 和基於函數的鉤子(hooks) API。
任何一個組件,可以用類來寫,也可以用鉤子來寫。下面是類的寫法。
class Welcome extends React.Component { render() { return <h1>Hello, {this.props.name}</h1>; } }
再來看鉤子的寫法,也就是函數。
function Welcome(props) { return <h1>Hello, {props.name}</h1>; }
這兩種寫法,作用完全一樣。初學者自然會問:"我應該使用哪一套 API?"
官方推薦使用鉤子(函數),而不是類。因爲鉤子更簡潔,代碼量少,用起來比較 "輕",而類比較 "重"。而且,鉤子是函數,更符合 React 函數式的本質。
下面是類組件(左邊)和函數組件(右邊)代碼量的比較。對於複雜的組件,差的就更多。
但是,鉤子的靈活性太大,初學者不太容易理解。很多人一知半解,很容易寫出混亂不堪、無法維護的代碼。那就不如使用類了。因爲類有很多強制的語法約束,不容易搞亂。
二、類和函數的差異
嚴格地說,類組件和函數組件是有差異的。不同的寫法,代表了不同的編程方法論。
類(class)是數據和邏輯的封裝。 也就是說,組件的狀態和操作方法是封裝在一起的。如果選擇了類的寫法,就應該把相關的數據和操作,都寫在同一個 class 裏面。
函數一般來說,只應該做一件事,就是返回一個值。 如果你有多個操作,每個操作應該寫成一個單獨的函數。而且,數據的狀態應該與操作方法分離。根據這種理念,React 的函數組件只應該做一件事情:返回組件的 HTML 代碼,而沒有其他的功能。
還是以上面的函數組件爲例。
function Welcome(props) { return <h1>Hello, {props.name}</h1>; }
這個函數只做一件事,就是根據輸入的參數,返回組件的 HTML 代碼。這種只進行單純的數據計算(換算)的函數,在函數式編程裏面稱爲 "純函數"(pure function)。
三、副效應是什麼?
看到這裏,你可能會產生一個疑問:如果純函數只能進行數據計算,那些不涉及計算的操作(比如生成日誌、儲存數據、改變應用狀態等等)應該寫在哪裏呢?
函數式編程將那些跟數據計算無關的操作,都稱爲 "副效應" (side effect) 。如果函數內部直接包含產生副效應的操作,就不再是純函數了,我們稱之爲不純的函數。
純函數內部只有通過間接的手段(即通過其他函數調用),才能包含副效應。
四、鉤子(hook)的作用
說了半天,那麼鉤子到底是什麼?
一句話,鉤子(hook)就是 React 函數組件的副效應解決方案,用來爲函數組件引入副效應。 函數組件的主體只應該用來返回組件的 HTML 代碼,所有的其他操作(副效應)都必須通過鉤子引入。
由於副效應非常多,所以鉤子有許多種。React 爲許多常見的操作(副效應),都提供了專用的鉤子。
useState()
:保存狀態useContext()
:保存上下文useRef()
:保存引用- ......
上面這些鉤子,都是引入某種特定的副效應,而 useEffect()
是通用的副效應鉤子 。找不到對應的鉤子時,就可以用它。其實,從名字也可以看出來,它跟副效應(side effect)直接相關。
五、useEffect() 的用法
useEffect()
本身是一個函數,由 React 框架提供,在函數組件內部調用即可。
舉例來說,我們希望組件加載以後,網頁標題(document.title
)會隨之改變。那麼,改變網頁標題這個操作,就是組件的副效應,必須通過useEffect()
來實現。
import React, { useEffect } from 'react'; function Welcome(props) { useEffect(() => { document.title = '加載完成'; }); return <h1>Hello, {props.name}</h1>; }
上面例子中,useEffect()
的參數是一個函數,它就是所要完成的副效應(改變網頁標題)。組件加載以後,React 就會執行這個函數。(查看運行結果)
useEffect()
的作用就是指定一個副效應函數,組件每渲染一次,該函數就自動執行一次。組件首次在網頁 DOM 加載後,副效應函數也會執行。
六、useEffect() 的第二個參數
有時候,我們不希望useEffect()
每次渲染都執行,這時可以使用它的第二個參數,使用一個數組指定副效應函數的依賴項,只有依賴項發生變化,纔會重新渲染。
function Welcome(props) { useEffect(() => { document.title = `Hello, ${props.name}`; }, [props.name]); return <h1>Hello, {props.name}</h1>; }
上面例子中,useEffect()
的第二個參數是一個數組,指定了第一個參數(副效應函數)的依賴項(props.name
)。只有該變量發生變化時,副效應函數纔會執行。
如果第二個參數是一個空數組,就表明副效應參數沒有任何依賴項。因此,副效應函數這時只會在組件加載進入 DOM 後執行一次,後面組件重新渲染,就不會再次執行。這很合理,由於副效應不依賴任何變量,所以那些變量無論怎麼變,副效應函數的執行結果都不會改變,所以運行一次就夠了。
七、useEffect() 的用途
只要是副效應,都可以使用useEffect()
引入。它的常見用途有下面幾種。
- 獲取數據(data fetching)
- 事件監聽或訂閱(setting up a subscription)
- 改變 DOM(changing the DOM)
- 輸出日誌(logging)
下面是從遠程服務器獲取數據的例子。(查看運行結果)
import React, { useState, useEffect } from 'react'; import axios from 'axios'; function App() { const [data, setData] = useState({ hits: [] }); useEffect(() => { const fetchData = async () => { const result = await axios( 'https://hn.algolia.com/api/v1/search?query=redux', ); setData(result.data); }; fetchData(); }, []); return ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> ); } export default App;
上面例子中,useState()
用來生成一個狀態變量(data
),保存獲取的數據;useEffect()
的副效應函數內部有一個 async 函數,用來從服務器異步獲取數據。拿到數據以後,再用setData()
觸發組件的重新渲染。
由於獲取數據只需要執行一次,所以上例的useEffect()
的第二個參數爲一個空數組。
八、useEffect() 的返回值
副效應是隨着組件加載而發生的,那麼組件卸載時,可能需要清理這些副效應。
useEffect()
允許返回一個函數,在組件卸載時,執行該函數,清理副效應。如果不需要清理副效應,useEffect()
就不用返回任何值。
useEffect(() => { const subscription = props.source.subscribe(); return () => { subscription.unsubscribe(); }; }, [props.source]);
上面例子中,useEffect()
在組件加載時訂閱了一個事件,並且返回一個清理函數,在組件卸載時取消訂閱。
實際使用中,由於副效應函數默認是每次渲染都會執行,所以清理函數不僅會在組件卸載時執行一次,每次副效應函數重新執行之前,也會執行一次,用來清理上一次渲染的副效應。
九、useEffect() 的注意點
使用useEffect()
時,有一點需要注意。如果有多個副效應,應該調用多個useEffect()
,而不應該合併寫在一起。
function App() { const [varA, setVarA] = useState(0); const [varB, setVarB] = useState(0); useEffect(() => { const timeoutA = setTimeout(() => setVarA(varA + 1), 1000); const timeoutB = setTimeout(() => setVarB(varB + 2), 2000); return () => { clearTimeout(timeoutA); clearTimeout(timeoutB); }; }, [varA, varB]); return <span>{varA}, {varB}</span>; }
上面的例子是錯誤的寫法,副效應函數里面有兩個定時器,它們之間並沒有關係,其實是兩個不相關的副效應,不應該寫在一起。正確的寫法是將它們分開寫成兩個useEffect()
。
function App() { const [varA, setVarA] = useState(0); const [varB, setVarB] = useState(0); useEffect(() => { const timeout = setTimeout(() => setVarA(varA + 1), 1000); return () => clearTimeout(timeout); }, [varA]); useEffect(() => { const timeout = setTimeout(() => setVarB(varB + 2), 2000); return () => clearTimeout(timeout); }, [varB]); return <span>{varA}, {varB}</span>; }
十、參考鏈接
- React useEffect: 4 Tips Every Developer Should Know, Helder Esteves
- Using the Effect Hook, React
- How to fetch data with React Hooks?, Robin Wieruch
(正文完)
React 系統視頻
對於每個想進大廠的前端開發者來說,React 是繞不過的坎,面試肯定會問到,業務也很可能會用。不懂一點 React 技術棧,大大降低了個人競爭力。
退一步說,即使你用不到 React,但是它的很多思想已經影響到了整個業界,比如虛擬 DOM、JSX、函數式編程、immutable 的狀態、單向數據流等等。懂了 React,面對其他輪子時,你也能得心應手。
但是,大家都知道 React 學習曲線比較陡峭,不少人抱怨:苦苦學了 1 個多月卻進展緩慢怎麼辦?
彆着急,這裏有一份開課吧的 《React 原理剖析 + 組件化》 系統視頻。不僅講解了原理,還包括了綜合性的實戰項目,裏面用到了 react-router、redux、react-redux、antd 等 React 全家桶。
訪問這個鏈接,或者微信掃描下面的二維碼,就可以免費領取。
(完)
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://www.ruanyifeng.com/blog/2020/09/react-hooks-useeffect-tutorial.html