React Hooks的醜陋一面

image

在這篇文章中,我將分享我對React Hooks的觀點,正如這篇文章的標題所暗示的那樣,我不是一個忠實的粉絲。javascript

讓咱們來分析一下React官方的文檔中描述的放棄類而使用鉤子的動機。前端

動機1:class使人困惑

咱們發現,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 的花哨名字)、useEffectuseMemouseImperativeHandle(說什麼呢?)等等。設計模式

類的語法是爲了處理多實例的概念和實例範圍的概念(this 的確切目的)而專門發明的。Funclass只是一種實現相同目標的奇怪方式,許多人將Funclass與函數式編程相混淆,但Funclass實際上只是變相的類。類是一個概念,而不是語法。數組

在React中,函數和類組件之間的區別,以及什麼時候使用每一種組件,甚至在有經驗的React開發人員之間也會產生分歧。服務器

到目前爲止,這種區別很是明顯——若是須要狀態或生命週期方法,則使用類,不然,使用函數或類實際上並不重要。就我我的而言,我很喜歡這樣的想法:當我偶然發現一個函數組件時,我能夠當即知道這是一個沒有狀態的「啞吧組件」。遺憾的是,隨着Funclasses的引入,狀況再也不是這樣了。ide

動機2:很難在組件之間重用有狀態邏輯

具備諷刺意味嗎?至少在我看來,React最大的問題是它沒有提供一個開箱即用的狀態管理方案,讓咱們對應該如何填補這個空白的問題爭論了好久,也爲Redux等一些很是糟糕的設計模式打開了一扇門。因此在經歷了多年的挫折以後,React團隊終於得出了一個結論:組件之間很難共享有狀態邏輯......誰能想到呢?函數式編程

不管如何,勾子會使狀況變得更好嗎?答案是不盡然。鉤子不能和類一塊兒工做,因此若是你的代碼庫已經用類來編寫,你仍是須要另外一種方式來共享有狀態的邏輯。另外,鉤子只解決了每一個實例邏輯共享的問題,但若是你想在多個實例之間共享狀態,你仍然須要使用stores和第三方狀態管理解決方案,正如我所說,若是你已經使用它們,你並不真正須要鉤子。

因此,與其只是治標不治本,或許React是時候行動起來,實現一個合適的狀態管理工具,同時管理全局狀態(stores)和本地狀態(每一個實例),從而完全扼殺這個漏洞。

動機3:複雜的組件變得難以理解

若是你已經在使用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 鉤子讓咱們將訂閱和退訂邏輯配對在一塊兒。這實際上是一個很是整潔的模式,一樣的,把 componentDidMountcomponentDidUpdate 配對在一塊兒也是如此。以個人經驗,這些狀況並不常見,但它們仍然是有效的用例,在這裏 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函數內部觸發效果,咱們能夠確保它在每次渲染/更新時都被調用,但只有當它的一個參數被改變時,給定的函數纔會再次運行,因此咱們在結合 componentDidMountcomponentDidUpdate 方面實現了相似 useEffect 的效果,但遺憾的是,咱們仍然須要在 componentWillUnmount 中手動進行最後的清理。另外,從render內調用效果函數也有點醜。爲了獲得和useEffect徹底同樣的效果,React須要增長對它的支持。

最重要的是 useEffect 不該該被認爲是進入funclass的有效動機,它自己就是一個有效的動機,也能夠爲類實現。

動機4:性能

React團隊說類很難優化和最小化,funclass應該以某種方式改進,關於這件事,我只有一件事要說——給我看看數字

我至今找不到任何論文,也沒有我能夠克隆並運行以比較Funclasses VS Class的性能的基準演示應用程序。事實上,咱們沒有看到這樣的演示並不奇怪——Funclasses須要以某種方式實現這個功能(若是你喜歡的話,也能夠用Ref),因此我很期待那些讓類難以優化的問題,也會影響到Funclasses。

無論怎麼說,全部關於性能的爭論,在不展現數據的狀況下實在是一文不值,因此咱們真的不能把它做爲論據。

動機5:Funclass不太冗長

你能夠找到不少經過將Class轉換爲Funclass來減小代碼的例子,但大多數甚至全部的例子都利用了 useEffect 鉤子,以便將 componentDidMountcomponentWillUnmount 結合在一塊兒,從而達到極大的效果。

但正如我前面所說,useEffect 不該該被認爲是Funclass的優點,若是忽略它所實現的代碼減小,那麼只會留下很是小的影響。並且,若是你嘗試使用 useMemouseCallback 等來優化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

經過在類的同時增長鉤子API,React的API實際上增長了一倍。如今每一個人都須要學習兩種徹底不一樣的方法,我必須說,新API比舊API晦澀得多。一些簡單的事情,如得到以前的props和state,如今都成了很好的面試材料。你能寫一個鉤子得到以前得 props 在不借助google的狀況下?

像React這樣的大型庫必須很是當心地在API中添加如此巨大的更改,這樣作的動機甚至是不合理的。

缺少說明性

在我看來,Funclass比類更混亂。例如,要找到組件的切入點就比較困難——用classes只需搜索 render 函數,但用Funclasses就很難發現主return語句。另外,要按照不一樣的 useEffect 語句來理解組件的流程是比較困難的,相比之下,常規的生命週期方法會給你一些很好的提示,讓你知道本身的代碼須要在哪裏尋找。若是我正在尋找某種初始化邏輯,我將跳轉(VSCode中的cmd + shift + o)到 componentDidMount,若是我正在尋找某種更新機制,則可能會跳到 componentDidUpdate 等。經過Funclass,我發現很難在大型組件內部定位。

約定驅動的API

鉤子的主要規則(可能也是最重要的規則)之一是使用前綴約定。

就是感受不對

你知道有什麼不對勁的感受嗎?這就是我對鉤子的感受。有時我能準確地指出問題所在,但有時只是一種廣泛的感受,即咱們走錯了方向。當你發現一個好的概念時,你能夠看到事情是如何很好地結合在一塊兒的,可是當你在爲錯誤的概念而苦惱的時候,發現你須要添加更多更具體的東西和規則,才能讓事情順利進行。

有了鉤子,就會有愈來愈多奇怪的東西跳出來,有更多「有用的」鉤子能夠幫助你作一些瑣碎的事情,也有更多的東西須要學習。若是咱們須要這麼多的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不那麼笨拙的方式。


首發於公衆號《前端全棧開發者》,第一時間閱讀最新文章,會優先兩天發表新文章。關注後私信回覆:大禮包,送某網精品視頻課程網盤資料,準能爲你節省很多錢!
image

相關文章
相關標籤/搜索