目录
  • 引言
    • hooks原理
    • useState初始化(mount)
    • mountState
    • mountWorkInProgressHook
    • 处理hook数据
    • 生成dispatch
    • 更新的总结
    • useState触发更新(dispatch)
    • updateState
    • hook数据从哪里来
    • updateWorkInProgressHook
    • hook在条件语句中报错
    • useState计算
    • 双缓存树
  • 总结

    引言

    本系列是讲述从0开始实现一个react18的基本版本。由于React源码通过Mono-repo 管理仓库,我们也是用pnpm提供的workspaces来管理我们的代码仓库,打包我们使用rollup进行打包。

    仓库地址

    具体章节代码3个commit

    上一节中我们讲解了update的过程中,begionWorkcompleteWorkcommitWork的具体执行流程。本节主要是讲解

    • hooks是如何存放数据的,以及一些hooks的规则。
    • 一次dispatch触发的更新整体流程,双缓存树的运用。

    我们有如下代码,在初始化的时候执行useState和调用setNum的时候,是如何更新的。

    function App() {
      const [num, setNum] = useState(100);
      window.setNum = setNum;
      return <div>{num}</div>;
    }
    

    hooks原理

    基于useState我们来讲讲hook在初始化和更新阶段的区别。以及react是如何做到hook不能在条件语句和函数组件外部使用的。

    react中,对于同一个hook,在不同的环境都是有不同的集合区分,这样就可以做到基于不同的执行环境的不同判断。

    首先有几个名词:

    currentlyRenderingFiber: 记录当前正在执行的函数组件的fiberNode

    workInProgressHook: 当前正在执行的hook

    currentHook:更新的时候的数据来源

    memoizedState: 对于fiberNode.memoizedState是存放hooks的指向。对于hook.memoizedState就是存放数据的地方。

    hook的结构如下图:

    React18从0实现dispatch update流程

    useState初始化(mount)

    我们知道当beginWork阶段的时候,对于函数组件,会执行renderWithHooks去生成当前对应的子fiberNode。 我们首先来看看renderWithHooks的逻辑部分。

    export function renderWithHooks(wip: FiberNode) {
      // 赋值操作
      currentlyRenderingFiber = wip;
      // 重置
      wip.memoizedState = null;
      const current = wip.alternate;
      if (current !== null) {
        // update
        currentDispatcher.current = HooksDispatcherOnUpdate;
      } else {
        // mount
        currentDispatcher.current = HooksDispatcherOnMount;
      }
      const Component = wip.type;
      const props = wip.pendingProps;
      const children = Component(props);
      // 重置操作
      currentlyRenderingFiber = null;
      workInProgressHook = null;
      currentHook = null;
      return children;
    }
    

    首先会将currentlyRenderingFiber赋值给当前的FC的fiberNode,然后重置掉memoizedState, 因为初始化的时候会生成,更新的时候会根据初始化的时候生成。

    可以看到对于mount阶段,主要是执行HooksDispatcherOnMount, 他实际上是一个hook集合。我们主要看看mountState的逻辑处理。

    const HooksDispatcherOnMount: Dispatcher = {
      useState: mountState,
    };
    

    mountState

    对于第一次执行useState, 我们根据结果来推算这个函数的主要功能。useState需要返回2个值,第一个是state,第二个是可以引发更新的setState。所以mountState的主要功能:

    • 根据传入的initialState生成新的state
    • 返回dispatch,便于之后调用更新state

    基于hook的结构图,我们知道每一个hook有三个属性, 所以我们首先要有一个函数去生成对应的hook的结构。

    interface Hook {
      memoizedState: any;
      updateQueue: unknown;
      next: Hook | null;
    }
    

    mountWorkInProgressHook

    mountWorkInProgressHook这个函数主要是构建hook的数据。分为2种情况,第一种是第一个hook, 第二种是不是第一个hook就需要通过next属性,将hook串联起来。

    在这个函数中,我们就可以判断当前执行的hook,是否是在函数中执行的。如果是在函数中执行的话,在执行函数组件的时候,我们将currentlyRenderingFiber 赋值给了wip, 如果是直接调用的话,currentlyRenderingFiber则为null,我们就可以抛出错误。

    /**
     * mount获取当前hook对应的数据
     */
    function mountWorkInProgressHook(): Hook {
      const hook: Hook = {
        memoizedState: null,
        updateQueue: null,
        next: null,
      };
      if (workInProgressHook === null) {
        // mount时,第一个hook
        if (currentlyRenderingFiber === null) {
          throw new Error("请在函数组件内调用hook");
        } else {
          workInProgressHook = hook;
          currentlyRenderingFiber.memoizedState = workInProgressHook;
        }
      } else {
        // mount时,后续的hook
        workInProgressHook.next = hook;
        workInProgressHook = hook;
      }
      return workInProgressHook;
    }
    

    当第一次执行的时候,workInProgressHook的值为null, 说明是第一个hook执行。所以我们将赋值workInProgressHook正在执行的hook, 同时将FC fiberNodememoizedState指向第一个hook。此时就生成了如下图的结构:

    React18从0实现dispatch update流程

    处理hook数据

    通过mountWorkInProgressHook我们得到当前的hook结构后,需要处理memoizedState以及updateQueue的值。

    function mountState<State>(
      initialState: (() => State) | State
    ): [State, Dispatch<State>] {
      // 找到当前useState对应的hook数据
      const hook = mountWorkInProgressHook();
      let memoizedState;
      if (initialState instanceof Function) {
        memoizedState = initialState();
      } else {
        memoizedState = initialState;
      }
      // useState是可以触发更新的
      const queue = createUpdateQueue<State>();
      hook.updateQueue = queue;
      hook.memoizedState = memoizedState;
      //@ts-ignore
      const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue);
      queue.dispatch = dispatch;
      return [memoizedState, dispatch];
    }
    

    从上面的代码中,我们可以看出memoizedState的处理很简单,就是通过传入的参数,进行赋值处理,重点在于如何生成dispatch

    生成dispatch

    因为触发dispatch的时候,react是要触发更新的,所以必然会和调度有关。

    由于要触发更新,我们就需要创建触发更新的队列

    • 执行createUpdateQueue() 生成更新队列。
    • 将更新队列赋值给当前hook保存起来,方便之后update使用。
    • 将生成的dispatch保存起来,方便之后update使用。
    // useState是可以触发更新的
    const queue = createUpdateQueue<State>();
    hook.updateQueue = queue;
    const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue);
    queue.dispatch = dispatch;
    

    主要是看如何生成dispatch的逻辑,通过调用dispatchSetState它接受三个参数,因为我们需要知道是从哪一个fiberNode开始调度的,所以当前的fiberNode是肯定看需要的。更新队列queue也是需要的,用于执行dispatch的时候触发更新。

    function dispatchSetState<State>(
      fiber: FiberNode,
      updateQueue: UpdateQueue<State>,
      action: Action<State>
    ) {
      const update = createUpdate(action); // 1. 创建update
      enqueueUpdate(updateQueue, update); //  2. 将更新放入队列中
      scheduleUpdateOnFiber(fiber); // 3. 开始调度
    }
    

    所以我们每次执行setState的时候,等同于执行上面函数,但是我们只需要传递action就可以,前2个参数,已经通过bind绑定。

    执行dispatch后,开始新一轮的调度,调和。

    更新的总结

    从上面的代码,我们可以看出我们首先是执行了createUpdateQueue, 然后执行了createUpdate, 然后enqueueUpdate。这里总结一下这些函数调用。

    createUpdateQueue本质上就创建了一个对象,用于保存值

     return {
       shared: {
         pending: null,
       },
       dispatch: null,
     }
    

    createUpdate就是也是返回一个对象。

    return {
      action,
    };
    

    enqueueUpdate就是将createUpdateQueue的pending 赋值。

    {
      updateQueue.shared.pending = update;
    };
    

    最后我们生成的单个hook结构如下图:

    React18从0实现dispatch update流程

    useState触发更新(dispatch)

    当我们执行setNum(3)的时候,我们之前讲过相当于是执行了下面函数, 将传递3为action的值。

    function dispatchSetState<State>(
      fiber: FiberNode,
      updateQueue: UpdateQueue<State>,
      action: Action<State>
    ) {
      const update = createUpdate(action);
      enqueueUpdate(updateQueue, update); 
      scheduleUpdateOnFiber(fiber); // 3. 开始调度
    }
    

    当再次执行到函数组件App的时候,会执行renderWithHooks如下的逻辑。将 currentDispatcher.current赋值给HooksDispatcherOnUpdate

    // 赋值操作
    currentlyRenderingFiber = wip;
    // 重置
    wip.memoizedState = null;
    const current = wip.alternate;
    if (current !== null) {
      // update
      currentDispatcher.current = HooksDispatcherOnUpdate;
    } else {
      // mount
      currentDispatcher.current = HooksDispatcherOnMount;
    }
    

    然后执行App函数,重新会调用useState

    const [num, setNum] = useState(100);

    updateState

    HooksDispatcherOnUpdate中,useState对应的是updateState。对比于mountState的话,updateState主要是:

    • hook的数据从哪里来
    • 会有2种情况执行,交互阶段触发,render的时候触发

    本节主要是分析交互阶段的触发的逻辑。

    hook数据从哪里来

    对比mountState中,我们可以通过新建hook数据结构。这个时候双缓存树的结构就可以解决,还记得我们之前的章节讲的react将正在渲染的和正在进行的分2个树,通过alternate进行链接。整体结构如下图:

    React18从0实现dispatch update流程

    还记得我们mount的时候说过,fiberNode.memoizedState的指向保存着hook的数据。

    所以我们可以通过currentlyRenderingFiber?.alternate中的memoizedState去查找对应的hook数据。

    updateWorkInProgressHook

    更新阶段hook的数据获取是通过updateWorkInProgressHook执行的。

    function updateWorkInProgressHook(): Hook {
      // TODO render阶段触发的更新
      let nextCurrentHook: Hook | null;
      // FC update时的第一个hook
      if (currentHook === null) {
        const current = currentlyRenderingFiber?.alternate;
        if (current !== null) {
          nextCurrentHook = current?.memoizedState;
        } else {
          nextCurrentHook = null;
        }
      } else {
        // FC update时候,后续的hook
        nextCurrentHook = currentHook.next;
      }
      if (nextCurrentHook === null) {
        // mount / update u1 u2 u3 
        // update u1 u2 u3 u4
        throw new Error(
          `组件${currentlyRenderingFiber?.type}本次执行时的Hook比上次执行的多`
        );
      }
      currentHook = nextCurrentHook as Hook;
      const newHook: Hook = {
        memoizedState: currentHook.memoizedState,
        updateQueue: currentHook.updateQueue,
        next: null,
      };
      if (workInProgressHook === null) {
        // update时,第一个hook
        if (currentlyRenderingFiber === null) {
          throw new Error("请在函数组件内调用hook");
        } else {
          workInProgressHook = newHook;
          currentlyRenderingFiber.memoizedState = workInProgressHook;
        }
      } else {
        // update时,后续的hook
        workInProgressHook.next = newHook;
        workInProgressHook = newHook;
      }
      return workInProgressHook;
    }
    

    主要逻辑总结如下:

    • 刚开始currentHook为null, 通过alternate指向memoizedState获取到正在渲染中的hook数据,赋值给nextCurrentHook
    • currentHook赋值为nextCurrentHook, 记录更新的数据来源,方便之后的hook,通过next连接起来。
    • 赋值workInProgressHook标记正在执行的hook

    这里有一个难点,就是nextCurrentHook === null的时候,我们可以抛出错误。

    hook在条件语句中报错

    我们晓得hook是不能在条件语句中执行的。那是如何做到报错的呢?接下来我们根据上面的updateWorkProgressHook源码分析。假如,伪代码如下所示: 在mount阶段的时候,是3个hook,在执行setNum(100),update阶段4个。

    const [num, setNum] = useState(99);
    const [num2, setNum] = useState(101);
    const [num3, setNum] = useState(102);
    if(num === 100) {
     const [num4, setNum] = useState(103);
    }
    

    这里我们就会执行四次updateWorkProgressHook,我们来分析一下。

    • nextCurrentHook = currentHook = m-hook1,第一次后currentHook不为null
    • nextCurrentHook等于m-hook2
    • nextCurrentHook等于m-hook3
    • 第四次的时候nextCurrentHook = m-hook3.next = null, 所以就会走到报错的逻辑。

    React18从0实现dispatch update流程

    useState计算

    上一部分我们已经知道了update的时候,hook的数据来源,我们现在得到数据了,那如何通过之前的数据,计算出新的数据呢?

    • 在执行setNum(action)后,我们知道action 存放在queue.shared.pending
    • queue是存放在对应hookupdateQueue中。所以我们可以拿到action
    • 第三步就是去消费action,即执行processUpdateQueue, 传入上一次的state, 以及我们这次接受的action,计算最新的值。
    function updateState<State>(): [State, Dispatch<State>] {
      // 找到当前useState对应的hook数据
      const hook = updateWorkInProgressHook();
      // 计算新的state逻辑
      const queue = hook.updateQueue as UpdateQueue<State>;
      const pending = queue.shared.pending;
      if (pending !== null) {
        const { memoizedState } = processUpdateQueue(hook.memoizedState, pending);
        hook.memoizedState = memoizedState;
      }
      return [hook.memoizedState, queue.dispatch as Dispatch<State>];
    }
    

    这样,我们就在渲染的时候拿到了最新的值,以及重新返回的dispatch

    双缓存树

    在第一次更新的时候,我们的双缓存树还没有建立起来,在第一次更新之后,双缓存树就建立完成。

    之后每一次调和生成子fiberNode的时候,都会利用alternate指针去重复利用相同type和相同key的节点。

    例如初始化的时候num的值为3, 通过setNum(4)调用第一次更新后。首先会创建一个wip tree

    React18从0实现dispatch update流程

    在执行完commitWork后,屏幕上渲染为4后,root.current的指向会被修改 为wip tree

    当我们再setNum(5)的时候,第二次更新后,双缓存树已经建立。会利用之前右边的4fiberNode tree,进行下一轮渲染。

    总结

    此节我们主要是讲了hook是如何存放数据的,以及mount阶段和update阶段不同的存放,也讲解了通过dispatch调用后,react是如何更新的。以及双缓存树在第一次更新后是如何建立的。

    以上就是React18从0实现dispatch update流程的详细内容,更多关于React18 dispatch update流程的资料请关注其它相关文章!

    声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。