---
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幀的時間。

為了解決這個問題`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
舉例:

[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模式

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即使開始很簡單,它們會隨著業務場景增多,時間的推移產生滾雪球式的複雜化

### 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)