react源码debugger-各个hooks的逻辑实现(useState和useEffect)

Posted coderlin_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了react源码debugger-各个hooks的逻辑实现(useState和useEffect)相关的知识,希望对你有一定的参考价值。

了解react的整体流程,会有助于理解本文。

hooks是什么?

要了解hooks是什么,我们得先想知道react怎么执行函数组件。

先看看函数组件的fiber是什么?

const fiber = 
    type: f App(), //函数本身,
    memoziedState: , //hooks链表
	updateQueue: , //effects链表
    ....

对于函数组件,我们现在只需要关注他这几个属性就行了

首先看到renderWithHooks函数,他是执行函数组件的方法。

function renderWithHooks(current, workInProgress, Component, props,secondArg, nextRenderLanes)
    	
    
  // 将当前fiber赋值给currentlyRenderingFiber,hooks执行的时候通过这个获取当前的fiber对象
  currentlyRenderingFiber = workInProgress;
    
  // 清除fiber上面的memoizedState和updateQueue,为啥呢?因为即将执行函数组件,
  // 函数组件的hooks对象以链表形式存放在fiber.memoizedState
  // 函数组件的useLayoutEffect和useEffect以effects链表的形式存放在fiber.updateQueue上
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
    
 // 赋值hooks对象
    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
    
  // 真正执行函数
  let children = Component(props, secondArg);

  // 重新赋值hooks对象。此时不能再调用hooks,会报错
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;
    
  // 重新置空全局变量
  currentlyRenderingFiber = (null: any); //指向当前的函数fiber
  currentHook = null; //current树上的指向的当前调度的 hooks节点。
  workInProgressHook = null; //workInProgress树上指向的当前调度的 hooks节点。
    
    
  return children

如上,可以看出,renderWithHooks主要做了:

  • 将当前调度的函数fiber赋值currentlyRenderingFiber,从名字可以看出,currentlyRenderingFiber就是当前调度的fiber,hooks执行的时候,通过全局变量currentlyRenderingFiber获取到当前的fiber。
  • 清除fiber上面一些属性,如fiber.memoizedState和fiber.updateQueue。fiber.memoizedState存放着hooks链表,指向第一个hooks。而updateQueue存放着useEffect/useLayoutEffect产生的effects。为什么这里要清除呢?因为函数组件即将执行,每一次重新执行函数组件,都会重新生成effects链表和hooks链表。
  • 赋值ReactCurrentDispatcher.current属性,我们调用的hooks实际上就是通过ReactCurrentDispatcher.current获取到的,在一开始是null。所以在函数组件外部执行的时候会报null上没有useState等的错误。可以看到执行函数组件之后,又赋值了ContextOnlyDispatcher,这个其实就是用来抛出错误的,因为函数组件已经执行完毕了,不能再执行hooks了。
  • 执行函数组件,可能会执行各种hooks。
  • 执行完函数组件之后,需要重新赋值全局变量,比如currentlyRenderingFiber置为null。因为要轮到下一个函数fiber调度了。curretnHook置为null。这个全局变量其实就是用来指向current fiber上的hooks节点,通过currentHook,复制出一个新的workInprogress hook。workInporgressHook跟currentHook一样,不过它指向的是wokrInporgress hook。
  • 返回children

现在来看这个demo。

const App2 = () => 
  const [state, setState] = useState(1);
  const stateMemo = useMemo(() => 
    state + 1;
  , [state]);
  const stateUseCallback = useCallback(() => 
    return state + 1;
  , [state]);
  const stateRef = useRef(state);
  useEffect(
    function useEffect() 
      console.log("useEffect=====create");
      return () => 
        console.log("effect====destory");
      ;
    ,
    [state]
  );
  useLayoutEffect(
    function useLayoutEffect() 
      console.log("useLayoutEffect=====create");
      return () => 
        console.log("useLayoutEffect====destory");
      ;
    ,
    [state]
  );

  return (
    <div onClick=() => setState(3)>
      <div>state: state</div>
      <div><>memo: stateMemo</></div>
      <div>callback: stateUseCallback()</div>
    </div>
  );
;

预知识:

每一个hooks都会创建一个hooks对象。结构基本一样

const Hook = 
    queue: , //挂载着更新的操作,类似于类fiber.updateQueue
	memoziedState: , //保存hook的状态,不同的hook保存的数据不同
    baseState: , // 如果有跳过的update,存放的就不是最新的state,而且跳过的update之前的state
    baseQuuee: , //存放着因为优先级较低而跳过的hook
    next:  ,//指针,指向下一个hook

上面我们说过,fiber.memoizedState存放着hooks链表。

从demo debugger上,发现函数fiber是这样的


    memoziedState: 
        baseQueue: null
        baseState: 1
        memoizedState: 1,
        queue: ,
        next: 
            baseQueue: null
            baseState: null
            memoizedState: (2) [2, Array(1)],
            queue: null,
                next: 
                    baseQueue: null
                    baseState: null
                    memoizedState: current: 1
                    next: memoizedState: …, baseState: null, baseQueue: null, queue: null, next: …
                    queue: null
                
        
    

可以看到fiber.memozedState就存放着hooks链表,每一个hook通过next指针相连。

useState

useState的第一次执行的函数就是mountState

function mountState(initialState)
    // 创建hooks对象
    const hook = mountWorkInProgressHook();
    
    //初始值是函数就执行
     if (typeof initialState === 'function') 
    initialState = initialState();
  
         
   // initState存放到hook对象上的memoizedState和baseState
  hook.memoizedState = hook.baseState = initialState;
         
  // 创建queue对象,每个hooks都有一个queue对象,用来存放当前hooks产生的update等信息。
  const queue: UpdateQueue<S, BasicStateAction<S>> = 
    pending: null, //存放update链表
    interleaved: null, 
    lanes: NoLanes, //优先级
    dispatch: null, 
    lastRenderedReducer: basicStateReducer, //存放reducer,useState是指定的,而useReducer是自定义的,所以说				useState是特殊的useReducer
    // 上一次render时候的update
    lastRenderedState: (initialState: any),
  ;
  hook.queue = queue;
    
    // 派发action的函数,通过bind传入了当前的fiber,和Queue对象
  const dispatch: Dispatch = queue.dispatch = dispatchSetState.bind(
    null,
    currentlyRenderingFiber, //函数的fiber
    queue,
  );
    
  // 返回初始化状态和dispatch
  return [hook.memoizedState, dispatch];

可以看到mountState做了

  • 调用mountWorkInProgressHook创建hooks对象。
  • 如果初始值是函数,就执行。
  • 更新hooks属性
  • 创建queue对象。
  • 创建dispatch函数,就是他来派发action。
先看看mountWorkInProgressHook
function mountWorkInProgressHook(): Hook 
  const hook: Hook = 
    memoizedState: null, // useState中 保存 state 信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和 deps | useRef 中保存的是 ref 对象。
    baseState: null,  //usestate和useReducer中,一次更新中 ,产生的最新state值。
    baseQueue: null, //usestate和useReducer中 保存最新的更新队列,存放着因为优先级比较低跳过的update链表
    queue: null,  //存放着本次更新相关的信息。
    next: null, //指针
  ;

  // 函数组件第一个hooks的时候,为空,通过workInProgressHook指针连接起整个函数组件的hooks链表。
  if (workInProgressHook === null) 
    // hooks链表第一个,挂载在fiber.memoizedState
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
   else 
    // 插在上一个hook对象后面,形成链表
    workInProgressHook = workInProgressHook.next = hook;
  
  return workInProgressHook;

可以看到,它创建了hooks,并且将hooks挂载到了fiber.memoizedState上,以链表的形式。workInProgressHook就是用来执行当前的hook。

queue对象

这个对象存放着更新的一系列值。

const queue: UpdateQueue<S, BasicStateAction<S>> = 
    pending: null, //存放update链表
    lanes: NoLanes, //优先级
    dispatch: null,  //存放着setState
    lastRenderedReducer: basicStateReducer, //存放reducer,useState是指定的,而useReducer是自定义的,所以说useState是特殊的useReducer
    // 上一次render时候的update
    lastRenderedState: initialState, //用来给setState第一次调用的时候做优化。
  ;

我们现在知道useState初始化会创建hooks,初始化hook.queue,挂载到fiber.memoizedState上,然后返回hook.memoizedState和dispatch。

更新的时候

更新调用的是dispatch。从mountState可以看到。

const dispatch: Dispatch = queue.dispatch = dispatchSetState.bind(
    null,
    currentlyRenderingFiber, //函数的fiber
    queue,
  );

调用dispatchSetState,并且传入了fiber和queue。

function dispatchSetState( fiber, queue, action
 const lane = requestUpdateLane(fiber);

  // 创建update对象
  const update: Update<S, A> = 
    lane, //优先级
    action, //值,对于useReducer来说是一个action
    hasEagerState: false, 
    eagerState: null,
    next: (null: any), //指针
  ;

 // 插入当前update
 enqueueUpdate(fiber, queue, update, lane);


  // 获取workInprogress fiber
  const alternate = fiber.alternate;
  
if( fiber.lanes === NoLanes  &&
      (alternate === null || alternate.lanes === NoLanes))
   // 让如果当前的fiber没有处于调度状态,那么第一次调用setState就会走优化逻辑。
    
    const lastRenderedReducer = queue.lastRenderedReducer; //获取reducer
    
     const currentState: S = (queue.lastRenderedState: any); // 当前的state
     const eagerState = lastRenderedReducer(currentState, action); //获取最新的值
    
     update.hasEagerState = true; //打标机,表示这个update已经计算过值了。
     update.eagerState = eagerState;
    
    if (is(eagerState, currentState)) 
           // 如果state没变,不调度
            return;
          


   const eventTime = requestEventTime(); //获取当前事件的执行时间
    // 开启新的一轮调度。
    const root = scheduleUpdateOnFiber(fiber, lane, eventTime);

可以看到dispatchSetState主要做了

  • 创建update,每一个更新都会创建对应的update对象。
  • 调用enqueueUpdate将update插入hook.queue.pending上,以链表的形式。
  • 判断是否处于调度状态,如果不是,当前的setState就可以直接计算值了。然后判断计算出来的值跟之前的state是否相等,相等则不调度,这也是函数的setState跟来组件的this.setState的区别。这里也不用担心说,万一计算出的值变了,那不还得继续调度,在这里如果计算值之后,会将update.hasEagerState置为true,表示已经计算过了,那么等调度完处理update的时候,就不会重新计算,而且直接取update.eagerState最后更新后的值。
  • 调用scheduleUpdateOnFiber开启新的一轮调度。

总结:现在知道了setState会创建update,插入到hook.queue.pending上。然后调用scheduleUpdateOnFiber开启新的一轮调度。

接着我们看update阶段执行的useState,执行的是updateState函数。

// useState的update函数
function updateState<S>(
  initialState: (() => S) | S,
): 
  // 跟useReducer调用同样的函数,不过第一个
  return updateReducer(basicStateReducer, (initialState: any));

这个updateState,其实也是调用updateReducer,从名字上可以看到,就是useReducer的更新函数,但是它默认传了basicStateReducer

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S 
  // $FlowFixMe: Flow doesn't like mixed types
  return typeof action === 'function' ? action(state) : action;

这个就是一个处理,如果setState传入的是普通值,那么reducer就直接返回,如果传入的是函数,那么就将当前的state传入,将返回值作为返回。这里也解释了为什么setState可以获取最新的State。我们来看updateReducer怎么计算新的state的。

function(reducer, initialArg)
  // 通过current fiber的Hooks对象,一个一个复制对应的hooks返回
  const hook = updateWorkInProgressHook();
   // 获取更新队列
  const queue = hook.queue;
  queue.lastRenderedReducer = reducer; //赋值redcuer,每一次useReducer执行,都会重新赋值reducer
    
    // ----------- 拼接跳过的update-----------
    const current: Hook = (currentHook: any); //获取当前hooks的对应在current fiber上的hook
    let baseQueue = current.baseQueue; //获取因为优先级较低而跳过的update链表
    const pendingQueue = queue.pending; //获取当前的update任务链表
    if (pendingQueue !== null) 
          if (baseQueue !== null) 
      const baseFirst = baseQueue.next;
      const pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  
    
   
    // -----------处理update链表--------------
    // 如果有跳过的Update,他们存放在了hooks.baseQueue上面,将他们取下来,然后插入到当前的update链表之前
   //存在更新的链表。
  if (baseQueue !== null) 
     // ----- 定义一些变量存储值---------
    // We have a queue to process.
    const first = baseQueue.next; //获取第一额update
    let newState = current.baseState; // 保存每一个update处理后得到的最新的state
 
   let newBaseState = null;   
   //保存着这次调度之后,fiber的最新state,跟newState不一样的是,有些update因为优先级较低被跳过,所以newBaseState的值是停留在被跳过的update的state,为的就是保证状态不丢失

    let newBaseQueueFirst = null; //新的需要跳过的update链表的第一个指针
    let newBaseQueueLast = null; //新的需要跳过的update链表的最后一个指针

    let update = first;
    
    // do-while循环处理update
    do 
      
        // -----------优先级判断-----------------
      const updateLane = update.lane; //优先级
        if(!isSubsetOfLanes(renderLanes, updateLane))
            //如果当前的update优先级不够
            
         //clone一个Update
         const clone: Update<S, A> =  
          lane: updateLane,
          action: update.action,
          hasEagerState: update.hasEagerState,
          eagerState: update.eagerState,
          next: (null: any),
        ;
            
          // --- 将跳过的update以链表的形式放到newBaseQueueFirst------
         if (newBaseQueueLast === null)  //这是第一个跳过的update
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState; 
         else 
          // 不是第一个跳过的,直接插在链表后面
          newBaseQueueLast = newBaseQueueLast.next = clone;
        
            
          // 更新队列中的剩余优先级。
        currentlyRenderingFiber.lanes = mergeLanes(
          currentlyRenderingFiber.lanes,
          updateLane,
        );
     else 
         //如果这个update优先级够了
         
       //-----为了保持状态的连续性,若update之前有别的update被跳过了,那么当前的update也得clone一份存入跳过的链表
          if (newBaseQueueLast !== null) 
          const clone: Update<S, A> = 
            lane: NoLane,
            action: update.action,
            hasEagerState: update.hasEagerState,
            eagerState: update.eagerState,
            next: (null: any),
          ;
          newBaseQueueLast = newBaseQueueLast.next = clone;
        
         
          // 开始处理这个update了,这里对照着上面setState做的优化,如果值已经计算了,直接取update.eagerState,
        if (update.hasEagerState) 
          newState = ((update.eagerState: any): S);
         else 
          const action = update.action;
          // 通过reducer计算最新的state,赋值给newState
          // 这里每一次调用reducer,都会将最新计算得到的NewState传入进去,所以这也是为什么,setState就可以获取到最新的state的原因了。因为上一个update处理过的state,已经赋值给了newState了。
          newState = reducer(newState, action);
        

     
        
      // 处理下一个update
      update = update.next;
       while (update !== null && update !== first); //直到所有的Update处理完毕
        
        
        
      //----- 处理新的state了,如果有跳过的update,那么hook.memozedState就不能是最新的state。而是跳过的第一个update的时候的state
    if (newBaseQueueLast === null) 
      //如果没有跳过的update,当前hooks.baseState才是最新的state
      newBaseState = newState;
     else 
      // 跳过的update链表首尾相连
      newBaseQueueLast.next = (newBaseQueueFirst: any);
      
      
      
    
      
    // 将状态更新到hook对象上
    hook.memoizedState = newState; //存放最新的state
    hook.baseState = newBaseState;  //如果有跳过的update,存放的就不是最新的state,而且跳过的update之前的state
    hook.baseQueue = newBaseQueueLast; //baseQueue存在跳过的update链表

    queue.lastRenderedState = newState;
  
    
  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch]; //这里返回的状态并不是newState,而是newBaseState

如上,可以看到useState的update执行的函数做的事情还是很多的。

  • 调用updateWorkInProgressHook获取到hooks对象

  • 判断是否有跳过的update链表,有的话就跟现在的update链表拼接到一起处理。

  • 递归处理update。如果优先级不够的,就先放着,优先级够的,还得判断他前面有没有跳过的update,有的话那么当前的update也得clone一份放入跳过的链表之中。然后处理update

  • 处理的时候会判断当前update是否已经计算过了,不是的话,就会调用reducer将hook.baseState传入。注意,这里处理update链表的基础state是hook.baseState。而不是hook.memoizedState,这是因为,为了状态的连续性,不止优先级较低的update,后面的update都得存起来,而且计算update的state必须留在跳过的update的时候的hook.state才行。这里通过reducer函数处理action,也可以知道为什么setState传入函数就可以获取最新的state,因为上一个Update已经计算得到新的值,然后才传入reducer的。

  • 接着更新hook,可以看到,如果有跳过的update,那么hook.baseState赋值的是newBaseState。而且只当有没调过的update的时候,才会执行newBaseState=newState

  • 最后返回最新的到的hook.memoizedState。

这里我们需要注意的有两点,一个是updateWorkInProgressHook如何获取hooks。一个是优先级跳过update,react为了避免状态丢失做的处理。

updateWorkInProgressHook
function updateWorkInProgressHook()
     let nextCurrentHook: null | Hook;
  if (currentHook === null) 
    // 函数组件第一个hooks,为null。
    // 获取current fiber
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) 
      // 从curentFiber上面获取mount时候创建的hook对象,现在nextCurrentHooks已经有一条完整的当前current fiber的hooks链表了
      nextCurrentHook = current.memoizedState;
     else 
      nextCurrentHook = null;
    
   else 
    // 第二次,直接从current fiber上往下取第二个hooks
    nextCurrentHook = currentHook.next;
  

  let nextWorkInProgressHook: null | Hook; //获取workInprogress fiber下一个hook
  if (workInProgressHook === null) 
    // 函数组件第一个hooks执行的时候,为null
    
    //update时候, 获取workInprogress fiber上面第一个hooks对象,应该为null
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
   else 
    // 直接往workInprogress fiber的next下获取
    nextWorkInProgressHook = workInProgressHook.next; //null
  

  if (nextWorkInProgressHook !== null) 
    // 已经有值了,将nextWorkInProgressHook赋值给workInProgressHook去返回,
    // There's already a work-in-progress. Reuse it.
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;

    currentHook = nextCurrentHook;
   else 
    // Clone from the current hook.
    // update 第一次进来应该为null
    // 第一次进来nextCurrentHooks不应该为null
    if (nextCurrentHook === null) 
      throw new Error('Rendered more hooks than during the previous render.');
    

    // 指向current fiber对应的hooks对象
    currentHook = nextCurrentHook;

    // 从Curent fiber上面复制一个hooks给workInprogress fiber
    const newHook: Hook = 
      memoizedState: currentHook.memoizedState,

      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,

      next: null,
    ;

    // update第一次为null 
    if (workInProgressHook === null) 
      // This is the first hook in the list.
      // 将新创建的Hooks复制给wokrInprogress fiber的memoizedState,
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
     else 
      // Append to the end of the list.
      workInP

以上是关于react源码debugger-各个hooks的逻辑实现(useState和useEffect)的主要内容,如果未能解决你的问题,请参考以下文章

react源码debugger-各个hooks的逻辑实现(useState和useEffect)

react源码debugger-17,18版本的事件更新流程

react源码debugger-17,18版本的事件更新流程

react源码debugger-各种fiber的completeWork阶段

react源码debugger-commit阶段的完成

react源码debugger-commit阶段的完成