hooks vs class component 之爭

我與疫情

2020 轉眼已來到3月,但疫情的突襲,讓這個春節遲遲沒有開始,也無法結束。這段時間看似是充電自我提高的大好時光,但家國情懷深厚的我爲疫情真的是操碎了心,時不時都要看看哪裏數據猛增了,哪裏暴發了。結束一個月的在家辦公,帶着口罩在公司上班,狀態稍有好轉,注意力終歸回到了技術。 前端

最近在組裏推BFF Node接入與微前端改造,看到了組裏各式各樣實現地業務代碼(組件),有激進開放的整個頁面都用hooks實現,有沉迷於過去的停留於redux + saga + model的類組件寫法,固然更多的是類組件中的子組件參和幾個hooks。我帶着好奇之心去goggle了一下這兩個誰勝一籌,因而有了此文。react

hooks vs class

網上看了不少大佬觀點,但脫離業務的爭論都是耍流氓。做爲眼見爲實躺坑無數的練習生,我決定用事實說話。說那麼多幹嗎,上代碼:
慢...,先說說接下來要乾的事情:git

  • 進入一個列表頁面,首次進入,發起請求,獲取列表數據;
  • 列表有搜索框,支持條件搜索和重置;
  • 列表支持翻頁;
  • 頁面彈出一個對話框,用於,新增,修改數據;
  • 修改數據時,須要發起一個請求,去獲取詳情;

發現沒,這就是一個標準的CRUD,頁面大體長這樣:
image.pnggithub

膚淺的看-表面

接下來,列一下兩種方式實現,頁面的大體結構,橙色方框內爲Hook重構後的頁面結構:
image.png
Hook重構後代碼變化主要在index.js,刪去了對Dva的Model依賴,轉由Hook本身管理數據, 代碼對比大體以下:
image.png
看代碼,主要是數據層的實現有變化,UI保持一致。當頁面跑起來,若是不看麪包屑的話,也很難分辨誰是Hooks組件的實現,誰是類組件的實現,因此表面的看,是看不出誰高誰低的。spring

仔細的看-性能

畢竟人的肉眼,只有當低於30fps時,才能明顯的觀察到變化;因此要想客觀的對比性能,得依賴Chrome的性能分析(Performerce);這裏作兩個對比:chrome

  1. 從其餘菜單(主頁)跳轉到列表頁
  2. 從列表頁到打開詳情編輯Modal;

跳轉到列表頁
image.pngredux

打開編輯頁
image.png緩存

爲保證試驗結果嚴謹,我儘可能保證惟一性變量原則、多(san)次、與減小本身手抖的次數,但與自動化測試仍是有較大差距,十幾毫秒的偏差再所不免。我觀察了列表頁切換頁的兩個圖,除開請求的波動於手動測試的偏差,兩個頁面從點擊到請求再到頁面渲染,時間相差無幾,甚至調用棧都是驚人的類似(裝逼的說法就是:從原理上分析,也應該是類似的:dispatch + diff + render)。
也觀察了詳情編輯Modal打開兩個圖,屢次測試,其打開速度hooks稍暫上風,因爲這一塊的實現邏輯有比較大的差異(class組件的數據獲取是在最外層,獲取完而後依次向裏傳遞;而hooks則是從外層獲取到id後,組件內部直接發起請求獲取數據),因此二者火焰圖也有比較大的差別;但從頁面渲染的角度總體感受差異不大。 性能優化

最後我得出的見解是:Hooks更可能是一種管理數據的手段,與class相比,並無什麼性能上的優點,更多的主動權,在編寫代碼的人手裏,就像我駕校老師愛說的那句狗屁不通的諺語:再好的車,給這個二傻子開,都能開熄火。關於更多,能夠關注B乎討論: React hooks 和 Class Component 的性能哪個更好?
若是對個人測試有疑惑,能夠本身動手,我提供示例項目:antd

我我的始終同意:框架只是實現業務的手段,在使用成熟的框架前提下,頁面的性能徹底由司機掌控。(我我的有個觀點就是:Vue是自動檔,React更像手動檔

hooks:useRequest

要想徹底脫離redux或mobx,簡單使用Hooks中的useState或useEffect來完成頁面,其難度仍是很大且很難管理,畢竟頁面大多數數據源都來自異步請求,因此封裝一個useRequest hooks是勢在必要的,並且Hooks最大的優點就是邏輯複用。如下將分享部門封裝useRequest組件的思考過程,其思路參考於Apollo-Graphqlreact-hooks項目。先看示例代碼(上面列表頁的部分代碼):

export default function Root() {
 const [search, onSearch, onReset] = usePagination({});

 // 請求:admin.closertb.site/rule/query接口
 const { data = {}, loading, error } = useRequest('/rule/query', search);

 const { datas, total } = data;
 const tableProps = {
   search,
   datas,
   fields,
   onSearch,
   total,
   loading,
 };

 const searchProps = {
   fields: searchFields,
   search,
   onReset,
   onSearch,
 };
 return (
   <div>
     <WithSearch {...searchProps} />
     <div className="pageContent">
       <EnhanceTable {...tableProps} />
     </div>
   </div>
 );
}

以上就是一個最多見的useRequest hook應用,用很是簡短的代碼替換了Dva中的路由監聽(subscription), 異步請求(effect),數據更新(reducer)等一連串邏輯;下圖是一個簡單的流程示意圖:
image.png

根據這個示意圖,老司機應該就大概知道怎麼實現的了:

  • 運用useRef 來緩存請求實例,即每一個useRequest僅建立一個query實例,頁面更新時,沿用已經存在的示例;
  • 運用useMemo作計算,判斷請求參數是否更新;
  • 運用useEffect, 用useMemo計算結果做爲依賴,判斷是否發起請求;
  • 固然,更新頁面,採用了一個自增的useReducer;

如今留下的惟一疑問就是,發佈訂閱怎麼實現的,看一下代碼:

// query 示例其中的兩個核心方法:
  startQuery() {
    const { url, body, forceUpdate, result } = this;
    if (this.status > STATUS.fetch) {
      return;
    }
    // 狀態反轉爲請求中
    this.status = STATUS.fetch;
    result.loading = true;
    // 發起請求
    this.request(url, body).then((data) => {
      // 更新結果
      result.data = data;
      result.loading = false;
      this.status = STATUS.success;
      // 更新訂閱
      forceUpdate();
    }).catch((error) => {
      this.status = STATUS.error;
      result.error = error;
      result.loading = false;
      forceUpdate();
    });
  }

  execute() {
    // skip:是否禁用查詢
    const { result, options: { skip = false } } = this;
    !skip && this.startQuery();

    // 當skip 爲true 時,說明沒有查詢結果,因此不能用上次的查詢結果來作過渡
    return Object.assign({
      error: undefined,
      data: undefined,
      loading: !skip,
      forceUpdate: this.forceQuery,
    }, skip ? {} : result);
  }

是否是有種恍然大悟,並無什麼發佈訂閱的具體實現,徹底依賴於Promise對象隱式的發佈訂閱,而forceUpdate的實現則依賴於useReducer:

const [tick, forceUpdate] = useReducer(x => x + 1, 0);

從去年用Graphql寫完本身的博客,發現Apollo的useQuery這個hooks彩蛋,就一直有想法去實現一個useRequest hooks,在去年的一次需求推動中,強迫本身去編寫了這個組件。收穫仍是很大的,在咱們團隊中已經有必定嘗試,固然還有很大的拓展空間。從這個組件的編寫歷程,我也再一次體驗了開手動擋,司機經驗的重要性,拿我本身挖到的一個坑舉例:

cleanup() {
    this.result.data = undefined;
    this.result.loading = false;
    this.result.error = undefined;
  }

上面一段代碼,是每次請求完成後,頁面更新後, 有一個useEffect反作用會執行query.cleanup()來保證請求示例回到初始狀態。但就這樣一段代碼,引發了極大的性能問題:

事件喚起查詢時,頁面會抖動,開始我覺得是我寫的hooks的組件不如 react-redux那麼多性能優化,其實當時在猜疑是否是hooks的性能問題。

但後面經過應用chrome performance,觀測到其抖動,是因爲this.result.data = undefined形成的,用一個示意圖表示:
image.png
大概意思就是,若是當前頁面有數據,再次發起查詢時,列表除了當前更新爲loading狀態,當前10天數據也會被清除,至關於列表作了一次diff並render;請求完成後,loading狀態消失,更新列表,又作了一次diff並render.因此會形成抖動。後面完善代碼後,就和下面正常的階躍曲線同樣,只有一次階躍,列表實際只會作一次render.

這次實現是基於團隊現有的http庫來作的,這個庫的原理在之前的一篇文章講過:邊看邊寫:基於Fetch仿洋蔥模型寫一個Http構造類

若是你感興趣,能夠在個人github看到:

結語

沒啥想總結的,願疫情早日過去。願口罩早日摘下,願火鍋早日成爲生活的平常。

對了,Antd4.0已經到來,Form表單基本被重寫,這意味着我組件庫Antd-doddle, 又得作一次大的升級了!!!!cd

相關文章
相關標籤/搜索