Q: React 引入hooks的原因
A: 讓函數組件可以做類組件的事,可以有自己的state,可以處理一些副作用,獲取ref。
hooks 與 fiber (workInProgress)
hooks主要以三種形態存在於react中:
- HooksDispatcherOnMount:函數組件初始化,建立fiber與hooks之間的關系
- HooksDispatcherOnUpdate: 函數組件的更新,需要 hooks 去獲取或者更新維護狀態。
- ContextOnlyDispatcher: 防止在函數外部調用,直接報錯
所有函數組件的觸發是在 renderWithHooks 方法中:
let currentlyRenderingFiber
function renderWithHooks(current,workInProgress,Component,props){
// 初始化時把處理中的fiber賦值給currentlyRenderingFiber,每個hooks內部讀取的就是currentlyRenderingFiber的內容。
currentlyRenderingFiber = workInProgress;
// memoizedState用來存放hooks列表
workInProgress.memoizedState = null;
workInProgress.updateQueue = null; /* 清空狀態(用於存放effect list) */
// 如果是初始化階段使用HooksDispatcherOnMount,更新階段使用HooksDispatcherOnUpdate
// React 通過賦予 current 不同的 hooks,以此來監控 hooks 是否在函數組件內部調用
ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate
// 此時,函數組件在Component函數內部被真正執行,對應的hooks也會被依次執行
let children = Component(props, secondArg);
ReactCurrentDispatcher.current = ContextOnlyDispatcher; /* 將hooks變成第一種,防止hooks在函數組件外部調用,調用直接報錯。 */
}
注意:函數組件觸發時把處理中的fiber賦值給currentlyRenderingFiber,后面的源碼會調用。
在組件初始化的時候,每一次hooks的執行,都會調用mountWorkInProgressHook。
function mountWorkInProgressHook() {
const hook = {
memoizedState: null, // useState中 保存 state信息 | useEffect 中 保存着 effect 對象 | useMemo 中 保存的是緩存的值和deps | useRef中保存的是ref 對象
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// 在renderWithHooks函數中已經將workInProgress賦值給currentlyRenderingFiber
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// 每一個 hooks 通過 next 鏈表建立起關系
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
如下例子:
export default function Index(){
const [ number,setNumber ] = React.useState(0) // 第一個hooks
const [ num, setNum ] = React.useState(1) // 第二個hooks
const dom = React.useRef(null) // 第三個hooks
React.useEffect(()=>{ // 第四個hooks
console.log(dom.current)
},[])
return <div ref={dom} >
<div onClick={()=> setNumber(number + 1 ) } > { number } </div>
<div onClick={()=> setNum(num + 1) } > { num }</div>
</div>
}
hooks鏈表如下圖:
Q:hooks為什么要放在函數頂部,不能寫在條件判斷語句中?
A:在更新階段,會先復用一份hooks,形成新的hooks鏈表。如果放在if語句中,會造成鏈表對比不一致的情況。
useState
在react中,useState會mountState函數中初始化
function mountState(
initialState
){
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// 如果 useState 第一個參數為函數,執行函數得到state
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
// 保存更新信息
const queue = (hook.queue = {
...
});
// 負責更新的函數
const dispatch = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
)))
return [hook.memoizedState, dispatch];
}
useState通過dispatchAction 來觸發state更新。
dispatchAction 定義如下:
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
)
const [ number , setNumber ] = useState(0)
dispatchAction 就是setNumber,dispatchAction前兩個參數已被固定寫死,我們傳入的是第三個參數action。
下面分析dispatchAction內部實現:
function dispatchAction(fiber, queue, action){
/* 第一步:創建一個 update */
const update = { ... }
const pending = queue.pending;
// 第一次更新
if (pending === null) {
update.next = update;
} else { /* 再次更新 */
update.next = pending.next;
pending.next = update;
}
if( fiber === currentlyRenderingFiber ){
/* 說明當前fiber正在發生調和渲染更新,那么不需要更新 */
}else{
if(fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)){
const lastRenderedReducer = queue.lastRenderedReducer;
// 上一次的state
const currentState = queue.lastRenderedState;
// 本次需要更新的state
const eagerState = lastRenderedReducer(currentState, action);
// 如果兩次更新state相同,則不更新
if (is(eagerState, currentState)) {
return
}
}
scheduleUpdateOnFiber(fiber, expirationTime); /* 發起調度更新 */
}
}
多次調用同一個setState/useState為何會合並處理?
useState 觸發更新的本質是updateReducer,源碼如下:
function updateReducer(){
// 第一步把待更新的pending隊列取出來。合並到 baseQueue
const first = baseQueue.next;
let update = first;
// 當同一個useState在執行時,會繼續給newState賦值,而不是向下執行
do {
newState = reducer(newState, action);
} while (update !== null && update !== first);
hook.memoizedState = newState;
return [hook.memoizedState, dispatch];
}
useEffect
當我們調用useEffect的時候,在組件第一次渲染的時候會調用mountEffect方法
function mountEffect(create,deps){
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// pushEffect創建一個 effect, 如果存在多個effect就會形成副作用鏈表
hook.memoizedState = pushEffect(
HookHasEffect | hookEffectTag,
create, // useEffect 第一次參數,就是副作用函數
undefined,
nextDeps, // useEffect 第二次參數,deps
)
}
對於函數組件,可能存在多個 useEffect / useLayoutEffect ,hooks 把這些 effect,獨立形成鏈表結構,在 commit 階段統一處理和執行。
更新流程就是判斷兩次deps是否相等:
function updateEffect(create,deps){
const hook = updateWorkInProgressHook();
// 如果deps沒有變化,則更新effect list就可以了
if (areHookInputsEqual(nextDeps, prevDeps)) {
pushEffect(hookEffectTag, create, destroy, nextDeps);
return;
}
// 如果deps依賴項發生改變,賦予 effectTag。在commit階段會根據 effectTag 判斷執行effect
currentlyRenderingFiber.effectTag |= fiberEffectTag
hook.memoizedState = pushEffect(HookHasEffect | hookEffectTag,create,destroy,nextDeps)
}
useRef
useRef 就是創建並維護一個 ref 原始對象。
function mountRef(initialValue) {
const hook = mountWorkInProgressHook();
const ref = {current: initialValue};
hook.memoizedState = ref; // 創建ref對象。
return ref;
}
更新:
function updateRef(initialValue){
const hook = updateWorkInProgressHook()
return hook.memoizedState // 取出復用ref對象。
}
useMemo
function mountMemo(nextCreate,deps){
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
function updateMemo(nextCreate,nextDeps){
const hook = updateWorkInProgressHook();
const prevState = hook.memoizedState;
const prevDeps = prevState[1]; // 之前保存的 deps 值
if (areHookInputsEqual(nextDeps, prevDeps)) { //判斷兩次 deps 值
return prevState[0];
}
const nextValue = nextCreate(); // 如果deps,發生改變,重新執行
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}