五年多前,我寫過 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 全家桶。
訪問這個連結,或者微信掃描下面的二維碼,就可以免費領取。
(完)