---
# System prepended metadata

title: 期末報告：React Hooks原理及實作

---

---
title: React Hooks


---
目錄：
[TOC]


 摘要
本報告有稍微探討React Hooks的底層原理，並編寫程式碼實現例子。首先概述React Hooks的背景和目的，然後稍微討論其底層實現原理。接著，介紹一些React Hooks，並通過一個實例演示來展示其用法。最後，總結本報告的內容並提出一些結論。

# 理論篇

## `React`的理念
> 我們認為，React 是用 JavaScript 構建快速響應的大型 Web 應用程序的首選方式。它在 Facebook 和 Instagram 上表現優秀。



* 瓶頸


為滿足60Hz以上的刷新率，瀏覽器必須小於`16.6ms`就刷新一次

而我們又知道GUI渲染線程與JS線程式互斥的，所以`JS腳本執行`和`瀏覽器布局、繪製`不能同時執行

所以當JS腳本執行時間過長時，超過16.6ms，在這一幀瀏覽器就沒有刷新頁面，導致顯示掉幀，造成卡頓

以下段程式碼為例，我們將會在瀏覽器渲染3000個`li`
```typescript
function App() {
  const len = 3000;
  return (
    <ul>
      {Array(len).fill(0).map((_, i) => <li>{i}</li>)}
    </ul>
  );
}

const rootEl = document.querySelector("#root");
ReactDOM.render(<App/>, rootEl);  
```
但是從執行堆棧圖看到，JS執行時間為73.65ms，遠遠多於1幀的時間。
![](https://hackmd.io/_uploads/rJko-J8Oh.png)


為了解決這個問題`React`使用了`時間切片`(time slice)的操作，將較長的任務切分到每一幀執行，這使得必須將同步的更新改為可中斷的異步更新。

React為實現快速響應，在v15升級到v16後重構了整個架構
## 老的React架構
分兩層：
* Reconciler（協調器）—— 負責找出變化的組件 
* Renderer（渲染器）—— 負責將變化的組件渲染到頁面上

### Reconciler（協調器）

[官方解釋](https://legacy.reactjs.org/docs/codebase-overview.html#reconcilers)

當調用`this.setState`、`this.forceUpdate`或`ReactDOM.remder`等其他觸發更新的API時，Reconciler（協調器）就會工作。
1. 調用函數組件、或class組件的render方法，將返回的JSX轉化為虛擬DOM
2. 將虛擬DOM和上次更新時的虛擬DOM對比
3. 通過對比找出本次更新中變化的虛擬DOM
4. 通知Renderer將變化的虛擬DOM渲染到頁面上

### Renderer（渲染器）
[官方解釋](https://legacy.reactjs.org/docs/codebase-overview.html#renderers)
`React`在不同平台有不同的Renderer。
* ReactDOM渲染器，渲染到瀏覽器DOM
* ReactNative渲染器，渲染App原生組件
* ReactTest渲染器，渲染用於測試的純JS對象
* ReactArt渲染器，渲染到Canvas、SVG


### 缺點
在`Reconciler`中，`mount`的組件會調用[`mountComponent`](https://github.com/facebook/react/blob/15-stable/src/renderers/dom/shared/ReactDOMComponent.js#L498)，`update`的組件會調用[`updateComponent`](https://github.com/facebook/react/blob/15-stable/src/renderers/dom/shared/ReactDOMComponent.js#L877)。這兩個方法都會遞迴更新子組件。
於是形成 call stack 的結構，而遞迴的過程是不能中斷且是同步的，如果當遞迴層數過深，一旦時間超過16ms，瀏覽器刷新就會卡頓。


## 新的Recat架構
分三層：
* Scheduler（調度器）—— 調度任務的優先級，高優任務優先進入Reconciler 
* Reconciler（協調器）—— 負責找出變化的組件 
* Renderer（渲染器）—— 負責將變化的組件渲染到頁面上


### Scheduler（調度器）
為了執行程式不超過16.6ms，需要一種機制，讓瀏覽器通知是否有剩餘時間，做為要不要中斷程式的標準。

由於[requestIdleCallback](https://developer.mozilla.org/zh-TW/docs/Web/API/Window/requestIdleCallback)具有瀏覽器兼容性以及不穩定的觸發頻率，因此`React`並沒有採用，而是使用功能更完整的`requestIdleCallback-polyfill` AKA Scheduler。

### Fiber Reconciler（協調器）
在React16中，更新從遞迴變成了可中斷的迴圈。在每次循環都會調用`shouldYield`判斷當前是否有剩餘時間

由於依靠遞迴的VirtualDOM資料無法改為非同步且可中斷的更新，於是誕生了一樣是虛擬的樹狀結構的fiber tree。在fiber架構下，是使用linked list資料結構來遍歷component tree。

在React Fiber架構下，linked list traversal演算法在每一個node之間都只會各有一個child（子節點）、sibling（兄弟節點）、return（父節點）。

```htmlmixed=
<div>
  <h1>Heading 1</h1>
  <h2>Heading 2</h2>
  <h3>Heading 3</h3>
</div>
```

以上結構為例，在fiber架構下只會把第一個 `<h1>` 當作 child，其他則使用 child 的 sibling 來記錄，並且透過 return 指回 parent node。
遍歷fiber tree是以while迴圈實現而非遞迴，遍歷規則如下
* DFS
* child -> 自身 > sibling
舉例：
![](https://hackmd.io/_uploads/BJ8s8AHO3.gif)


[referenlink](https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js#L1673)
```javascript=         
/** @noinline */
function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}
```


## Hooks 是啥?
> Hook 是React 16.8 中增加的新功能。它讓你不必寫class 就能使用state 以及其他React 的功能。

Hook是另一種複用狀態邏輯的解決方案，React開發者一直以來對狀態邏輯的複用方案不斷提出以及改進，從Mixin到高階組件到Render Propss 到現在的Hook。

### Mixin模式
![](https://hackmd.io/_uploads/B1e3uCH_n.png)
Mixin繼承可以被看作是一種通過擴展來收集功能的方式。每個新對象都有一個原型，可以從中繼承更多屬性。原型可以繼承自其他對象，更重要的是可以為任意數量的對象定義屬性。利用這一事實，我們可以促進功能的重用。

在React中，Mixin主要用於在完全不相關的兩個組件之間具有一套基本相似的功能時，可以將其提取出來並通過Mixin注入的方式實現程式碼復用。例如，不同的組件需要每隔一段時間更新一次，我們可以通過創建setInterval()函數來實現這個功能，並在組件銷毀時卸載此函數。因此，可以創建一個簡單的Mixin，提供一個簡單的setInterval()函數，在組件被銷毀時自動清理。

```javascript=
var SetIntervalMixin = {
  componentWillMount: function() {
    this.intervals = [];
  },
  setInterval: function() {
    this.intervals.push(setInterval.apply(null, arguments));
  },
  componentWillUnmount: function() {
    this.intervals.forEach(clearInterval);
  }
};

var createReactClass = require('create-React-class');

var TickTock = createReactClass({
  mixins: [SetIntervalMixin], // 使用 mixin
  getInitialState: function() {
    return {seconds: 0};
  },
  componentDidMount: function() {
    this.setInterval(this.tick, 1000); // 調用 mixin 上的方法
  },
  tick: function() {
    this.setState({seconds: this.state.seconds + 1});
  },
  render: function() {
    return (
      <p>
        React has been running for {this.state.seconds} seconds.
      </p>
    );
  }
});

ReactDOM.render(
  <TickTock />,
  document.getElementById('example')
);
```

### 缺點
1. 不同mixin可能會相互依賴，耦合性太強，導致後期維護成本過高
2. mixin中的命名可能會衝突，無法使用同一命名的mixin
3. mixin即使開始很簡單，它們會隨著業務場景增多，時間的推移產生滾雪球式的複雜化
![](https://hackmd.io/_uploads/H1fVcArd3.png)


### HOC（高階組件）
> A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the React API, per se. They are a pattern that emerges from React’s compositional nature.

> Concretely, a higher-order component is a function that takes a component and returns a new component.

舉例：通過高階組件動態給其他組件增加日誌打印功能，而不影響原先組件的功能

```javascript=
function logProps(WrappedComponent) {
  return class extends React.Component {
    componentWillReceiveProps(nextProps) {
      console.log('Current props: ', this.props);
      console.log('Next props: ', nextProps);
    }
    render() {
      return <WrappedComponent {...this.props} />;
    }
  }
}
```

### Render Props
> The term “render prop” refers to a technique for sharing code between React components using a prop whose value is a function.

> A component with a render prop takes a function that returns a React element and calls it instead of implementing its own render logic.

以下我們提供了一個帶有prop的組件，它能夠動態決定什麼需要渲染，這樣就能對組件的邏輯以及狀態復用，而不用改變它的渲染結構。
```javascript=
class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        {this.props.render(this.state)}
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>移動鼠標!</h1>
        <Mouse render={mouse => (

        )}/>
      </div>
    );
  }
}
```
然而通常我們說的Render Props 是因為模式才被稱為 Render Props ，又不是因為一定要用render對prop進行命名。我們也可以這樣來表示
```javascript=
<Mouse>
  {mouse => (
    <Cat mouse={mouse} />
  )}
</Mouse>
```

### 引入Hooks的動機
引入React Hooks的主要動機是為了解決以下問題：

1. 複雜的組件結構
在React中，當組件邏輯變得複雜時，我們可能會使用高階組件（Higher-Order Components）或渲染屬性（Render Props）等模式來共享邏輯。這種方式可能導致組件層級的深度嵌套，使程式碼難以理解和維護。

Hooks提供了一種更直接的方式來共享和重用組件邏輯，減少了層級嵌套。

2. 難以理解的生命週期方法
類組件中的生命週期方法（如componentDidMount和componentDidUpdate）往往需要在不同的生命週期階段編寫相關程式碼。這使得邏輯分散，難以追踪。

Hooks通過提供單個useEffect Hook來處理組件的副作用和生命週期，使得邏輯更加集中和易於理解。

3. 難以重用的狀態邏輯
在類組件中，要在多個組件之間共享狀態邏輯，通常需要使用高階組件或渲染屬性。這樣的模式增加了額外的複雜性，使得狀態邏輯的複用變得困難。

Hooks通過提供useState和其他自定義Hooks，使得狀態邏輯的重用更加簡單和直接。

這些問題和挑戰促使React團隊設計並引入了React Hooks，以改進狀態管理和副作用處理的方式。

# 實作篇
## useState Hook
useState是React Hooks中最常用的一個。它允許我們在函數組件中使用和管理局部狀態（state）。通過useState，我們可以定義一個狀態變量，並在需要時更新它。
以下是useState的基本語法：
```javascript=
const [state, setState] = useState(initialState);
```
* `state`：狀態變量，用於存儲和訪問狀態的值。
* `setState`：用於更新狀態變量的函數。當調用setState時，React會重新渲染組件，並將新的狀態值應用到相應的地方。
* `initialState`：狀態的初始值。

例如，我們可以使用useState來管理一個計數器的狀態：
```javascript=
import React, { useState } from 'react';

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

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </div>
  );
}
```
在上述程式碼中，我們使用useState定義了一個名為count的狀態變量，並通過setCount函數來更新它。每次點擊"Increment"或"Decrement"按鈕時，計數器的值會相應地增加或減少。

## useEffect Hook
useEffect是另一個常用的React Hook，用於處理副作用和生命週期。副作用包括數據獲取、訂閱、手動DOM操作等。在函數組件中，我們可以使用useEffect來處理這些副作用。
以下是useEffect的基本語法：
```javascript=
useEffect(() => {
  // 副作用程式碼
  return () => {
    // 清理程式碼
  };
}, [dependency]);
```
* 副作用程式碼：在useEffect的函數參數中定義副作用的邏輯。這段程式碼將在組件渲染後執行。
* 清理程式碼（可選）：如果需要在組件卸載時執行一些清理操作，可以在返回的函數中定義清理程式碼。
* 依賴數組（可選）：一個數組，包含影響副作用執行的依賴項。只有當依賴項發生變化時，副作用程式碼才會重新執行。

例如，我們可以使用useEffect來獲取和顯示數據：
```javascript=
import React, { useState, useEffect } from 'react';

function DataDisplay() {
  const [data, setData] = useState(null);

  useEffect(() => {
    // 在組件渲染後，獲取數據並更新狀態
    fetchData()
      .then(response => setData(response))
      .catch(error => console.error(error));

    // 在組件卸載時清理操作
    return () => {
      cleanup();
    };
  }, []); // 空的依賴數組，表示副作用只在組件首次渲染後執行

  return (
    <div>
      {data ? (
        <p>Data: {data}</p>
      ) : (
        <p>Loading...</p>
      )}
    </div>
  );
}
```
在上述程式碼中，我們使用useEffect來獲取數據並更新狀態。當組件首次渲染後，副作用程式碼會執行，然後將獲取的數據設置為狀態的值。在組件卸載時，清理程式碼會執行清理操作。

## useContext Hook

useContext Hook用於在React組件之間共享上下文（context）。上下文是一種跨組件層級傳遞數據的方式，它可以幫助我們避免通過多層嵌套的props傳遞數據。 
以下是useContext的基本語法：
```javascript=
const value = useContext(MyContext);
```
* MyContext：上下文對象，通過React.createContext創建。

例如，我們可以創建一個主題上下文，並在組件中使用它來動態設置主題樣式：
```javascript=
import React, { useContext } from 'react';

// 創建上下文
const ThemeContext = React.createContext();

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <ThemeProvider />
    </ThemeContext.Provider>
  );
}

function ThemeProvider() {
  // 使用上下文
  const theme = useContext(ThemeContext);

  return (
    <div className={theme === 'dark' ? 'dark-theme' : 'light-theme'}>
      {/* 組件內容 */}
    </div>
  );
}
```
在上述程式碼中，我們通過React.createContext創建了一個主題上下文（ThemeContext）。然後，在App組件中通過ThemeProvider組件將上下文提供給子組件。在ThemeProvider組件中，我們使用useContext來獲取當前的主題，從而動態設置組件的樣式。

## useReducer Hook

useReducer Hook是一個類似於Redux中的reducer的狀態管理工具。它適用於管理複雜的狀態邏輯，當狀態具有多個相關值時特別有用。 
以下是useReducer的基本語法：
```javascript=
const [state, dispatch] = useReducer(reducer, initialState);
```
* state：狀態變量，用於存儲和訪問狀態的值。
* dispatch：派發函數，用於觸發狀態變更的操作。
* reducer：一個函數，根據舊的狀態和操作類型返回新的狀態。reducer函數接收兩個參數：舊的狀態和操作對象。
* initialState：狀態的初始值。

例如，我們可以使用useReducer來管理一個簡單的計數器狀態：
```javascript=
import React, { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

function Counter() {
  const [count, dispatch] = useReducer(reducer, 0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
    </div>
  );
}
```
在上述程式碼中，我們定義了一個reducer函數來處理不同的操作類型（INCREMENT和DECREMENT），並返回新的狀態。通過useReducer，我們將reducer函數和初始值0傳遞給它，並獲取到當前的計數器狀態以及派發函數。每次點擊"Increment"或"Decrement"按鈕時，我們通過dispatch函數觸發對應的操作，從而更新計數器的狀態

##  useCallback和useMemo Hooks

useCallback和useMemo是用於性能優化的Hooks。它們都可以用於避免不必要的重新計算。 
* useCallback用於緩存函數引用，避免在每次渲染時創建新的函數實例。
```javascript=
const memoizedCallback = useCallback(() => {
  // 函數體
}, [dependency]);

```
* useMemo用於緩存計算結果，只有當依賴項發生變化時才重新計算。
```javascript=
const memoizedValue = useMemo(() => {
  // 計算結果
}, [dependency]);
```
在性能敏感的場景下，使用useCallback和useMemo可以有效減少不必要的計算和渲染。

## useRef Hook
useRef Hook用於獲取DOM節點的引用或保存其他可變值。它類似於類組件中的ref屬性。 

以下是useRef的基本語法：
```javascript=
const ref = useRef(initialValue);
```
* ref：引用對象，可以在組件的整個生命週期中保持穩定。
* initialValue：初始值。
* 例如，我們可以使用useRef來獲取輸入框的引用並設置焦點：

```javascript=
import React, { useRef } from 'react';

function TextInput() {
  const inputRef = useRef(null);

  const handleClick = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>Focus</button>
    </div>
  );
}
```
在上述程式碼中，我們使用useRef創建了一個inputRef引用對象，並將它綁定到輸入框的ref屬性上。當點擊"Focus"按鈕時，我們調用inputRef.current.focus()來設置輸入框的焦點。

## React Hooks的最佳用法


1. 遵循Hooks規則
React Hooks有一些特定的規則需要遵循，以確保其正確使用。

* Hooks只能在函數組件的頂層使用，不要在循環、條件語句或嵌套函數中使用。
* Hooks的調用順序必須保持一致，不要在條件語句中改變調用順序。
* Hooks的命名必須以"use"開頭，這有助於標識其為Hooks。
遵循這些規則可以確保Hooks的正確性和一致性。

2. 將邏輯拆分為獨立的Hooks
為了提高程式碼的可讀性和可維護性，建議將組件的狀態管理邏輯和副作用邏輯拆分為獨立的自定義Hooks。

將邏輯封裝為獨立的Hooks可以使組件更加簡潔和可複用。這樣，當組件的需求變化時，只需要調整對應的Hooks，而不需要修改組件本身的程式碼。

3. 注意依賴項的設置
在使用useEffect、useCallback和useMemo等Hooks時，需要注意正確設置依賴項。

依賴項是一個數組，用於指定在依賴項變化時觸發Hooks的重新執行。如果依賴項沒有正確設置，可能會導致意外的行為和性能問題。

確保依賴項準確反映需要跟踪的數據，避免不必要的重新計算和渲染。

4. 使用React DevTools進行調試
React DevTools是一個強大的調試工具，可以幫助我們理解組件的狀態和渲染行為。

在使用React Hooks時，建議使用React DevTools來檢查組件的狀態、props和Hooks的執行順序。這有助於調試和優化組件的性能。


# 總結

本報告探討了React Hooks的底層原理，並提供了一些相關的背景知識。首先，介紹了React的理念和React v16之前的架構，解釋了由於同步更新的限制而導致應用程序卡頓的問題。接著，介紹了React v16之後的架構，包括Scheduler、Fiber Reconciler和Renderer，並詳細解釋了Fiber架構下的更新機制。最後，引入了React Hooks作為解決狀態邏輯複用的解決方案，並比較了Mixin、HOC和Render Props等其他解決方案的缺點。

結論：
1. React v16的架構重構解決了同步更新造成的應用程序卡頓問題，並提供了可中斷的異步更新機制。
2. React Hooks提供了一種更簡單、更直觀的方式來實現狀態邏輯的複用，避免了Mixin、HOC和Render Props等解決方案的一些缺點。
3. 使用React Hooks可以使程式碼更加簡潔、易於理解和維護，並提高開發效率。
4. 開發者應該適時地使用React Hooks來提高程式碼的可讀性和可重用性，並根據具體的業務場景選擇最適合的解決方案。


# reference
[React官方](https://reactjs.org)
[听说你还不懂React Hook？ - 知乎专栏](https://zhuanlan.zhihu.com/p/81323354)
[React 開發者一定要知道的底層機制— React Fiber Reconciler](https://medium.com/starbugs/react-開發者一定要知道的底層架構-react-fiber-c3ccd3b047a1)

