原理篇:hooks


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鏈表如下圖:
image

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;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM