【牆裂推薦】Talking about hooks

從React16.8開始,Hooks API正式被React支持,而就在最近,Vue做者尤雨溪翻譯併發布了一篇本身的文章《Vue Function-based API RFC》,並在全文開頭強調這是Vue 3.0最重要的RFC,並在文中提到html

Function-based API 受 React Hooks 的啓發,提供了一個全新的邏輯複用方案。前端

能夠簡單的理解爲,React 和 Vue 爲了解決相同的問題,基於不一樣的技術實現了類似的API。因此本文也將結合兩種框架各自的特色,簡單講講我的對Hooks的理解。react

在將來版本的規劃裏,React並不如Vue激進,React的文檔裏專門提到es6

並無從 React 中移除 class的計劃。設計模式

而Vue卻採起了不一樣的升級策略,作好了拋棄大部分歷史語法的準備數組

  • 兼容版本:同時支持新 API 和 2.x 的全部選項;
  • 標準版本:只支持新 API 和部分 2.x 選項。

爲何咱們再也不須要Class Component?

爲了回答這個問題,咱們先看看以前和如今的React組件劃分產生了哪些變化。promise

1. 既然原本就有函數組件,開始爲何引入class組件?

早期的React組件能夠依據「有沒有狀態(state)」分爲數據結構

// 無狀態組件
const Welcome = (props) => <h1>Hello, {props.name}</h1>;

// 有狀態組件
class Welcome extends React.Component {
    constructor(props) {
        super(props);
        this.state = {name: 'KuaiGou'};
    }
    
    render() {
        return <h1>Hello, {this.state.name}</h1>;
    }
}
複製代碼

雖然class也能夠不添加狀態,但想要使一個函數組件具備狀態,不得不將其轉換成class組件。架構

直觀來看,好像形成這種差別是由於在class裏,咱們能經過this保存和訪問「狀態(state)」,而函數組件在其做用域內難以維持「狀態(state)」,由於再次函數運行會重置其做用域內部變量,這種差別致使了咱們「不得不」使用class至今。併發

看來如何解決函數組件保存state的成了移除class這種「難以理解」的關鍵。

2. 那Hook是如何保留組件狀態的?

這就是我看見Hook API產生的第一個疑問。其實在React裏,這並非問題,熟悉React Fiber的同窗應該知道,事實上state是保存到Fiber上的屬性memoizedState上的,而並不算是class的this.state上。那狀態問題就迎刃而解了,若是函數組件一樣訪問Fiber上的memoizedState屬性,就能夠解決這個問題。

基於Fiber架構,解決這個問題很是容易,將memoizedState看做一個普通的變量,那麼Hook的原理就容易理解和實現了。

在文章[譯] 理解 React Hooks中提到

記住,在 Hooks 的實現中也沒有什麼「魔術」。就像 Jamie 指出的那樣,它像極了這個:

let hooks = null;

export function useHook() {
    hooks.push(hookData);
}

function reactsInternalRenderAComponentMethod(component) {
    hooks = [];
    component();
    let hooksForThisComponent = hooks;
    hooks = null
}

複製代碼

如Fiber同樣,React實際上使用鏈表代替了數組這種數據結構,依次執行Hook,有興趣的同窗能夠去看下React源碼。

但是,class目前也能良好的支撐業務迭代,到底有什麼動力去從新學習Hooks?

3. 爲何咱們須要Hooks?

針對這個問題,React文檔提到了下面三點:

  • 在組件之間複用狀態邏輯很難
  • 複雜組件變得難以理解
  • 難以理解的 class

其實我以爲第三點就是來湊數的,畢竟React推出至今一直用着class,再難用各位也都會了,會者不難難者不會嘛(反正對於剛入前端坑那時候的我來講,沒有啥是容易的)。

那就回答下一個問題,目前基於class實現的生命週期函數,是否真的會形成邏輯難以複用?

答案是NO

不管高階組件或是render props,都提供了很好的方式來達到聚合業務邏輯的目的,業務邏輯並不會被生命週期「分割」。

那究竟是哪裏引入了複雜度?熟悉套娃的同窗...呸

熟悉Ajax、Promise的等異步API的同窗可能還記得「回調地獄」。相似的,高階函數、render props等也極容易形成「嵌套地獄」,結合裝飾器、函數式的compose等嵌套起來纔是真的爽...一直嵌套一直爽...

可是,不管是什麼地獄確定是很差的,那一塊兒來看最後一個問題。

複雜組件變得難以理解

之因此迴避前兩個問題,是由於我我的認爲,不管是class仍是HOC,它們都很好的解決了它們須要解決的問題,雖然生命週期函數將不少業務邏輯拆分的七零八碎,可是HOC卻依舊能把它們集合在一塊兒,僅考慮保留生命週期而言,就像Function-based同樣(這是後話)。

因此咱們換一個思路不難發現,真正的問題是在於它們在抽象業務邏輯的時候貌似引入了沒必要要的概念,才使得邏輯複用困難和難以理解。

這些概念致使了過多的嵌套,加深了組件層級,層級之間互相牽扯,就像我如今兜裏的耳機線同樣。

Hook獨特之處在於化繁爲簡。

真正繁瑣的是層級與層級之間的關係,我將借用React文檔關於自定義Hook的例子說明這個問題

import React, { useState, useEffect } from 'react';
// 經過friendID訂閱好友狀態
function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

複製代碼

經過useFriendStatus這個自定義的hook,我能夠很是輕鬆的在下面兩個組件中實現邏輯的複用

// FriendStatus獲取好友狀態
function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

// FriendListItem獲取好友狀態
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}> {props.friend.name} </li>
  );
};

複製代碼

但是對於熟悉高階組件的同窗來講(不熟悉的同窗請看高階組件)依舊能夠輕鬆的提取一個名叫useFriendStatus的高階組件

function useFriendStatus(WrappedComponent) {
  return class extends React.Component {
    componentDidMount() {
        ChatAPI.subscribeToFriendStatus(
          this.props.friend.id,
          this.handleStatusChange
        );
    }
     
    componentDidUpdate(prevProps) {
        ChatAPI.unsubscribeFromFriendStatus(
          prevProps.friend.id,
          this.handleStatusChange
        );
        ChatAPI.subscribeToFriendStatus(
          this.props.friend.id,
          this.handleStatusChange
        );
      }

    componentWillUnmount() {
        ChatAPI.unsubscribeFromFriendStatus(
          this.props.friend.id,
          this.handleStatusChange
        );
    }
    
    render() {
      return <WrappedComponent isOnline={isOnline} />; } } } 複製代碼

而後,分別套上FriendStatus和FriendListItem依舊能夠很是完美的多處複用此邏輯。

須要注意的是,高階組件等概念仍是有不少特色和優勢的,hooks並非萬能的,hooks只是hooks而已

須要提到的是,官方文檔在這裏對比了新舊方案代碼量的長度和複雜度,但其實在特定的業務狀況下,確實不可避免會出現這種問題,我的認爲這是次要矛盾(既然HOC已經封裝了複雜度,還糾結裏面長不長、復不復雜幹啥)。

反而,這容易讓剛接觸Hooks的同窗忽略Hook最大的亮點,先看下面這段熟悉的代碼(不熟悉的同窗看Promise 對象async 函數)。

// async 
const P1 = new Promise(Something)
const P2 = new Promise(Something)

export default async function () {
  const res1 = await P1;
  //do Something
  const res2 = await P2;
  // ...
  return res2
}
複製代碼

和剛纔的Hook對比

// 以及 hook
function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);
  ...
  return isOnline ? 'Online' : 'Offline';
}
複製代碼

像不像...就問你像不像...(模仿團隊某成員說話)

寫(粘)了這麼多代碼,簡單來講,Hook解決的就是「嵌套地獄」的問題,正如async解決「回調地獄」同樣。它們都作到了將原來不一樣「維度」的代碼封裝到了同一維度,以達到更直觀、透明的將「計算結果」傳遞下去的目的。

而class不得不借助高階組件等等概念,解決代碼複用等問題,可是因爲引入額外的概念(函數)反而使得代碼更加複雜,如今的class難以解決這個問題,因此他就被拋棄了。

問題來了,誰能夠不引入別的概念,完成邏輯封裝?

就是函數自己啊!!還要class幹嗎?!

綜上,被拋棄和class生命週期函數致使的代碼複雜度提高無關,Hook簡化生命週期函數只是不過是舉手之勞,並非什麼重要的特性。

爲何這裏說這個不重要呢,下面將要講的Function-based會幫助我解答這個問題。

4. 繼續保留生命週期函數的Function-based API

Vue在組件化的道路上和React走過了大體相同的道路,這也就是爲何有人問我Vue和React之間有什麼區別的時候,鑑於我的水平,我是真的答不上來😂...

話題扯回來,它們都通過了下面幾個階段

  • Mixins
  • 高階組件 (Higher-order Components)
  • Renderless Components

拋開框架,咱們思考下「組件化」這個概念是否是爲邏輯抽象服務的,咱們提取共有的邏輯,各處複用,各種設計模式最終落到實體我我的認爲他就是「組件」。

同時瞭解Hooks和Function-based的同窗,應該不難發現他兩巨大的差別,並且具備後發優點的Vue也解決了React Hooks還沒有解決的問題(或者React並不認爲這是問題,也不打算解決),但這裏並不想對兩種API的使用作過多的說明,畢竟它們的文檔已經寫的很是好了。

爲了說明它們類似的地方,借用尤大文章中「用組合函數來封裝鼠標位置偵聽邏輯」的例子

function useMouse() {
  const x = value(0)
  const y = value(0)
  const update = e => {
    x.value = e.pageX
    y.value = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  return { x, y }
}

// 在組件中使用該函數
const Component = {
  setup() {
    const { x, y } = useMouse()
    // 與其它函數配合使用
    const { z } = useOtherLogic()
    return { x, y, z }
  },
  template: `<div>{{ x }} {{ y }} {{ z }}</div>`
}
複製代碼

對比上文的「訂閱好友狀態」的React版本例子

import React, { useState, useEffect } from 'react';
// 經過friendID訂閱好友狀態
function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

複製代碼

不難發現,不管是Hooks仍是Function-based,都僅僅作了一件事。 那就是千方百計啊,把邏輯用一個函數包了起來,使得代碼邏輯表現的很是天然,我相信這就是爲何尤大堅決果斷拋棄Vue存在多年的選項,直接用Function-based來代替它。

由於函數天生就是用來計算(狀態)的。

解釋下上面提到的「並不重要的生命週期」,觀察上面的代碼對比狀況,有個很是明顯的差別。在React裏面被聚合(消失掉)的生命週期函數,具備後發優點的Vue仍舊保留下來了。

容易得出,邏輯有沒有被生命週期切分,或者究竟在onMounted裏面計算仍是在useEffect裏計算並不重要,重要的是邏輯自己有沒有被切分?

寫在最後的話

React、Vue也一直試圖推出各類手段簡化業務邏輯,可是正如Mixins、HOC、render-props以及本文說到的Hook和Function-based等。雖然它們都不是React或Vue原創的概念,但絕不影響初見時的驚豔,轉念又會思考爲何本身沒有想到,說到底不就是在外面套一個函數嘛,總結起來就是。

背那麼多API,依舊寫很差代碼。

參考文獻

關於咱們

快狗打車前端團隊專一前端技術分享,按期推送高質量文章,歡迎關注點贊。

相關文章
相關標籤/搜索