jeremygo

jeremygo

我是把下一颗珍珠串在绳子上的人

Hooks 隨談

React 16.8 正式推出了 Hooks 的特性,這期間也從一些方面了解嘗試過 Hooks,謹參考多篇文章來談談 Hooks

Hooks 可以讓你不需要通過類來使用 Reactstate 以及其它特性。你也可以自定義自己的 Hooks 來在組件之間共享可復用的狀態邏輯 。

起源#

當下,組件以及自頂向下的數據流可以幫助我們將大型 UI 組織成小型、獨立可復用的部分。但是很多時候,我們的邏輯是帶有狀態的,因此不能進一步打破複雜的組件,也不能拆分出一個函數或是另一個組件。這些情況包括動畫表單控件連接外部數據源 等等是非常普遍的 。

雖然現在對於 React 應用我們已經有很多種方式來復用這些邏輯,比如寫簡單的函數來調用,或是通過組件(函數或類的形式),但是對於非可視化的邏輯它就不是那麼方便了,所以我們又提出了一些複雜的模式,比如 render props 和 高階組件 。

如果 React 只有一種通用的方式來復用邏輯是不是會更簡單呢?

對於代碼復用,函數似乎是一種完美的機制,但是函數並不能包含本地 React 的狀態,我們不能簡單從類組件提取一些行為,比如 依據 window 的大小來更新狀態 或是 讓一個值隨時間變化。我們通常都需要重構這部分代碼或者是採用抽象的模式比如觀察者。這些都破壞了 React 的簡潔性 。

基於以上的思考,React 開發團隊提出了 Hooks 。它可以讓我們通過函數來使用 React 的特性(比如狀態),並且 React 官方也提供了一些內置的與 React 構建模塊相關的 Hooks : 狀態生命週期上下文,因為Hooks 就是普通的 JavaScript 函數,因此我們也可以自定義 Hooks 來使用 。

Demo#

讓我們看一個例子:隨 window 大小的調整來更新 React 的狀態 。

import { useState, useEffect } from 'react'

function useWindowWidth () {
    const [width, setWidth] = useState(window.innerWidth)
    
    useEffect(() => {
        const handleResize = () => setWidth(window.innerWidth)
        window.addEventListener('resize', handleResize)
        return () => {
            window.removeEventListener('resize', handleResize)
        }
    })
    
    return width
}

上述代碼用到了 React 自帶最常用的兩個 Hooks : useStateuseEffect :

  • useState : 傳入一個初始狀態,返回帶狀態的值和可以更新這個值的函數 。
  • useEffect : 傳入帶有副作用的函數(改變數據,訂閱,定時器,日誌記錄等等)。

React 基礎的 Hooks 還有一個 useContext

同時我們還有一個基於內置 Hooks 的自定義 Hooks : useWindowWidth

我們來看看 useWindowWidth 做了什麼:傳入 window 的初始大小,返回狀態值 width 和可以更新 widthsetWidth,在 useEffect 內部我們定義了一個通過 setWidth 設置 window 大小的 handleResize 函數,並且監聽了 window resize 事件來更新 window 的大小,最終返回取消監聽的函數 。

這就是一個可以隨 window 大小來調整佈局的 Hooks

試想一下如果我們用類組件的方式來實現會是怎樣?

class setWidth extends React.Component {
    constructor (props) {
        super(props)
        this.state = {
             width: window.innerWidth
        }
    }
	handleResize = () => this.setState({ width: window.innerWidth })
    componentDidMount () {
		window.addEventListener('resize', handleResize)
    }
    componentWillUnmount () {
		window.removeEventListener('resize', handleResize)
    }
    render () {
		return <SomeUI />
    }
}

對比之下我們很容易感覺到類組件的寫法我們要設置 state、結合繁瑣的生命週期以及必須要渲染 UI (當然可以是 null),並且更大的問題是如果我們需要復用這個組件同時 <SomeUI /> 又不能相同,我們往往就會採用高階組件的寫法,這又引入了抽象的模式。所以對比下來, Hooks 讓開發者可以有更小的學習成本,同時也更貼合 React 簡潔的理念 。

這裡有一個類組件與 Hooks 更直觀的對比的視頻 : https://twitter.com/i/status/1056960391543062528

同時我們也能感受到自定義 Hooks 的魅力,隨著 Hooks 的正式提出,之後將會有越來越多的 Hooks npm 包來更好的幫助開發者 。

這裡有一個各類自定義 Hooks 實現的網站 : https://usehooks.com/

深入#

作為一個 React 開發者,Hooks 剛提出時我是感到很驚艷的,React 從一開始的定位就是為了更好地構建 UI,這就帶給了我兩個疑問 :

  • Hooks 的出現代表了什麼?
  • Hooks 是怎麼實現的?

對 React Hooks 的一些思考 中有一句話 : "有狀態的組件沒有渲染,有渲染的組件沒有狀態"React 團隊想要推行的正確理念就是 "狀態與 UI 分開"。而這其實也正與 React Hooks 的特性相合,我們來看一下使用 Hooks兩個規則 :

  • 只在函數頂部調用 Hooks
  • 只在 React 函數中調用 Hooks

Hooks 必須集中在函數頂部來寫,這其實很容易養成書寫無狀態 UI 組件的習慣,因此也會更容易踐行 "狀態與 UI 分開" 這個理念。個人覺得這也是 React 團隊將 Hooks 視為 狀態共享問題 長久以來最完美的解決方案的原因之一 。

那麼為什麼 Hooks 必須在函數頂部被調用?是為了防止我們用條件判斷來包裹 Hooks 。為什麼不能用條件判斷包裹就涉及到 Hooks 的實現問題,簡單解釋一下就是 : Hooks 並不是通過 Proxy 或者 getters 實現的,而是通過類似數組的實現方式,因此 Hooks 的正常調用與下標順序有關,每次 useState 都會改變下標,如果在條件判斷中使用了 Hooks ,可能就會影響下標造成 Hooks 調用出錯,因此官方也建議在 Hooks 內部進行條件判斷是更正確的做法 。

具體原因可以見 官方文檔

Hooks 的原理可以見 React hooks: not magic, just arrays

如果你想更深入地了解 Hooks 體系,可以見 Under the hood of React’s hooks system

可能有人會感覺到 Hooks 是帶有一些限制的,但是更正確的理解是我們應該遵循 Hooks 的約定,它也是 React 官方第一次將 "約定優先" 的理念引入 React 框架當中,有了限制,但也有了更好的便利性 。

Next.jsUmi 中都有 "約定路由" 的功能,這大大降低了路由配置的複雜度,而 Hooks 就像代碼級別的約定,大大降低了代碼的複雜度,因此我們可以考慮這是不是未來很重要的一個趨勢 。

更多思考#

Hooks 的設計並不是綁定在 React 上的,現在也已經有了 VueWeb Components 甚至是原生 JavaScript 函數的實現(實驗性的 API 設計)。

你可能會有這樣的疑問 : Vue 需要 Hooks 嗎? Vue 確實沒有像 React 類組件一樣的問題,如果是需要復用邏輯的話,Mixins 也可以解決。但是從本質上看的話,Vue 的確需要 Hooks 來解決 狀態共享 的問題,相比於 MixinsHooks 可以幫助 Vue 解決兩個主要問題 :

  • 實現狀態的共享 。
  • 更清晰地展示邏輯的來向 。

目前 Vue 團隊也計劃在 Vue 3.0 中集成 Hooks ,但是會偏離 React 的 API 而貼合 Vue 的思想去設計,並且很有可能會成為 Mixins 的替代品,因此也是十分值得探索的 。

Vue 實驗性質的實現 : https://github.com/yyx990803/vue-hooks

參考 :

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。