React Hooks的丑陋一面
在這篇文章中,我將分享我對React Hooks的觀點,正如這篇文章的標題所暗示的那樣,我不是一個忠實的粉絲。
讓我們來分析一下React官方的文檔中描述的放棄類而使用鉤子的動機。
動機1:class令人困惑
我們發(fā)現(xiàn),class可能是學習React的一大障礙,你必須了解 this 在JavaScript中的工作方式,這與大多數(shù)語言中的工作方式截然不同。你必須記住要綁定事件處理程序,代碼會非常啰嗦,React中函數(shù)和類組件之間的區(qū)別,以及何時使用每個組件,甚至在有經(jīng)驗的React開發(fā)人員之間也會導致分歧。 |
好吧,我可以同意 this 在你剛開始使用Javascript的時候可能會有點混亂,但是箭頭函數(shù)解決了混亂,把一個已經(jīng)被Typescript開箱即用支持的第三階段功能稱為“不穩(wěn)定的語法建議”,這純粹是煽動性的。React團隊指的是class字段語法,該語法已經(jīng)被廣泛使用并且可能很快會得到正式支持:
- class Foo extends React.Component {
- onPress = () => {
- console.log(this.props.someProp);
- } render() { return <Button onPress={this.onPress} />
- }
- }
如你所見,通過使用class字段箭頭函數(shù),你無需在構造函數(shù)中綁定任何內容,并且它始終指向正確的上下文。
如果Class令人困惑,那么對于新的鉤子函數(shù)我們能說些什么呢?鉤子函數(shù)不是常規(guī)函數(shù),因為它具有狀態(tài),看起來很奇怪的 this(又名 useRef ),并且可以具有多個實例。但這絕對不是類,介于兩者之間,從現(xiàn)在開始,我將其稱為 Funclass。那么,對于人類和機器而言,那些Funclass會更容易嗎?我不確定機器,但我真的不認為Funclass從概念上比類更容易理解。
類是一個眾所周知的思想概念,每個開發(fā)人員都熟悉 this 的概念,即使在javascript中也有所不同。另一方面,F(xiàn)unclass是一個新概念,一個很奇怪的概念。它們讓人感覺更神奇,而且它們過于依賴慣例而不是嚴格的語法。你必須遵循一些嚴格而奇怪的規(guī)則,你需要小心你的代碼放在哪里,而且有很多陷阱。還要準備好一些可怕的命名,比如 useRef( this 的花哨名字)、useEffect、useMemo、useImperativeHandle(說什么呢?)等等。
類的語法是為了處理多實例的概念和實例范圍的概念(this 的確切目的)而專門發(fā)明的。Funclass只是一種實現(xiàn)相同目標的奇怪方式,許多人將Funclass與函數(shù)式編程相混淆,但Funclass實際上只是變相的類。類是一個概念,而不是語法。
在React中,函數(shù)和類組件之間的區(qū)別,以及何時使用每一種組件,甚至在有經(jīng)驗的React開發(fā)人員之間也會產(chǎn)生分歧。
到目前為止,這種區(qū)別非常明顯——如果需要狀態(tài)或生命周期方法,則使用類,否則,使用函數(shù)或類實際上并不重要。就我個人而言,我很喜歡這樣的想法:當我偶然發(fā)現(xiàn)一個函數(shù)組件時,我可以立即知道這是一個沒有狀態(tài)的“啞巴組件”。遺憾的是,隨著Funclasses的引入,情況不再是這樣了。
動機2:很難在組件之間重用有狀態(tài)邏輯
具有諷刺意味嗎?至少在我看來,React最大的問題是它沒有提供一個開箱即用的狀態(tài)管理方案,讓我們對應該如何填補這個空白的問題爭論了很久,也為Redux等一些非常糟糕的設計模式打開了一扇門。所以在經(jīng)歷了多年的挫折之后,React團隊終于得出了一個結論:組件之間很難共享有狀態(tài)邏輯......誰能想到呢?
無論如何,勾子會使情況變得更好嗎?答案是不盡然。鉤子不能和類一起工作,所以如果你的代碼庫已經(jīng)用類來編寫,你還是需要另一種方式來共享有狀態(tài)的邏輯。另外,鉤子只解決了每個實例邏輯共享的問題,但如果你想在多個實例之間共享狀態(tài),你仍然需要使用stores和第三方狀態(tài)管理解決方案,正如我所說,如果你已經(jīng)使用它們,你并不真正需要鉤子。
所以,與其只是治標不治本,或許React是時候行動起來,實現(xiàn)一個合適的狀態(tài)管理工具,同時管理全局狀態(tài)(stores)和本地狀態(tài)(每個實例),從而徹底扼殺這個漏洞。
動機3:復雜的組件變得難以理解
如果你已經(jīng)在使用stores,這種說法幾乎沒有意義,讓我們看看為什么。
- class Foo extends React.Component {
- componentDidMount() {
- doA();
- doB();
- doC();
- }
- }
在這個例子中,你可以看到,我們可能在 componentDidMount 中混合了不相關的邏輯,但這是否會使我們的組件膨脹?不完全是。整個實現(xiàn)位于類之外,而狀態(tài)位于store中,沒有store 所有狀態(tài)邏輯都必須在類內部實現(xiàn),而該類確實會臃腫。但看起來React又解決了一個問題,這個問題大多存在于一個沒有狀態(tài)管理工具的世界里。實際上,大多數(shù)大型應用程序已經(jīng)在使用狀態(tài)管理工具,并且該問題已得到緩解。另外,在大多數(shù)情況下,我們也許可以將這個類分解成更小的組件,并將每個 doSomething() 放在子組件的 componentDidMount 中。
使用Funclass,我們可以編寫如下代碼:
- function Foo() {
- useA();
- useB();
- useC();
- }
看起來有點干凈,但是是嗎?我們還需要在某個地方寫3個不同的useEffect鉤子,所以最后我們要寫更多的代碼,看看我們在這里做了什么——有了類組件,你可以一目了然地知道組件在mount上做什么。在Funclass的例子中,你需要按照鉤子并嘗試搜索帶有空依賴項數(shù)組的 useEffect,以了解組件在mount上做什么。生命周期方法的聲明性本質上是一件好事,我發(fā)現(xiàn)研究Funclasss的流程要困難得多。我見過很多案例是Funclasses讓開發(fā)者更容易寫出糟糕的代碼,我們后面會看到一個例子。
但是首先,我必須承認 useEffect 有一些好處,請看以下示例:
- useEffect(() => {
- subscribeToA(); return () => {
- unsubscribeFromA(); }; }, []);
useEffect 鉤子讓我們將訂閱和退訂邏輯配對在一起。這其實是一個非常整潔的模式,同樣的,把 componentDidMount 和 componentDidUpdate 配對在一起也是如此。以我的經(jīng)驗,這些情況并不常見,但它們仍然是有效的用例,在這里 useEffect 確實很有用。問題是,為什么我們必須使用Funclass才能獲得 useEffect?為什么我們的Class不能有類似的東西?答案是我們可以:
- class Foo extends React.Component {
- someEffect = effect((value1, value2) => {
- subscribeToA(value1, value2); return () => {
- unsubscribeFromA(); }; }) render(){ this.someEffect(this.props.value1, this.state.value2);
- return <Text>Hello world</Text>
- }}
effect 函數(shù)將記住給定的函數(shù),并且僅當其參數(shù)之一已更改時才會再次調用它。通過從我們的render函數(shù)內部觸發(fā)效果,我們可以確保它在每次渲染/更新時都被調用,但只有當它的一個參數(shù)被改變時,給定的函數(shù)才會再次運行,所以我們在結合 componentDidMount 和 componentDidUpdate 方面實現(xiàn)了類似 useEffect 的效果,但遺憾的是,我們仍然需要在 componentWillUnmount 中手動進行最后的清理。另外,從render內調用效果函數(shù)也有點丑。為了得到和useEffect完全一樣的效果,React需要增加對它的支持。
最重要的是 useEffect 不應該被認為是進入funclass的有效動機,它本身就是一個有效的動機,也可以為類實現(xiàn)。
動機4:性能
React團隊說類很難優(yōu)化和最小化,funclass應該以某種方式改進,關于這件事,我只有一件事要說——給我看看數(shù)字。
我至今找不到任何論文,也沒有我可以克隆并運行以比較Funclasses VS Class的性能的基準演示應用程序。事實上,我們沒有看到這樣的演示并不奇怪——Funclasses需要以某種方式實現(xiàn)這個功能(如果你喜歡的話,也可以用Ref),所以我很期待那些讓類難以優(yōu)化的問題,也會影響到Funclasses。
不管怎么說,所有關于性能的爭論,在不展示數(shù)據(jù)的情況下實在是一文不值,所以我們真的不能把它作為論據(jù)。
動機5:Funclass不太冗長
你可以找到很多通過將Class轉換為Funclass來減少代碼的例子,但大多數(shù)甚至所有的例子都利用了 useEffect 鉤子,以便將 componentDidMount 和 componentWillUnmount 結合在一起,從而達到極大的效果。
但正如我前面所說,useEffect 不應該被認為是Funclass的優(yōu)勢,如果忽略它所實現(xiàn)的代碼減少,那么只會留下非常小的影響。而且,如果你嘗試使用 useMemo,useCallback 等來優(yōu)化Funclass,你甚至可能得到比等效類更冗長的代碼。
當比較小而瑣碎的組件時,F(xiàn)unclasses毫無疑問地贏了,因為類有一些固有的模板,無論你的類有多小你都需要付出。但在比較大的組件時,你幾乎看不出差別,有時正如我所說,類甚至可以更干凈。
最后,我不得不對 useContext 說幾句:useContext其實比我們目前原有的類的context API有很大的改進。但是再一次,為什么我們不能為類也有這樣漂亮而簡潔的API呢? 為什么我們不能做這樣的事情。
- //inside "./someContext" :
- export const someContext = React.Context({helloText: 'bla'});
- //inside "Foo":
- import {someContext} from './someContext';
- class Foo extends React.component {
- render() {
- <View>
- <Text>{someContext.helloText}</Text>
- </View>
- }
- }
當上下文中的 helloText 發(fā)生變化時,組件應該重新渲染以反映這些變化。就是這樣,不需要丑陋的高階組件(HOC)。
那么,為什么React團隊選擇只改進useContext API而不是常規(guī)content API?我不知道,但這并不意味著Funclass本質上更干凈。這意味著React應該通過為類實現(xiàn)相同的API改進來做得更好。
因此,在提出有關動機的問題之后,讓我們看一下我不喜歡的有關Funclass的其他內容。
隱藏的副作用
在Funclasses的 useEffect 實現(xiàn)中,最讓我困擾的一件事,就是沒有弄清楚某個組件的副作用是什么。對于類,如果你想知道一個組件在掛載時做了什么,你可以很容易地檢查 componentDidMount 中的代碼或檢查構造函數(shù)。如果你看到一個重復的調用,你可能應該檢查一下 componentDidUpdate,有了新的 useEffect 鉤子,副作用可以深深地嵌套在代碼中。
假設我們檢測到一些不必要的服務器調用,我們查看可疑組件的代碼,然后看到以下內容:
- const renderContacts = (props) => {
- const [contacts, loadMoreContacts] = useContacts(props.contactsIds);
- return (
- <SmartContactList contacts={contacts}/>
- )
- }
這里沒什么特別的,我們應該研究 SmartContactList,還是應該深入研究 useContacts?讓我們深入研究一下 useContacts 吧:
- export const useContacts = (contactsIds) => {
- const {loadedContacts, loadingStatus} = useContactsLoader(); const {isRefreshing, handleSwipe} = useSwipeToReresh(loadingStatus); // ... many other useX() functions
- useEffect(() => {
- //** 很多代碼,都與一些加載聯(lián)系人的動畫有關。*//
- }, [loadingStatus]); //..rest of code
- }
好的,開始變得棘手。隱藏的副作用在哪里?如果我們深入研究 useSwipeToRefresh,我們將看到:
- export const useSwipeToRefresh = (loadingStatus) => {
- // ..lot's of code
- // ...
- useEffect(() => {
- if(loadingStatus === 'refresing') {
- refreshContacts(); // bingo! 我們隱藏的副作用
- }
- }); //<== 我們忘記了依賴項數(shù)組!
- }
我們發(fā)現(xiàn)了我們的隱藏效果,refreshContacts 會在每個組件渲染時意外地調用fetch contacts。在大型代碼庫和某些結構不良的組件中,嵌套的 useEffect 可能會造成麻煩。
我并不是說你不能用類編寫糟糕的代碼,但是Funclasses更容易出錯,而且沒有嚴格定義生命周期方法的結構,更容易做糟糕的事情。
膨脹的API
通過在類的同時增加鉤子API,React的API實際上增加了一倍?,F(xiàn)在每個人都需要學習兩種完全不同的方法,我必須說,新API比舊API晦澀得多。一些簡單的事情,如獲得之前的props和state,現(xiàn)在都成了很好的面試材料。你能寫一個鉤子獲得之前得 props 在不借助google的情況下?
像React這樣的大型庫必須非常小心地在API中添加如此巨大的更改,這樣做的動機甚至是不合理的。
缺乏說明性
在我看來,F(xiàn)unclass比類更混亂。例如,要找到組件的切入點就比較困難——用classes只需搜索 render 函數(shù),但用Funclasses就很難發(fā)現(xiàn)主return語句。另外,要按照不同的 useEffect 語句來理解組件的流程是比較困難的,相比之下,常規(guī)的生命周期方法會給你一些很好的提示,讓你知道自己的代碼需要在哪里尋找。如果我正在尋找某種初始化邏輯,我將跳轉(VSCode中的cmd + shift + o)到 componentDidMount,如果我正在尋找某種更新機制,則可能會跳到 componentDidUpdate 等。通過Funclass,我發(fā)現(xiàn)很難在大型組件內部定位。
約定驅動的API
鉤子的主要規(guī)則(可能也是最重要的規(guī)則)之一是使用前綴約定。
就是感覺不對
你知道有什么不對勁的感覺嗎?這就是我對鉤子的感覺。有時我能準確地指出問題所在,但有時只是一種普遍的感覺,即我們走錯了方向。當你發(fā)現(xiàn)一個好的概念時,你可以看到事情是如何很好地結合在一起的,但是當你在為錯誤的概念而苦惱的時候,發(fā)現(xiàn)你需要添加更多更具體的東西和規(guī)則,才能讓事情順利進行。
有了鉤子,就會有越來越多奇怪的東西跳出來,有更多“有用的”鉤子可以幫助你做一些瑣碎的事情,也有更多的東西需要學習。如果我們需要這么多的utils在我們的日常工作中,只是為了隱藏一些奇怪的復雜,這是一個巨大的跡象,說明我們走錯了路。
幾年前,當我從Angular 1.5轉到React時,我驚訝于React的API是如此簡單,文檔是如此的薄。Angular曾經(jīng)有龐大的文檔,你可能要花上幾天的時間才能涵蓋所有內容——消化機制、不同的編譯階段、transclude、綁定、模板等等。光是這一點就給我很大的啟示,而React它簡潔明了,你可以在幾個小時內把整個文檔看一遍就可以了。在第一次,第二次以及以后的所有次嘗試使用鉤子的過程中,我發(fā)現(xiàn)自己有義務一次又一次地使用文檔。
總結
我討厭成為聚會的掃興者,但我真的認為Hooks可能是React社區(qū)發(fā)生的第2件最糟糕的事情(第一名仍然由Redux占據(jù))。它給已經(jīng)脆弱的生態(tài)系統(tǒng)增加了另一場毫無用處的爭論,目前尚不清楚鉤子是否是推薦的使用方式,還是只是另一個功能和個人品味的問題。
我希望React社區(qū)能夠醒來,并要求在Funclass和class的功能之間保持平衡。我們可以在類中擁有更好的Context API,并且可以為類提供諸如useEffect之類的東西。如果需要,React應該讓我們選擇繼續(xù)使用類,而不是通過僅為Funclass添加更多功能而強行殺死它而將類拋在后面。
另外,早在2017年底,我就曾以《Redux的丑陋面》為題發(fā)表過一篇文章,如今連Redux的創(chuàng)造者Dan Abramov都已經(jīng)承認Redux是一個巨大的錯誤。

只是歷史在重演嗎?時間會證明一切。
無論如何,我和我的隊友決定暫時堅持用類,并使用基于Mobx的解決方案作為狀態(tài)管理工具。我認為,在獨立開發(fā)人員和團隊工作人員之間,Hooks的普及率存在很大差異——Hooks的不良性質在大型代碼庫中更加明顯,你需要在該代碼庫中處理其他人的代碼。我個人真的希望React能把 ctrl+z 的鉤子全部放在一起。
我打算開始著手制定一個RFC,為React提出一個簡單、干凈、內置的狀態(tài)管理方案,一勞永逸地解決共享狀態(tài)邏輯的問題,希望能用一種比Funclasses不那么笨拙的方式。