在這篇文章中,我將分享我對React Hooks的觀點,正如這篇文章的標題所暗示的那樣,我不是一個忠實的粉絲。javascript
讓咱們來分析一下React官方的文檔中描述的放棄類而使用鉤子的動機。前端
咱們發現,class多是學習React的一大障礙,你必須瞭解
this
在JavaScript中的工做方式,這與大多數語言中的工做方式大相徑庭。你必須記住要綁定事件處理程序,代碼會很是囉嗦,React中函數和類組件之間的區別,以及什麼時候使用每一個組件,甚至在有經驗的React開發人員之間也會致使分歧。
好吧,我能夠贊成 this
在你剛開始使用Javascript的時候可能會有點混亂,可是箭頭函數解決了混亂,把一個已經被Typescript開箱即用支持的第三階段功能稱爲「不穩定的語法建議」,這純粹是煽動性的。React團隊指的是class字段語法,該語法已經被普遍使用而且可能很快會獲得正式支持:java
class Foo extends React.Component { onPress = () => { console.log(this.props.someProp); } render() { return <Button onPress={this.onPress} /> } }
如你所見,經過使用class字段箭頭函數,你無需在構造函數中綁定任何內容,而且它始終指向正確的上下文。面試
若是Class使人困惑,那麼對於新的鉤子函數咱們能說些什麼呢?鉤子函數不是常規函數,由於它具備狀態,看起來很奇怪的this
(又名useRef
),而且能夠具備多個實例。但這絕對不是類,介於二者之間,從如今開始,我將其稱爲Funclass。那麼,對於人類和機器而言,那些Funclass會更容易嗎?我不肯定機器,但我真的不認爲Funclass從概念上比類更容易理解。編程
類是一個衆所周知的思想概念,每一個開發人員都熟悉 this
的概念,即便在javascript中也有所不一樣。另外一方面,Funclass是一個新概念,一個很奇怪的概念。它們讓人感受更神奇,並且它們過於依賴慣例而不是嚴格的語法。你必須遵循一些嚴格而奇怪的規則,你須要當心你的代碼放在哪裏,並且有不少陷阱。還要準備好一些可怕的命名,好比 useRef
( this
的花哨名字)、useEffect
、useMemo
、useImperativeHandle
(說什麼呢?)等等。設計模式
類的語法是爲了處理多實例的概念和實例範圍的概念(this
的確切目的)而專門發明的。Funclass只是一種實現相同目標的奇怪方式,許多人將Funclass與函數式編程相混淆,但Funclass實際上只是變相的類。類是一個概念,而不是語法。數組
在React中,函數和類組件之間的區別,以及什麼時候使用每一種組件,甚至在有經驗的React開發人員之間也會產生分歧。服務器
到目前爲止,這種區別很是明顯——若是須要狀態或生命週期方法,則使用類,不然,使用函數或類實際上並不重要。就我我的而言,我很喜歡這樣的想法:當我偶然發現一個函數組件時,我能夠當即知道這是一個沒有狀態的「啞吧組件」。遺憾的是,隨着Funclasses的引入,狀況再也不是這樣了。ide
具備諷刺意味嗎?至少在我看來,React最大的問題是它沒有提供一個開箱即用的狀態管理方案,讓咱們對應該如何填補這個空白的問題爭論了好久,也爲Redux等一些很是糟糕的設計模式打開了一扇門。因此在經歷了多年的挫折以後,React團隊終於得出了一個結論:組件之間很難共享有狀態邏輯......誰能想到呢?函數式編程
不管如何,勾子會使狀況變得更好嗎?答案是不盡然。鉤子不能和類一塊兒工做,因此若是你的代碼庫已經用類來編寫,你仍是須要另外一種方式來共享有狀態的邏輯。另外,鉤子只解決了每一個實例邏輯共享的問題,但若是你想在多個實例之間共享狀態,你仍然須要使用stores和第三方狀態管理解決方案,正如我所說,若是你已經使用它們,你並不真正須要鉤子。
因此,與其只是治標不治本,或許React是時候行動起來,實現一個合適的狀態管理工具,同時管理全局狀態(stores)和本地狀態(每一個實例),從而完全扼殺這個漏洞。
若是你已經在使用stores,這種說法幾乎沒有意義,讓咱們看看爲何。
class Foo extends React.Component { componentDidMount() { doA(); doB(); doC(); } }
在這個例子中,你能夠看到,咱們可能在 componentDidMount
中混合了不相關的邏輯,但這是否會使咱們的組件膨脹?不徹底是。整個實現位於類以外,而狀態位於store中,沒有store 全部狀態邏輯都必須在類內部實現,而該類確實會臃腫。但看起來React又解決了一個問題,這個問題大多存在於一個沒有狀態管理工具的世界裏。實際上,大多數大型應用程序已經在使用狀態管理工具,而且該問題已獲得緩解。另外,在大多數狀況下,咱們也許能夠將這個類分解成更小的組件,並將每一個 doSomething()
放在子組件的 componentDidMount
中。
使用Funclass,咱們能夠編寫以下代碼:
function Foo() { useA(); useB(); useC(); }
看起來有點乾淨,可是是嗎?咱們還須要在某個地方寫3個不一樣的useEffect鉤子,因此最後咱們要寫更多的代碼,看看咱們在這裏作了什麼——有了類組件,你能夠一目瞭然地知道組件在mount上作什麼。在Funclass的例子中,你須要按照鉤子並嘗試搜索帶有空依賴項數組的useEffect
,以瞭解組件在mount上作什麼。生命週期方法的聲明性本質上是一件好事,我發現研究Funclasss的流程要困可貴多。我見過不少案例是Funclasses讓開發者更容易寫出糟糕的代碼,咱們後面會看到一個例子。
可是首先,我必須認可 useEffect
有一些好處,請看如下示例:
useEffect(() => { subscribeToA(); return () => { unsubscribeFromA(); }; }, []);
useEffect
鉤子讓咱們將訂閱和退訂邏輯配對在一塊兒。這實際上是一個很是整潔的模式,一樣的,把 componentDidMount
和componentDidUpdate
配對在一塊兒也是如此。以個人經驗,這些狀況並不常見,但它們仍然是有效的用例,在這裏 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
函數將記住給定的函數,而且僅當其參數之一已更改時纔會再次調用它。經過從咱們的render函數內部觸發效果,咱們能夠確保它在每次渲染/更新時都被調用,但只有當它的一個參數被改變時,給定的函數纔會再次運行,因此咱們在結合 componentDidMount
和 componentDidUpdate
方面實現了相似 useEffect
的效果,但遺憾的是,咱們仍然須要在 componentWillUnmount
中手動進行最後的清理。另外,從render內調用效果函數也有點醜。爲了獲得和useEffect徹底同樣的效果,React須要增長對它的支持。
最重要的是 useEffect
不該該被認爲是進入funclass的有效動機,它自己就是一個有效的動機,也能夠爲類實現。
React團隊說類很難優化和最小化,funclass應該以某種方式改進,關於這件事,我只有一件事要說——給我看看數字。
我至今找不到任何論文,也沒有我能夠克隆並運行以比較Funclasses VS Class的性能的基準演示應用程序。事實上,咱們沒有看到這樣的演示並不奇怪——Funclasses須要以某種方式實現這個功能(若是你喜歡的話,也能夠用Ref),因此我很期待那些讓類難以優化的問題,也會影響到Funclasses。
無論怎麼說,全部關於性能的爭論,在不展現數據的狀況下實在是一文不值,因此咱們真的不能把它做爲論據。
你能夠找到不少經過將Class轉換爲Funclass來減小代碼的例子,但大多數甚至全部的例子都利用了 useEffect
鉤子,以便將 componentDidMount
和 componentWillUnmount
結合在一塊兒,從而達到極大的效果。
但正如我前面所說,useEffect
不該該被認爲是Funclass的優點,若是忽略它所實現的代碼減小,那麼只會留下很是小的影響。並且,若是你嘗試使用 useMemo
,useCallback
等來優化Funclass,你甚至可能獲得比等效類更冗長的代碼。
當比較小而瑣碎的組件時,Funclasses毫無疑問地贏了,由於類有一些固有的模板,不管你的類有多小你都須要付出。但在比較大的組件時,你幾乎看不出差異,有時正如我所說,類甚至能夠更乾淨。
最後,我不得不對 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
發生變化時,組件應該從新渲染以反映這些變化。就是這樣,不須要醜陋的高階組件(HOC)。
那麼,爲何React團隊選擇只改進useContext API而不是常規content API?我不知道,但這並不意味着Funclass本質上更乾淨。這意味着React應該經過爲類實現相同的API改進來作得更好。
所以,在提出有關動機的問題以後,讓咱們看一下我不喜歡的有關Funclass的其餘內容。
在Funclasses的 useEffect
實現中,最讓我困擾的一件事,就是沒有弄清楚某個組件的反作用是什麼。對於類,若是你想知道一個組件在掛載時作了什麼,你能夠很容易地檢查 componentDidMount
中的代碼或檢查構造函數。若是你看到一個重複的調用,你可能應該檢查一下 componentDidUpdate
,有了新的 useEffec
t鉤子,反作用能夠深深地嵌套在代碼中。
假設咱們檢測到一些沒必要要的服務器調用,咱們查看可疑組件的代碼,而後看到如下內容:
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(() => { //** 不少代碼,都與一些加載聯繫人的動畫有關。*// }, [loadingStatus]); //..rest of code }
好的,開始變得棘手。隱藏的反作用在哪裏?若是咱們深刻研究 useSwipeToRefresh
,咱們將看到:
export const useSwipeToRefresh = (loadingStatus) => { // ..lot's of code // ... useEffect(() => { if(loadingStatus === 'refresing') { refreshContacts(); // bingo! 咱們隱藏的反作用 } }); //<== 咱們忘記了依賴項數組! }
咱們發現了咱們的隱藏效果,refreshContacts
會在每一個組件渲染時意外地調用fetch contacts。在大型代碼庫和某些結構不良的組件中,嵌套的 useEffect
可能會形成麻煩。
我並非說你不能用類編寫糟糕的代碼,可是Funclasses更容易出錯,並且沒有嚴格定義生命週期方法的結構,更容易作糟糕的事情。
經過在類的同時增長鉤子API,React的API實際上增長了一倍。如今每一個人都須要學習兩種徹底不一樣的方法,我必須說,新API比舊API晦澀得多。一些簡單的事情,如得到以前的props和state,如今都成了很好的面試材料。你能寫一個鉤子得到以前得 props 在不借助google的狀況下?
像React這樣的大型庫必須很是當心地在API中添加如此巨大的更改,這樣作的動機甚至是不合理的。
在我看來,Funclass比類更混亂。例如,要找到組件的切入點就比較困難——用classes只需搜索 render
函數,但用Funclasses就很難發現主return語句。另外,要按照不一樣的 useEffect 語句來理解組件的流程是比較困難的,相比之下,常規的生命週期方法會給你一些很好的提示,讓你知道本身的代碼須要在哪裏尋找。若是我正在尋找某種初始化邏輯,我將跳轉(VSCode中的cmd + shift + o)到 componentDidMount
,若是我正在尋找某種更新機制,則可能會跳到 componentDidUpdate
等。經過Funclass,我發現很難在大型組件內部定位。
鉤子的主要規則(可能也是最重要的規則)之一是使用前綴約定。
你知道有什麼不對勁的感受嗎?這就是我對鉤子的感受。有時我能準確地指出問題所在,但有時只是一種廣泛的感受,即咱們走錯了方向。當你發現一個好的概念時,你能夠看到事情是如何很好地結合在一塊兒的,可是當你在爲錯誤的概念而苦惱的時候,發現你須要添加更多更具體的東西和規則,才能讓事情順利進行。
有了鉤子,就會有愈來愈多奇怪的東西跳出來,有更多「有用的」鉤子能夠幫助你作一些瑣碎的事情,也有更多的東西須要學習。若是咱們須要這麼多的utils在咱們的平常工做中,只是爲了隱藏一些奇怪的複雜,這是一個巨大的跡象,說明咱們走錯了路。
幾年前,當我從Angular 1.5轉到React時,我驚訝於React的API是如此簡單,文檔是如此的薄。Angular曾經有龐大的文檔,你可能要花上幾天的時間才能涵蓋全部內容——消化機制、不一樣的編譯階段、transclude、綁定、模板等等。光是這一點就給我很大的啓示,而React它簡潔明瞭,你能夠在幾個小時內把整個文檔看一遍就能夠了。在第一次,第二次以及之後的全部次嘗試使用鉤子的過程當中,我發現本身有義務一次又一次地使用文檔。
我討厭成爲聚會的掃興者,但我真的認爲Hooks多是React社區發生的第2件最糟糕的事情(第一名仍然由Redux佔據)。它給已經脆弱的生態系統增長了另外一場毫無用處的爭論,目前尚不清楚鉤子是不是推薦的使用方式,仍是隻是另外一個功能和我的品味的問題。
我但願React社區可以醒來,並要求在Funclass和class的功能之間保持平衡。咱們能夠在類中擁有更好的Context API,而且能夠爲類提供諸如useEffect之類的東西。若是須要,React應該讓咱們選擇繼續使用類,而不是經過僅爲Funclass添加更多功能而強行殺死它而將類拋在後面。
另外,早在2017年末,我就曾以《Redux的醜陋面》爲題發表過一篇文章,現在連Redux的創造者Dan Abramov都已經認可Redux是一個巨大的錯誤。
只是歷史在重演嗎?時間會證實一切。
不管如何,我和個人隊友決定暫時堅持用類,並使用基於Mobx的解決方案做爲狀態管理工具。我認爲,在獨立開發人員和團隊工做人員之間,Hooks的普及率存在很大差別——Hooks的不良性質在大型代碼庫中更加明顯,你須要在該代碼庫中處理其餘人的代碼。我我的真的但願React能把 ctrl+z
的鉤子所有放在一塊兒。
我打算開始着手製定一個RFC,爲React提出一個簡單、乾淨、內置的狀態管理方案,一勞永逸地解決共享狀態邏輯的問題,但願能用一種比Funclasses不那麼笨拙的方式。
首發於公衆號《前端全棧開發者》,第一時間閱讀最新文章,會優先兩天發表新文章。關注後私信回覆:大禮包,送某網精品視頻課程網盤資料,準能爲你節省很多錢!