Skip to main content

Hooks 的運作原理與設計思維

Hooks 的資料本體到底存放在何處

在 React 中,Hooks 的資料本體(例如 useState 的狀態值、useEffect 的副作用邏輯等)並不是直接儲存在元件本身的變數或屬性中,而是由 React 內部的 Fiber 架構管理。每個使用 Hooks 的元件在 React 的 Fiber 樹中都有一個對應的 Fiber 節點,這些 Hooks 的資料本體被儲存在該 Fiber 節點的某個內部結構中。

具體來說:

  1. Fiber 節點:React 在渲染時會為每個元件建立一個 Fiber 節點,這個節點負責儲存元件的狀態、屬性、DOM 參考等資訊。

  2. Hooks 的資料結構:在 Fiber 節點中,React 會維護一個 鏈結串列(linked list),用來儲存該元件中所有 Hooks 的資料。每個 Hook 都是一個物件,包含其狀態或相關資訊(例如 useState 的狀態值、useEffect 的回呼函數與依賴陣列)。

  3. 如何存取:React 透過內部的 Dispatcher 機制,在元件渲染時按順序存取這個鏈結串列中的 Hook 資料。當你呼叫 useState 或 useEffect,React 會從目前正在渲染的 Fiber 節點中取出對應的 Hook 資料。

簡單來說,Hooks 的資料本體並不在你的程式碼中,而是由 React 的運行時環境(Runtime)管理,存在於記憶體中的 Fiber 節點內。

程式碼範例

以下是一個簡單的 useState 範例,幫助你理解資料本體的儲存:

import React, { useState } from "react";

function Counter() {
const [count, setCount] = useState(0); // 狀態值儲存在 Fiber 節點的 Hooks 鏈結串列中

return (
<div>
<p>目前計數:{count}</p>
<button onClick={() => setCount(count + 1)}>1</button>
</div>
);
}

export default Counter;

說明

  • useState(0) 的狀態值 count 並不是儲存在 Counter 元件內的某個變數,而是由 React 在 Fiber 節點中維護。

  • 當你呼叫 setCount,React 會更新對應 Fiber 節點中的 Hooks 資料,然後觸發重新渲染。

為什麼這樣設計?

  • 集中管理:將狀態儲存在 Fiber 節點中,讓 React 可以統一管理所有元件的狀態,方便進行狀態更新與渲染最佳化。

  • 支援動態 Hooks:透過鏈結串列,React 可以在每次渲染時動態存取 Hooks 的資料,而不需要依賴固定的變數名稱。


為什麼 Hooks 的運作是依賴於固定的呼叫順序

解答

Hooks 的運作依賴固定的呼叫順序,這是因為 React 使用 鏈結串列 來儲存和管理 Hooks 的資料,而這個串列是按 Hooks 的呼叫順序建立的。如果呼叫順序改變,React 無法正確對應到原本的 Hook 資料,導致狀態或副作用錯亂。

詳細原理

  1. Hooks 鏈結串列

    • 當 React 渲染一個元件時,它會記錄該元件內所有 Hooks 的呼叫順序,並將每個 Hook 的資料(例如 useState 的狀態值)儲存在一個鏈結串列中。

    • 每個 Hook 物件在串列中有一個固定的位置,React 會根據呼叫順序來存取這些物件。

    • 例如,第一個 useState 對應串列中的第一個 Hook 物件,第二個 useState 對應第二個,以此類推。

  2. 為什麼需要固定順序

    • React 在渲染時並不知道 Hooks 的名稱或用途,它只知道「目前是第幾個 Hook」。

    • 如果你在不同渲染間改變 Hooks 的呼叫順序(例如在條件語句中呼叫 Hooks),React 會誤以為某個 Hook 是另一個 Hook,導致狀態或副作用對應錯誤。

  3. 錯誤範例: 以下程式碼會因為違反 Hooks 的規則而導致錯誤:

    import React, { useState } from "react";

    function BadExample() {
    const [count, setCount] = useState(0);

    if (count > 0) {
    const [name, setName] = useState(""); // 違反規則:useState 在條件語句中
    }

    return (
    <div>
    <p>計數:{count}</p>
    <button onClick={() => setCount(count + 1)}>1</button>
    </div>
    );
    }

錯誤原因

  • count <= 0 時,只有第一個 useState 被呼叫;當 count > 0 時,第二個 useState 也會被呼叫。

  • React 無法預測這種動態的呼叫順序,導致 Hooks 串列中的資料對應錯誤。

正確做法

確保 Hooks 總是在元件的頂層呼叫,且順序固定:

import React, { useState } from "react";

function GoodExample() {
const [count, setCount] = useState(0);
const [name, setName] = useState(""); // 總是在頂層呼叫,順序固定

return (
<div>
<p>計數:{count}</p>
<p>名稱:{name}</p>
<button onClick={() => setCount(count + 1)}>1</button>
<input value={name} onChange={(e) => setName(e.target.value)} />
</div>
);
}

export default GoodExample;

為什麼這樣設計?

  • 簡單性:React 透過固定順序簡化了 Hooks 的管理邏輯,無需為每個 Hook 指定唯一識別碼。

  • 高效能:使用鏈結串列按順序存取 Hooks 資料,減少了額外的查找成本。


Hooks 的誕生是為了解決什麼問題

解答

Hooks 於 React 16.8 引入,主要是為了解決 類別元件(Class Components) 在開發中遇到的問題,並提升函數元件(Function Components)的功能性與靈活性。以下是 Hooks 解決的主要問題:

  1. 類別元件的複雜性

    • 類別元件需要使用 this 關鍵字,容易導致綁定問題(例如 this.handleClick = this.handleClick.bind(this))。

    • 類別元件的生命週期方法(componentDidMount、componentDidUpdate 等)經常將不相關的邏輯混雜在一起,難以維護。

    • 類別元件在程式碼重用和邏輯分離方面不夠靈活,例如需要使用高階元件(HOC)或 Render Props,增加了學習成本。

  2. 邏輯重用的困難

    • 在類別元件中,重用狀態邏輯通常需要透過 HOC 或 Render Props,這會導致「包裝地獄」(Wrapper Hell),程式碼層次過多,難以理解。

    • Hooks 允許將狀態邏輯封裝成自訂 Hooks,方便在多個元件間重用。

  3. 函數元件的局限性

    • 在 Hooks 引入之前,函數元件只能是無狀態元件(Stateless Components),無法管理狀態或生命週期。

    • Hooks 讓函數元件也能擁有狀態(useState)和副作用(useEffect),完全取代類別元件。

程式碼對比

以下展示類別元件與 Hooks 的對比,突顯 Hooks 的簡潔性:

類別元件(舊方式)

import React, { Component } from "react";

class Counter extends Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.handleClick = this.handleClick.bind(this); // 綁定 this
}

handleClick() {
this.setState({ count: this.state.count + 1 });
}

componentDidMount() {
console.log("元件已掛載");
}

render() {
return (
<div>
<p>計數:{this.state.count}</p>
<button onClick={this.handleClick}>1</button>
</div>
);
}
}

export default Counter;

Hooks(新方式)

import React, { useState, useEffect } from "react";

function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
console.log("元件已掛載");
}, []); // 空依賴陣列模擬 componentDidMount

return (
<div>
<p>計數:{count}</p>
<button onClick={() => setCount(count + 1)}>1</button>
</div>
);
}

export default Counter;

Hooks 的優勢

  • 程式碼更簡潔,無需 this 或綁定。

  • 邏輯按功能組織(例如 useEffect 專注於副作用),而非分散在生命週期方法中。

  • 容易提取成自訂 Hooks,方便重用。


Hooks API 的設計思維與脈絡

解答

Hooks API 的設計思維是為了讓 React 的開發更直觀、更靈活,並解決類別元件的痛點。以下是 Hooks API 的核心設計思維與脈絡:

  1. 以函數式思維為基礎

    • Hooks 鼓勵使用函數式程式設計,減少類別元件的複雜性。

    • 每個 Hook 都是一個純粹的函數,輸入(參數)與輸出(返回值)明確,符合函數式程式設計的原則。

  2. 模組化與邏輯分離

    • Hooks 讓開發者能將狀態邏輯封裝成自訂 Hooks,實現邏輯的模組化。

    • 例如,可以將計數器的邏輯提取為 useCounter:

    import React, { useState } from "react";

    function useCounter(initialValue = 0) {
    const [count, setCount] = useState(initialValue);

    const increment = () => setCount(count + 1);
    const decrement = () => setCount(count - 1);

    return { count, increment, decrement };
    }

    function Counter() {
    const { count, increment, decrement } = useCounter(0);

    return (
    <div>
    <p>計數:{count}</p>
    <button onClick={increment}>1</button>
    <button onClick={decrement}>1</button>
    </div>
    );
    }

    export default Counter;
  3. 統一狀態與副作用管理

    • Hooks 提供一致的 API(例如 useState 管理狀態、useEffect 管理副作用),讓開發者以統一的方式處理元件的邏輯。

    • 這取代了類別元件中分散的生命週期方法。

  4. 依賴陣列的設計

    • useEffect 和 useMemo 等 Hooks 使用依賴陣列來控制執行時機,這種設計讓開發者明確指定哪些變數會影響副作用或計算結果,避免不必要的重新計算或副作用執行。
  5. 避免過度抽象

    • Hooks API 設計簡單,核心 API(如 useState、useEffect、useContext)數量少且功能專一,避免過多抽象導致學習曲線陡峭。

    • 開發者可以透過組合這些基本 Hooks,實現複雜邏輯。

設計脈絡

  • 靈感來源:Hooks 的設計受到函數式程式設計、React 社群的反饋以及其他框架(如 Vue 的 Composition API)的啟發。

  • 目標:讓函數元件成為 React 的主要開發方式,逐步淘汰類別元件。

  • 迭代過程:React 團隊在設計 Hooks 時,考慮了向後相容性(Hooks 與類別元件可並存)以及未來擴展性(允許新增自訂 Hooks)。

實務應用

以下是一個結合多個 Hooks 的範例,展示其模組化與靈活性:

import React, { useState, useEffect } from "react";

function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
async function fetchData() {
setLoading(true);
const response = await fetch(url);
const result = await response.json();
setData(result);
setLoading(false);
}
fetchData();
}, [url]); // 當 url 改變時重新執行

return { data, loading };
}

function App() {
const { data, loadingtyp } = useFetch("https://api.example.com/data");

return (
<div>
{loading ? <p>載入中...</p> : <pre>{JSON.stringify(data, null, 2)}</pre>}
</div>
);
}

export default App;

說明

  • useFetch 是一個自訂 Hook,封裝了資料抓取的邏輯,展示了 Hooks 的模組化特性。

  • useEffect 使用依賴陣列 [url],確保只有在 url 改變時才重新抓取資料。


總結

  1. Hooks 的資料本體:儲存在 Fiber 節點的 Hooks 鏈結串列中,由 React 運行時管理。

  2. 固定呼叫順序:因為 React 依賴鏈結串列的順序來存取 Hooks 資料,改變順序會導致錯誤。

  3. Hooks 的誕生:解決類別元件的複雜性、邏輯重用困難以及函數元件的局限性。

  4. 設計思維:以函數式思維為基礎,強調模組化、統一管理與簡單性,讓開發更直觀。