作者:京東零售 鄭炳懿
前言
React Hooks是React16.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的核心實現方式。