Hooks
React v16.8
釋出了 Hooks
,其主要是解決跨元件、元件複用的狀態管理問題。
在 class
中元件的狀態封裝在物件中,然後通過單向資料流來組織元件間的狀態互動。這種模式下,跨元件的狀態管理變得非常困難,複用的元件也會因為要相容不同的元件變得產生很多副作用,如果對元件再次拆分,也會造成冗餘程式碼增多,和元件過多帶來的問題。
後來有了 Redux
之類的狀態管理庫,來統一管理元件狀態。但是這種分層依然會讓程式碼變得很複雜,需要更多的巢狀、狀態和方法,寫程式碼時也常在幾個檔案之間不停切換。hooks
就是為了解決以上這些問題。
文章不對 hooks
做太多詳細介紹,建議閱讀此文前,先到官網做大概的瞭解。此文基於上一篇文章《實現ssr服務端渲染》的程式碼,進行 hooks
改造。程式碼已經提交到倉庫的 hooks
分支中,倉庫連結 github.com/zimv/react-…。
物件導向程式設計和函數語言程式設計
在瞭解 hooks
的過程中,慢慢的感覺到了物件導向和函數語言程式設計的區別。
在 class
模式中狀態和屬性方法等被封裝在元件內,元件之間是相互以完整物件個體做互動,狀態的修改需要在物件內部的 setState 中處理。
而 hooks
模式中,一切皆函式,也就是 hooks
,可以被拆分成很多小單元再進行組合,修改狀態的是一個 set
方法,此方法可以在任何其他的 hooks
中出現和呼叫。 class 更屬於物件導向程式設計,而 hooks
更屬於函數語言程式設計。
React
也並不會移除 class
,而是引入 hooks
使開發者能根據場景做更好的選擇。它們依舊會在未來保持應有的迭代。
變化
使用 hooks
之後,原本的生命週期概念就會有所變化了。比如我們定義一個 hooks
元件 Index, 當元件執行時,Index
函式的呼叫就是一次 render
,那麼我們第一次 render
相當於原來的 willMount
,而 useEffect
會在第一次 render
以後執行。官網文件也說過你可以把 useEffect
Hooks
視作 componentDidMount
、componentDidUpdate
和 componentWillUnmount
的結合。 state
也被 useState
替代,useState
傳入初始值並返回變數和修改變數的 set
方法。
在我們服務端渲染的時候,上篇文章說過生命週期只會執行到 willMount
後的第一次 render
。 那在我們 hooks
模式下,服務端渲染會執行 Index hooks
第一次 render
,而 useEffect
不會被執行。
function Index(props){
console.log('render');
const [desc, setDesc] = useState("惹不起");
useEffect(() => {
console.log('effect')
})
return (<div>{desc}</div>)
}複製程式碼
useEffect
如果使用了 useEffect
,元件鉤子每次 render
以後,useEffect
會被執行。 useEffect
第一個入參是需要呼叫的方法,方法可以返回一個方法,返回的方法會在非首次執行此 useEffect
之前呼叫,也會在元件解除安裝時呼叫。
第二個引數是傳入一個陣列,是用來限制 useEffect
執行次數的,如果不傳入此引數,useEffect
會在每次 render
時執行。如果傳入第二個陣列引數,在非首次執行 useEffect
時,陣列中的變數較上一次 render
發生了變化,才會再次觸發 useEffect
執行。
看如下程式碼,當頁面首次 render
,useEffect
執行非同步資料獲取,當資料獲取成功,setList
設定值以後(類似 setState
會觸發 render
),會再次執行 render
,而 useEffect
還會再次執行,資料請求結束以後,setList
又會導致 render
,因此陷入死迴圈。
const [list, setList] = useState([]);
useEffect(() => {
API.getData().then(data=>{
if (data) {
setList(data.list);
}
});
});複製程式碼
所以需要使用第二個引數,限制執行次數,我們傳入一個 1
,就可以實現僅執行一次 useEffect
。當然也可以通過傳入一個 useState
變數。
const [list, setList] = useState([]);
useEffect(() => {
API.getData().then(data=>{
if (data) {
setList(data.list);
}
});
}, [1]);複製程式碼
class 改造
在原本的 SSR 倉庫的前提下,僅針對元件部分,進行 hooks
改造。首先回顧 getInitialProps
在 class
模式下,是在 class
寫一個 static
靜態方法,如下:
export default class Index extends Base {
static async getInitialProps() {
let data;
const res = await request.get("/api/getData");
if (!res.errCode) data = res.data;
return {
data
};
}
}複製程式碼
在 hooks
中,class
變成了普通函式,以前的繼承變得沒有必要也無法適應需求,因此 getInitialProps
直接寫在函式的屬性中,方法本身返回的資料格式依然不變,返回一個物件。如下:
function Index(props) {
}
Index.getInitialProps = async () => {
let data;
const res = await request.get("/api/getData");
if (!res.errCode) data = res.data;
return {
data
};
};複製程式碼
包括定義的網頁 title
,之前也是使用 static
,現在我們也 Index.title = 'index'
這樣定義。
hooks
規範要求如下,援引中文 React
文件:
- 只能在頂層呼叫鉤子。不要在迴圈,控制流和巢狀的函式中呼叫鉤子。
- 只能從 React 的函式式元件中呼叫鉤子。不要在常規的 JavaScript 函式中呼叫鉤子。(此外,你也可以在你的自定義鉤子中呼叫鉤子。)
在 class
模式下,我們繼承了 Base
,Base
會定義 constructor
和 componentWillMount
來處理 state
和 props
,可以幫助我們解決服務端渲染和客戶端渲染下初始化狀態資料的賦值和獲取,因此我們才可以統一一套程式碼在客戶端和服務端中執行(如需要,檢視上篇文章瞭解詳情)。
class
模式下的繼承 Base
屬於物件導向程式設計模式,而 hooks
模式下,由於需要在函式內使用 useState
來定義狀態,並且返回方法來設定狀態,這樣看起來更偏向函數語言程式設計,在這種場景下,繼承變得不適應。因此需要對 Base
進行改造,在 Base
編寫 hooks
,在頁面元件 hooks
中使用。
在 class
模式下,我們使用繼承 Base
來處理 state
和 props
,由於 Base
已經封裝了 constructor
和 componentWillMount
處理 state
和 props
,因此我們只需要定義好靜態 state
和 getInitialProps
,元件便會自動處理相關邏輯,大致使用程式碼如下
export default class Index extends Base {
static state = {
desc: "Hello world~"
};
static async getInitialProps() {
let data;
const res = await request.get("/api/getData");
if (!res.errCode) data = res.data;
return {
data
};
}
}複製程式碼
在 hooks
模式下不一樣,因為摒棄了繼承,需要用 Base
自定義 hooks
,然後在頁面元件中使用。Base
中的 getProps
和 requestInitialData
鉤子呼叫時,需要傳入當前 Index
元件的部分物件,然後在 Base hooks
中返回變數初始值值或者呼叫 set
修改當前 hooks
中的狀態值,大致使用如下:
import { getProps, requestInitialData } from "../base";
function Index(props) {
const [desc, setDesc] = useState("Hello world~");
//getProps獲取props中的ssrData,重構和服務端渲染時props有值,第三個引數為預設值
const [data, setData] = useState(getProps(props, "data", ""));
//在單頁面路由頁面跳轉,渲染元件時,requestInitialData呼叫getInitialProps
requestInitialData(props, Index, { data: setData });
return (<div>{data}</div>)
}
Index.getInitialProps = async () => {
let data;
const res = await request.get("/api/getData");
if (!res.errCode) data = res.data;
return {
data
};
};
export default Index;複製程式碼
如此封裝以後,我們依然保證了一套程式碼能在服務端和客戶端執行,requestInitialData
方法第三個傳入引數,是一個物件,傳入了需要被修改的狀態的 set
方法,最終 getInitialProps
返回資料後,會和傳入的物件對比,屬性名一致便會呼叫 set
方法進行狀態修改,requestInitialData
是一個 useEffect hook
,程式碼如下
export function requestInitialData(props, component, setFunctions) {
useEffect(() => {
//客戶端執行時
if (typeof window != "undefined") {
//非同構時,並且getInitialProps存在
if (!props.ssrData && component.getInitialProps) {
component.getInitialProps().then(data => {
if (data) {
//遍歷結果,執行set賦值
for (let key in setFunctions) {
for (let dataKey in data) {
if (key == dataKey) {
setFunctions[key](data[dataKey]);
break;
}
}
}
}
});
}
}
},[1]);
}複製程式碼
至此,針對我之前的 SSR 程式碼,就完成了 hooks
的改造。React hooks
的改造非常平滑,class
和 hooks
混用也不會造成什麼問題,如果需要在舊的專案中使用 hooks
或者對原有的 class
進行改造,完全可以慢慢的一部分一部分迭代。當然 React Hooks
還有 useContext
useReducer
等,不妨現在就去試試 Hooks ?
關聯文章:《 實現ssr服務端渲染》
關聯倉庫: github.com/zimv/react-…