React Hooks原理
React Hooks 簡介
React Hooks 是 React 16.8 以及之后版本的產物,React Hooks 就是一堆鈎子函數,不同的鈎子函數提供了不同的功能,React 通過這些鈎子函數對函數型組件進行增強。Hooks 允許你在不編寫 class 的情況下使用狀態(state)和其他 React 特性。 你還可以構建自己的 Hooks, 跨組件共享可重用的有狀態邏輯。
踐行代數效應
React 核心成員 Sebastian Markbåge (Hooks 的發明者)曾說:我們在 React 中做的就是踐行代數效應。
代數效應是函數式編程的一個概念,它所解決的一部分問題只在純函數式編程中才存在,是為了分離一些副作用。代數效應實際上是一個比較領先的理念(寫這篇博客為止),這種感覺就像你在寫回調去解決異步問題,突然有一天有一個人告訴你有一個叫 async/await 的東西!
有人看到這可能就會說了,我不關心什么破代數效應,不要跟我講大道理,直接給老子上代碼就行了≦(._.)≧
代數效應看起來像是很高深的原理,下面我們用一些虛構的偽代碼來解釋一下什么是代數效應。
假設我們有一個 getName 是根據 id 獲取用戶信息的函數,還有一個 makeFriends 函數用來處理 getName 返回的用戶信息
function getName(id) {
// ... 獲取用戶數據
}
function makeFriends(id1, id2) {
const user1 = getName(id1)
const user2 = getName(id2)
return `${user1.name}和${user2.name}變成了好朋友`
}
現在我們將 getName 變成異步函數
async function getName(id) {
// ... 異步操作
}
async function makeFriends(id1, id2) {
const user1 = await getName(id1)
const user2 = await getName(id2)
return `${user1.name}和${user2.name}變成了好朋友`
}
可以看到,由於 getName 函數變成了異步函數,導致 makeFriends 也需要變成異步函數去獲取 getName 返回的數據,getName 變成異步函數破壞了 makeFriends 函數的同步特性。但是其實以正常編程思想,我才不想關注 getName 是怎樣的實現的,我只在乎 getName 返回的數據。
這其實就是 getName 變成異步函數導致的副作用。
我們嘗試虛構一個類似 try...catch 的語法 —— try...handle 和兩個操作符 perform、resume 去分離一下這樣的副作用
function getName(id) {
const user = perform axios.get(id)
return user
}
function makeFriends(id1, id2) {
const user1 = getName(id1)
const user2 = getName(id2)
return `${user1.name}和${user2.name}變成了好朋友`
}
try {
makeFriends('9527', '9528')
} handle (user) {
if (user) {
resume with user
} else {
resume with {
name: '毛小星'
}
}
}
當 makeFriends 執行到 getName 方法的時候,會執行 perform 后面的語句,perform 跳出當時的執行棧,try...handle 會捕獲 perform 執行的結果,這就是一個效應。這種語法看起來非常像 try...catch,但是一旦 catch 到了 Error,那么當前執行的這條邏輯就完成了,當前的調用棧就銷毀了,那么我們能不能使用一種語法“回到”之前的執行邏輯中去呢?try...handle 讓這種場景成為了可能,handle 捕獲了 perform 的執行結果后,依然會捕獲下一個 perform。
上面這段偽代碼就是代數效應的思想,是不是很簡單,其實代數效應就是將副作用從函數中分離,讓函數變得更加純粹一些,這也是函數式編程的核心思想。
代數效應在 React 中的實踐
React 16.8 中的 Hooks 就是在踐行代數效應,像 useState、useReducer、useRef 等,我們不需要關注函數式組件中的 state 在 Hooks 中是如果保存的,React 會為我們處理。你可以把 useState 看做成是一個 perform State(),這個效應就被 React 處理了,這樣我們就直接使用 useState 返回的 state,編寫我們的業務邏輯即可。
下面我們看看 useState 的基本使用
useState 的基本使用
在 16.8 之前的版本,函數型組件基本只負責展示數據,不負責狀態的保存。useState 的出現就可以讓函數型組件保存狀態了,下面我們來看看 useState 是怎樣的使用的
- useState 可以接收一個初始的值
- 返回值為數組,數組中存儲狀態值和更改狀態值的方法
- useState 方法可以被調用多次,用來保存不同狀態值
- 參數可以是一個函數,函數返回什么,初始狀態就是什么,函數只會被調用一次,用在初始值是動態值的情況
import React, { useState, useEffect } from 'react'
import { Button } from 'antd'
const userMap = new Map([
['9527', { name: '毛小星' }],
['9528', { name: '楊秘書' }],
])
const Friend = () => {
const [count, setCount] = useState(() => 0)
const [id1] = useState('9527')
const [id2] = useState('9528')
const [content, setContent] = useState('')
const getName = (_id) => {
return userMap.get(_id)
}
const makeFriend = (_id1, _id2) => {
const user1 = getName(_id1)
const user2 = getName(_id2)
const result = `${user1.name} 和 ${user2.name} 變成了好朋友`
setContent(result)
}
useEffect(() => {
makeFriend(id1, id2)
}, [id1, id2, count])
return (
<div>
<p>{count && content}</p>
<Button type="primary" onClick={() => setCount((_count) => _count + 1)}>increment</Button>
</div>
)
}
export default Friend
上面這段代碼,是不是和 try...handle 非常像,有點那個味了,是不是?沒錯,useState 和 useEffect 的組合就是 React 踐行代數效應的最好示例。
useState 原理
上面我們在講代數效應的時候,我們說不用關心 useState 里面做了什么,我們只需要使用 useState 返回給我們的 state 即可。誒,但是我就是個好奇寶寶,我就想知道 useState 內部到底是怎樣實現的。
Hooks 架子
下面我們自己實現一個簡易版的 useState,來了解一下 Hooks 內部的基本原理。
我們先來准備一些基礎代碼,也就是我們要寫的 useState 的架子,寫代碼之前,我們先來梳理一下我們需要干什么。
- 首先我們要模擬 useState ,那么我們肯定要先聲明一個 useState 函數
- 然后我們需要一個組件來測試我們實現的 hook,我們暫且叫它 App
- 我們都知道 React 16.8 之后采用了新的 Fiber 架構,Fiber 也就是一個對象用來存儲組件的信息,一般組件都會被存儲在 stateNode 這個屬性上,而 Hooks 的 state 會用鏈表結構被存儲在 Fiber 的 memoizedState 這個屬性上。
- React 設計最精妙之處就在於它的調度,我們需要一個調度函數 schedule
- 組件是需要區分生命周期的,首次渲染和更新階段是不一樣的,我們使用一個 isMount 字段去標識
- 最后我們需要一個 workInprogressHook 來處理最近的一個 hook
下面我們就通過上面的思路來把 hooks 的框架搭出來
let isMount = true // 是否渲染
let workInprogressHook = null // 當前處理 hook
// Fiber對象
const fiber = {
stateNode: App,
memoizedState: null, // 用鏈表去存儲 hook
}
function useState (initialState) {
// todo 實現 useState
}
// 調度
function schedule() {
// 初始化 當前處理 hook
workInprogressHook = fiber.memoizedState
const app = fiber.stateNode()
isMount = false
return app
}
// 測試組件
// 為了簡化流程,我們忽略 DOM 更新
function App () {
const [num, setNum] = useState(0)
const [count, setCount] = useState(0)
return {
onClick() {
setNum(num => num + 1)
},
updateCount() {
setCount(count => count + 1)
}
}
}
// 將調度掛載到 window 對象上,方便測試點擊效果
window.app = schedule()
初始狀態的 useState
上面我們在 useState 函數中留了一個 todo項,在實現 useState 函數之前,我們先來思考一個問題,我們應該怎樣存儲 useState 生成的狀態呢,通常我們調用 useState 是像下面這樣的
const [count1, setCount1] = useState(0)
const [count2, setCount2] = useState(0)
const [count3, setCount3] = useState(0)
在 React 源碼中,React 是通過鏈表結構來存儲這些 hook 的,我們要把所有的 state 通過鏈表的形式存儲,並且我們要將 workInprogressHook 指向當前 hook 方便我們處理,下面我們來試着實現 useState
function useState(initialState) {
let hook // 當前 hook 節點
if (typeof initialState === 'function') {
initialState = initialState();
}
if (isMount) {
hook = {
memoizedState: initialState,
next: null,
}
// 創建 hook 鏈表
// 如果沒有初始化的 hook 則初始化 hook 節點,並將當前處理節點(workInprogressHook)指向當前 hook
// 如果不是初始化的話,則將 當前處理節點(workInprogressHook)的下一個節點指向 hook
if (!fiber.memoizedState) {
fiber.memoizedState = hook
} else {
workInprogressHook.next = hook
}
workInprogressHook = hook
}
// todo 實現更新邏輯
}
更新 state
在完善 useState 的更新邏輯,我們先來想想,既然 state 是需要用鏈表來存儲的,那么update 函數也得需要對應一個鏈表來存儲啊,我們來看看為什么需要鏈表來存儲
const [count, setCount] = useState(0)
return (
<p onClick={() => {
setCount(num => num + 1)
setCount(num => num + 1)
setCount(num => num + 1)
}}>
{num}
</p>
)
可以看到更新函數 setCount 可能不是只調用一次,在 React 中,這些 update 函數被環狀鏈表組合在了一起。這時我們就需要在 hook 上增加一個 queue 屬性來存儲 update 函數
hook = {
memoizedState: initialState,
next: null,
// 保存改變的狀態
// 隊列是因為 有可能有多個更新函數
// setCount(num => num + 1)
// setCount(num => num + 1)
// setCount(num => num + 1)
queue: {
pending: null,
}
}
在 React 源碼中,更新階段會調用 dispatchAction.bind(null, hook.queue) 這個函數來更新 state,我們先來看看是怎樣實現的
function dispatchAction(queue, action) {
// 更新節點
const update = {
action,
next: null,
}
// 構建更新鏈表 環狀鏈表
// queue.pending === null 還沒有觸發更新,創建第一個更新
if (queue.pending === null) {
// u0 -> u0 -> u0
update.next = update
} else {
// u0 -> u0
// u1 -> u0 -> u1
update.next = queue.pending.next
queue.pending.next = update
}
queue.pending = update
// 觸發更新
schedule()
}
環狀鏈表的操作可能不太容易理解,下面我們來詳細講解下。
- 首先,當產生第一個 update 的時候(我們叫它 u0),此時queue.pending === null。update.next = update;即u0.next = u0,他會和自己首尾相連形成單向環狀鏈表。然后queue.pending = update;即queue.pending = u0
queue.pending = u0 ---> u0
^ |
| |
---------
- 當產生第二個update(我們叫他u1),update.next = queue.pending.next;,此時queue.pending.next === u0, 即u1.next = u0。queue.pending.next = update;,即u0.next = u1。然后queue.pending = update;即queue.pending = u1
queue.pending = u1 ---> u0
^ |
| |
---------
這樣做的好處就是,當我們需要遍歷 update 時,queue.pending.next指向第一個插入的update,方便我們去操作 update 函數。邏輯還是比較清晰明了的,如果上面看不懂的話,需要去好好補一下數據結構了哦。
完善 useState
在 dispatchAction 中,我們將 update 構建成環狀鏈表后, 接着我們就可以繼續實現 useState 中的更新邏輯,當我們需要更新 state 時,我們就需要遍歷環狀鏈表,將新的狀態更新到 update 函數中去,當遍歷完,我們將鏈表清空,最后我們將新的 state 和 update 函數返回即可。
function useState(initialState) {
let hook // 當前 hook 節點
if (typeof initialState === 'function') {
initialState = initialState();
}
if (isMount) {
... mount 階段
} else {
// 如果是 update 的情況,則將 hook 指向 workInprogressHook
// workInprogressHook 指向 hook 鏈表的下一個節點
hook = workInprogressHook
workInprogressHook = workInprogressHook.next
}
// 處理更新 遍歷更新函數的環狀鏈表
// 獲取初始狀態
let baseState = hook.memoizedState
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next
do {
const action = firstUpdate.action
// 處理更新狀態
baseState = action(baseState)
firstUpdate = firstUpdate.next
} while (firstUpdate !== hook.queue.pending.next) // 遍歷完環狀鏈表
// 清空鏈表
hook.queue.pending = null
}
hook.memoizedState = baseState
return [baseState, dispatchAction.bind(null, hook.queue)]
}
淺析 Hooks 源碼
上面我們實現一個簡單的 useState,我們使用 isMount 來判斷更新時機,但是 React 中沒有這么 low,React 中使用了不同的 hash 值來標識不同的 hooks 的狀態
本篇博客 React 源碼為 16.12.0 版本
// 利用 hash 來存儲不同狀態的方法
// mount 階段
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useResponder: createDeprecatedResponderListener,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
};
// update 階段
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useResponder: createDeprecatedResponderListener,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
};
Redux 的作者 Dan Abramov 在加入 React 團隊中也是將 Redux 的思想帶入了 React 中,useState 和 useReducer 這兩個 hook 就是他的代表作,而且從本質來說,useState 不過就是預置了 reducer 的 useReducer,下面的源碼會印證這點。
mount 階段的 useState 和 useReducer
在 mount 階段,useState 會調用 mountState, 而 useReducer 則會調用 mountReducer
下面我們來看看這兩個方法
// \react\packages\react-reconciler\src\ReactFiberHooks.js
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// 創建hook對象
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
function mountReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = mountWorkInProgressHook();
let initialState;
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = ((initialArg: any): S);
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
能看到 mountState 和 mountReducer 的區別就是 queue 中 lastRenderedReducer 字段
const queue = (hook.queue = {
// 與極簡實現中的同名字段意義相同,保存update對象
pending: null,
// 保存dispatchAction.bind()的值
dispatch: null,
// 上一次render時使用的reducer
lastRenderedReducer: reducer,
// 上一次render時的state
lastRenderedState: (initialState: any),
});
mountReducer 的 lastRenderedReducer 接收的就是傳入你自定義的 reducer;而 mountState 接收的 lastRenderedReducer 是一個預置的 basicStateReducer。
下面我們來看看 basicStateReducer 的實現
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
// $FlowFixMe: Flow doesn't like mixed types
return typeof action === 'function' ? action(state) : action;
}
這也直接證明了 useState 即 reducer 為 basicStateReducer 的 useReducer。
update 階段的 useState 和 useReducer
在 update 階段 updateState 則是直接調用了 updateReducer 方法,更加證明了 useState 就是特殊的 useReducer
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
下面我們來看看 updateReducer 是怎樣實現的
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
// 獲取當前hook
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
// ...計算 newState 的過程
hook.memoizedState = newState;
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
源碼這個部分比較長,我只保留了一些主干代碼,大致流程就是重新計算新的 state,然后將新的 state 返回。
調用更新函數
我們在使用 setCount((count) => count + 1) 這樣的更新函數更新 state 的時候,會觸發 dispatchAction 函數,這個時候當前的函數組件對應的 Fiber 和 對應的更新方法(hook.queue)就通過調用 dispatchAction.bind 傳入了方法內
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
const currentTime = requestCurrentTimeForUpdate();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = computeExpirationForFiber(
currentTime,
fiber,
suspenseConfig,
);
const update: Update<S, A> = {
expirationTime,
suspenseConfig,
action,
eagerReducer: null,
eagerState: null,
next: (null: any),
};
// 構建 update 環狀鏈表
// Append the update to the end of the list.
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
const alternate = fiber.alternate;
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
// This is a render phase update. Stash it in a lazily-created map of
// queue -> linked list of updates. After this render pass, we'll restart
// and apply the stashed updates on top of the work-in-progress hook.
didScheduleRenderPhaseUpdate = true;
update.expirationTime = renderExpirationTime;
currentlyRenderingFiber.expirationTime = renderExpirationTime;
} else {
if (
fiber.expirationTime === NoWork &&
(alternate === null || alternate.expirationTime === NoWork)
) {
// 只保留核心代碼
// ...優化調度渲染
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
update.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
return;
}
}
// 調度
scheduleWork(fiber, expirationTime);
}
}
dispatchAction 函數我只留了一些主干代碼,總結一下:將 update 加入 queue.pending,構建環狀鏈表,在優化渲染后,開啟調度。
if...else... 是 React 的一些優化手段,if 內:
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
)
這是需要 render 階段觸發的更新,所以需要給當前的更新放到一個延遲隊列中,在渲染階段,再重新啟用 workInProgress 去觸發更新
而下面的 else...if
else if (
fiber.expirationTime === NoWork &&
(alternate === null || alternate.expirationTime === NoWork)
)
fiber.expirationTime 保存的是 fiber 對象的 update的優先級,fiber.expirationTime === NoWork 則意味着 fiber 對象上不存在 update。
通過源碼的學習,我們已經知道了 update 計算 state 是在 hook 的聲明階段,在調用階段還通過內置的 reducer 重新計算 state,如果調用階段的 state 和聲明階段的 state 是相等的,那么就完全不需要重新開啟一次新的調度了。
到此我們就了解了 hooks 的理念,其實 React 就是在踐行函數式編程,如果你覺得這篇“人類高質量文章”寫的不錯就點個贊吧!