【重構(gòu)】使用 Hooks 讓代碼更易于變更
作者:alisecued
來源:SegmentFault 思否社區(qū)
重構(gòu)過程中,肯定會遇到新的代碼如何做技術(shù)選型的問題,要考慮到這套技術(shù)的生命力,也就是他是否是更新的技術(shù),還有他的靈活和拓展性,期望能夠達(dá)到在未來至少 3 年內(nèi)不需要做大的技術(shù)棧升級。我的這次重構(gòu)經(jīng)歷是把 jQuery 的代碼變?yōu)?React ,你品品,算是最難,勞動最密集的重構(gòu)任務(wù)了吧。看多了之前代碼動輒上千行的 Class ,混亂的全局變量使用,越來越覺得,代碼一定要寫的簡單,不要使用過多的黑科技,尤其是各種設(shè)計模式,為了復(fù)用而迭代出來的海量 if 判斷。代碼不是給機(jī)器看的,是給人看的,他需要讓后來人快速的看懂,還要能讓別人在你的代碼的基礎(chǔ)上快速的迭代新的需求。所以我們需要想清楚,用什么技術(shù)棧,怎么組織代碼。
為什么要用 Function Component
對于 Class Component 和 Function Component 之爭由來已久。從我自身的實踐來看,我覺得這是兩種不同的編程思路。
| Class Component? | 面向?qū)ο缶幊?/th> | 繼承 | 生命周期 |
|---|---|---|---|
| Function Component? | 函數(shù)式編程 | 組合 | 數(shù)據(jù)驅(qū)動 |
為什么不用 Class
首先,如果我們使用面向?qū)ο筮@種編程方式,我們要注意,他不只是定義一個 Class 那么簡單的事情,我們知道面向?qū)ο笥腥筇匦?,繼承,封裝,多態(tài)。
首先前端真的適合繼承的方式嗎?準(zhǔn)確的說,UI 真的適合繼承的方式嗎?在真實世界里,抽象的東西更適合定義成一個類,類本來的意思就是分類和類別,正如我們把老虎,貓,獅子這些生物統(tǒng)稱為動物,所以我們就可以定義一個動物的類,但是真實世界并沒有動物這種實體,但是頁面 UI 都是真實存在可以看到的東西,我們可以把一個頁面分成不同的區(qū)塊,然后區(qū)塊之間采用的是「組合」的方式。因此我認(rèn)為 UI 組件不適合繼承,更應(yīng)該組合。如果你寫過繼承類的組件,你將很難去重構(gòu),甚至是重寫他。
封裝講究使用封裝好的方法對外暴露類中的屬性,但是我們的組件基本是通過 props 暴露內(nèi)部事件和數(shù)據(jù),通過 Ref 暴露內(nèi)部方法,本質(zhì)上并沒有使用封裝的特性。
多態(tài)就更少用了,多態(tài)更多是基于接口,或者抽象類的,但是 JS 這塊比較弱,用 TS 或許會好一些。
綜上,作為前端 UI 編程,我更傾向于使用函數(shù)組合的方式。
為什么要用數(shù)據(jù)變化驅(qū)動
不論是在 React 或者在 Vue 里,都講究數(shù)據(jù)的變化,數(shù)據(jù)與視圖的綁定關(guān)系,數(shù)據(jù)驅(qū)動,數(shù)據(jù)的變化引起 UI 的重新渲染,但是生命周期在描述這個問題的時候,并不直接,在 Class Component 里,我們?nèi)绾螜z測某個數(shù)據(jù)的變化呢,基本是用 shouldUpdate 的生命周期,為什么我們在編程的時候,正在關(guān)注數(shù)據(jù)和業(yè)務(wù)的時候,還要關(guān)心一個生命周期呢,這部分內(nèi)容對于業(yè)務(wù)來說更像是副作用,或者不應(yīng)該暴露給開發(fā)者的。
綜上,是我認(rèn)為 Function Component + Hooks 編程體驗更好的地方,但是這也只是一個相對片面的角度,并沒有好壞之分,畢竟連 React 的官方都說,兩種寫法沒有好壞之分,性能差距也幾乎可以忽略,而且 React 會長期支持這兩種寫法。
hooks:真正的響應(yīng)式編程
到底是什么是響應(yīng)式編程?大家各執(zhí)一詞,模模糊糊,懵懵懂懂。很多人沒有把他的本質(zhì)說明白。從我多年的編程經(jīng)驗來看,響應(yīng)式編程就是「使用異步數(shù)據(jù)流編程」。我們來看看前端在處理異步操作的時候通常是怎么做的,常見的異步操作有異步請求和頁面的鼠標(biāo)操作事件,在處理這樣的操作的時候,我們通常采取的方法是事件循環(huán),也就是異步事件流的方式。但是事件循環(huán)并沒有顯式的解決事件依賴問題,而是需要我們自己在編碼的時候做好調(diào)用順序的管理,比如:
const?x?=?1;
const?a?=?(x)?=>?new?Promise((r,?j)=>{
??const?y?=?x?+?1;
????r(y);
});
const?b?=?(y)?=>?new?Promise((r,?j)=>{
??const?z?=?y?+?1;
????r(z);
});
const?c?=?(z)?=>??new?Promise((r,?j)=>{
??const?w?=?z?+?1;
????r(w);
});
//?上面是三個異步請求,他們之間有依賴關(guān)系,我們通常的操作是
a(x).then((y)=>{
????b(y).then((z)=>{
??????c(z).then((w)=>{
??????????//?最終的結(jié)果
??????console.log(w);
??????})
??})
})
上述的基于事件流的回調(diào)方式,我們使用 Hooks 來替換的話,就是這樣的:
import?{?useState,?useEffect?}?from?'react';
const?useA?=?(x)?=>?{
????const?[y,?setY]?=?useState();
??useEffect(()=>{
????//?假設(shè)此處包含異步請求
??????setY(x?+?1);
??},?[x]);
??return?y;
}
const?useB?=?(y)?=>?{
????const?[z,?setZ]?=?useState();
??useEffect(()=>{
????//?假設(shè)此處包含異步請求
??????setZ(y?+?1);
??},?[y]);
??return?z;
}
const?useC?=?(z)?=>?{
????const?[w,?setW]?=?useState();
??useEffect(()=>{
????//?假設(shè)此處包含異步請求
??????setW(z?+?1);
??},?[z]);
??return?w;
}
//?上面是三個是自定義?Hooks,他表明了每個變量數(shù)據(jù)之間的依賴關(guān)系,你甚至不需要
//?知道他們每個異步請求的返回順序,只需要知道數(shù)據(jù)是否發(fā)生了變化。
const?x?=?1;
const?y?=?useA(x);
const?z?=?useB(y);
const?w?=?useC(z);
//?最終的結(jié)果
console.log(w);
我們從上面的例子看到, Hooks 的寫法,簡直就像是在進(jìn)行簡單的過程式編程一樣,步驟化,邏輯清晰,而且每個自定義 Hooks 你可以把他理解為一個函數(shù),他不需要與外界共享狀態(tài),他是自封閉的,可以很方便的進(jìn)行測試。
開始精簡代碼
我們基于 React Hooks 提供的工具和上面講的響應(yīng)式編程的思維,開始我們的精簡代碼之旅,這次旅程可以概括為:遇到千行代碼文件怎么辦?拆分最有效!怎么拆分?先按照功能模塊來分文件,這里的功能模塊是指相同的語法結(jié)構(gòu),比如副作用函數(shù),事件處理函數(shù)等。單個文件內(nèi)可以按照具體實現(xiàn)寫多個自定義 Hooks 和函數(shù)。這樣做的最終目的就是,讓主文件里只保留這個組件要實現(xiàn)的業(yè)務(wù)邏輯的步驟。

為什么會有上千行的單個代碼文件?
如果我們把一個組件的所有代碼都寫到一個組件里,那么極有可能會出現(xiàn)一個文件里有上千行代碼的情況,如果你用的是 Function Component 來寫這個組件的話,那么就會出現(xiàn)一個函數(shù)里有上千行代碼的情況。當(dāng)然上千行代碼的文件對于一個健全的開發(fā)者來說都是不可忍受的,對于后來的重構(gòu)者來說也是一個大災(zāi)難。
為什么要把這個代碼都放到一個文件里?拆分下不香嗎?那下面的問題就變成了如何拆分一個組件,要拆分一個組件,我們要先知道一個典型的組件是什么樣子的。
一個典型的組件
Hooks 是個新東西,他像函數(shù)一樣靈活,甚至不包含我選用了上面的方式來編寫新的代碼,那我們來看看一個典型的基于 Function Component + Hooks 的組件包含什么?
import?React,?{?useState,?useEffect?}?from?'react';
import?PropTypes?from?'prop-types';
import?{
??Row,?Select,
}?from?'antd';
import?Service?from?'@/services';
let?originList?=?[];
const?Demo?=?({
??onChange,
??value,
??version,
})?=>?{
??//?狀態(tài)管理
??const?[list,?setList]?=?useState([]);
??//?副作用函數(shù)?
??useEffect(()?=>?{
????const?init?=?async?()?=>?{
????????const?list?=?await?Service.getList(version);
????????originList?=?list;
????????setList(list);
????};
????init();
??},?[]);
??//?事件?handler
??const?onChangeHandler?=?useCallback((data)?=>?{
????const?item?=?{?...val,?value:?val.code,?label:?val.name?};
????onChange(item);
??},?[onChange]);
??
??const?onSearchHandler?=?useCallback((val)?=>?{
????if?(val)?{
??????const?listFilter?=?originList.filter(item?=>?item.name.indexOf(val)?>?-1);
??????setList(listFilter);
????}?else?{
??????setList(originList);
????}
??},?[]);
??
??//?UI?組件渲染
??return?(
????|
????????
????
??);
};
export?default?Demo;
從上面的例子我們可以看出,一個基本的 Function Component 包含哪些功能模塊:
useState 為主的狀態(tài)管理 useEffect 為主的副作用管理 useCallback 為主的事件 handler UI 部分 轉(zhuǎn)換函數(shù),用于請求返回數(shù)據(jù)的轉(zhuǎn)換,或者一些不具有通用性的工具函數(shù)
拆分功能模塊
|—?container
????????|—?hooks.js?//?各種自定義的?hooks
??????|—?handler.js?//?轉(zhuǎn)換函數(shù),以及不需要?hooks?的事件處理函數(shù)
????????|—?index.js?//?主文件,只保留實現(xiàn)步驟
????????|—?index.css?//?css?文件
什么樣的代碼一看就懂?
Hooks 是一個做代碼拆分的高效工具,但是他也非常的靈活,業(yè)界一直沒有比較通用行的編碼規(guī)范,但是我有點不同的觀點,我覺得他不需要像 Redux 一樣的模式化的編碼規(guī)范,因為他就是函數(shù)式編程,他遵循函數(shù)式編程的一般原則,函數(shù)式編程最重要的是拆分好步驟和實現(xiàn)細(xì)節(jié),這樣的代碼就好讀,好讀的代碼才是負(fù)責(zé)任的代碼。
步驟和細(xì)節(jié)分清楚以后,對重構(gòu)也有很大的好處,因為每個步驟都是一個函數(shù),不會有像 class 中 this 這種全局變量,當(dāng)你需要刪除一個步驟或者重寫這個步驟的時候,不用影響到其他步驟函數(shù)。
同樣,函數(shù)化以后,無疑單元測試就變得非常簡單了。
按照步驟拆分主文件
import?React,?{?useState,?useEffect?}?from?'react';
import?PropTypes?from?'prop-types';
import?{
??Row,?Select,
}?from?'antd';
import?{?onChangeHandler?}?from?'./handler';
import?{?useList?}?from?'./hooks';
import?Service?from?'@/services';
const?Demo?=?({
??onChange,
??value,
??version,
})?=>?{
??//?list?狀態(tài)的操作,其中有搜索改變?list?
??const?[originList,?list,?onSearchHandler]?=?useList(version);
??
??//?UI?組件渲染
??return?(
????|
??????
????
??);
};
export?default?Demo;
對 list 數(shù)據(jù)的操作 UI 渲染
hooks.js 里文件內(nèi)容如下:
import?{?useState,?useEffect,?useCallback?}?from?'react';
let?originList?=?[];
export?const?useList?=?(version)?=>?{
??//?狀態(tài)管理
??const?[list,?setList]?=?useState([]);
???//?副作用函數(shù)?
??useEffect(()?=>?{
????const?init?=?async?()?=>?{
????????const?list?=?await?Service.getList(version);
????????originList?=?list;
????????setList(list);
????};
????init();
??},?[]);
??
??//?處理?select?搜索
??const?onSearchHandler?=?useCallback((val)?=>?{
????if?(val)?{
??????const?listFilter?=?originList.filter(item?=>?item.name.indexOf(val)?>?-1);
??????setList(listFilter);
????}?else?{
??????setList(originList);
????}
??},?[]);
??
??return?[originList,?list,?onSearchHandler];
}
//?事件?handler
export?const?onChangeHandler?=?(originList,?data,?onChange)?=>?{
??const?val?=?originList.find(option?=>?(option.id?===?data.value));
??const?item?=?{?...val,?value:?val.code,?label:?val.name?};
??onChange(item);
};
編碼價值觀 ETC
參考:
響應(yīng)式編程?
https://zhuanlan.zhihu.com/p/27678951
《程序員修煉之道》Andrew Hunt, David Thomas

