作者:京東零售 鄭炳懿
前言
React Hooks
是React
16.8 引入的一個新特性,它允許函式元件中使用state
和其他 React 特性,而不必使用類元件。Hooks
是一個非常重要的概念,因為它們提供了更簡單、更易於理解的React
開發體驗。
React Hooks
的核心原始碼主要包括兩個部分:React
內部的Hook
管理器和一系列預置的Hook
函式。
首先,讓我們看一下React
內部的Hook
管理器。這個管理器是React
內部的一個重要機制,它負責管理元件中的所有Hook
,並確保它們在元件渲染期間以正確的順序呼叫。
內部Hook管理器
示例:
const Hook = {
queue: [],
current: null,
};
function useState(initialState) {
const state = Hook.current[Hook.queue.length];
if (!state) {
Hook.queue.push({
state: typeof initialState === 'function' ? initialState() : initialState,
setState(value) {
this.state = value;
render();
},
});
}
return [state.state, state.setState.bind(state)];
}
function useHook(callback) {
Hook.current = {
__proto__: Hook.current,
};
try {
callback();
} finally {
Hook.current = Hook.current.__proto__;
}
}
function render() {
useHook(() => {
const [count, setCount] = useState(0);
console.log('count:', count);
setTimeout(() => {
setCount(count + 1);
}, 1000);
});
}
render();
在這個示例中,Hook
物件有兩個重要屬性:queue
和current
。queue
儲存元件中所有Hook
的狀態和更新函式,current
儲存當前正在渲染的元件的Hook
連結串列。useState
和useHook
函式則分別負責建立新的Hook
狀態和在元件中使用Hook
。
預置 Hook 函式
useState Hook
以下是useState Hook
的實現示例:
function useState(initialState) {
const hook = updateWorkInProgressHook();
if (!hook.memoizedState) {
hook.memoizedState = [
typeof initialState === 'function' ? initialState() : initialState,
action => {
hook.queue.pending = true;
hook.queue.dispatch = action;
scheduleWork();
},
];
}
return hook.memoizedState;
}
上述程式碼實現了useState Hook
,其主要作用是返回一個state
和更新函式的陣列,state 初始值為initialState
。
在這個實現中,updateWorkInProgressHook()
函式用來獲取當前正在執行的函式元件的 fiber 物件並判斷是否存在對應的hook
。它的實現如下:
function updateWorkInProgressHook() {
const fiber = getWorkInProgressFiber();
let hook = fiber.memoizedState;
if (hook) {
fiber.memoizedState = hook.next;
hook.next = null;
} else {
hook = {
memoizedState: null,
queue: {
pending: null,
dispatch: null,
last: null,
},
next: null,
};
}
workInProgressHook = hook;
return hook;
}
getWorkInProgressFiber()
函式用來獲取當前正在執行的函式元件的fiber
物件,workInProgressHook
則用來儲存當前正在執行的hook
物件。在函式元件中,每一個useState
呼叫都會建立一個新的 hook 物件,並將其新增到fiber
物件的hooks
連結串列中。這個hooks
連結串列是透過fiber
物件的memoizedState
屬性來維護的。
我們還需要注意到在useState Hook
的實現中,每一個hook
物件都包含了一個queue
物件,用來儲存待更新的狀態以及更新函式。scheduleWork()
函式則用來通知React
排程器有任務需要執行。
在React
的原始碼中,useState
函式實際上是一個叫做useStateImpl
的內部函式。
下面是useStateImpl
的原始碼:
function useStateImpl<S>(initialState: (() => S) | S): [S, Dispatch<SetStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
可以看到,useStateImpl
函式的作用就是獲取當前的dispatcher
並呼叫它的useState
方法,返回一個陣列,第一個元素是狀態的值,第二個元素是一個dispatch
函式,用來更新狀態。這裡的resolveDispatcher
函式用來獲取當前的dispatcher
,其實現如下:
function resolveDispatcher(): Dispatcher {
const dispatcher = currentlyRenderingFiber?.dispatcher;
if (dispatcher === undefined) {
throw new Error('Hooks can only be called inside the body of a function component. (https://fb.me/react-invalid-hook-call)');
}
return dispatcher;
}
resolveDispatcher
函式首先嚐試獲取當前正在渲染的fiber
物件的dispatcher
屬性,如果獲取不到則說
明當前不在元件的渲染過程中,就會丟擲一個錯誤。
最後,我們來看一下useState
方法在具體的dispatcher
實現中是如何實現的。我們以useReducer
的
dispatcher
為例,它的實現如下:
export function useReducer<S, A>(
reducer: (prevState: S, action: A) => S,
initialState: S,
initialAction?: A,
): [S, Dispatch<A>] {
const [dispatch, currentState] = updateReducer<S, A>(
reducer,
// $FlowFixMe: Flow doesn't like mixed types
[initialState, initialAction],
// $FlowFixMe: Flow doesn't like mixed types
reducer === basicStateReducer ? basicStateReducer : updateStateReducer,
);
return [currentState, dispatch];
}
可以看到,useReducer
方法實際上是呼叫了一個叫做updateReducer
的函式,返回了一個包含當前狀態和dispatch
函式的陣列。updateReducer
的實現比較複雜,涉及到了很多細節,這裡不再展開介紹。
useEffect Hook
useEffect
是React
中常用的一個Hook
函式,用於在元件中執行副作用操作,例如訪問遠端資料、新增/移除事件監聽器、手動操作DOM
等等。useEffect
的核心功能是在元件的渲染過程結束之後非同步執行回撥函式,它的實現方式涉及到 React 中的非同步渲染機制。
以下是useEffect Hook的實現示例:
function useEffect(callback, dependencies) {
// 透過呼叫 useLayoutEffect 或者 useEffect 方法來獲取當前的渲染批次
const batch = useContext(BatchContext);
// 根據當前的渲染批次判斷是否需要執行回撥函式
if (shouldFireEffect(batch, dependencies)) {
callback();
}
// 在元件被解除安裝時清除當前 effect 的狀態資訊
return () => clearEffect(batch);
}
在這個示例中,useEffect
接收兩個引數:回撥函式和依賴項陣列。當依賴項陣列中的任何一個值發生變化時,
React
會在下一次渲染時重新執行useEffect
中傳入的回撥函式。
useEffect
函式的實現方式主要依賴於React
中的非同步渲染機制。當一個元件需要重新渲染時,React
會將所有的state
更新操作加入到一個佇列中,在當前渲染批次結束之後再非同步執行這些更新操作,從而避免在同一個渲染批次中連續執行多次更新操作。
在useEffect
函式中,我們透過呼叫useContext(BatchContext)
方法來獲取當前的渲染批次,並根據shouldFireEffect
方法判斷是否需要執行回撥函式。在回撥函式執行完畢後,我們需要透過clearEffect
方法來清除當前effect
的狀態資訊,避免對後續的渲染批次產生影響。
總結
總的來說,React Hooks
的實現原理並不複雜,它主要依賴於React
內部的fiber
資料結構和排程系統,透過這些機制來實現對元件狀態的管理和更新。Hooks
能夠讓我們在函式元件中使用狀態和其他React
特性,使得函式元件的功能可以和類元件媲美。
除了useState
、useEffect
等hook
,React
還有useContext
等常用的Hook
。它們的實現原理也基本相似,都是利用fiber
架構來實現狀態管理和生命週期鉤子等功能。
以上是hook
簡單實現示例,它們並不是React
中實際使用的程式碼,但是可以幫助我們更好地理解hook
的核心實現方式。