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