好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

React18从0实现dispatch update流程

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

仓库地址

具体章节代码3个commit

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

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

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

function App() {
  const [num, setNum] = useState(100);
  window.setNum = setNum;
  return &lt;div&gt;{num}&lt;/div&gt;;
}
复制代码

hooks原理

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

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

首先有几个名词:

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

workInProgressHook : 当前正在执行的hook

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

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

hook的结构如下图:

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 fiberNode 的 memoizedState 指向第一个hook。此时就生成了如下图的结构:

处理hook数据

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

function mountState&lt;State&gt;(
  initialState: (() =&gt; State) | State
): [State, Dispatch&lt;State&gt;] {
  // 找到当前useState对应的hook数据
  const hook = mountWorkInProgressHook();

  let memoizedState;
  if (initialState instanceof Function) {
    memoizedState = initialState();
  } else {
    memoizedState = initialState;
  }

  // useState是可以触发更新的
  const queue = createUpdateQueue&lt;State&gt;();
  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&lt;State&gt;();
hook.updateQueue = queue;
    
const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue);
queue.dispatch = dispatch;
复制代码

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

function dispatchSetState&lt;State&gt;(
  fiber: FiberNode,
  updateQueue: UpdateQueue&lt;State&gt;,
  action: Action&lt;State&gt;
) {
  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结构如下图:

useState 触发更新(dispatch)

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

function dispatchSetState&lt;State&gt;(
  fiber: FiberNode,
  updateQueue: UpdateQueue&lt;State&gt;,
  action: Action&lt;State&gt;
) {
  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 进行链接。整体结构如下图:

还记得我们 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, 所以就会走到报错的逻辑。

useState 计算

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

在执行 setNum(action) 后,我们知道 action 存放在 queue.shared.pending 中 而 queue 是存放在对应 hook 的 updateQueue 中。所以我们可以拿到 action 第三步就是去消费 action ,即执行 processUpdateQueue , 传入上一次的 state , 以及我们这次接受的 action ,计算最新的值。

function updateState&lt;State&gt;(): [State, Dispatch&lt;State&gt;] {
  // 找到当前useState对应的hook数据
  const hook = updateWorkInProgressHook();

  // 计算新的state逻辑
  const queue = hook.updateQueue as UpdateQueue&lt;State&gt;;
  const pending = queue.shared.pending;

  if (pending !== null) {
    const { memoizedState } = processUpdateQueue(hook.memoizedState, pending);
    hook.memoizedState = memoizedState;
  }

  return [hook.memoizedState, queue.dispatch as Dispatch&lt;State&gt;];
}
复制代码

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

双缓存树

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

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

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

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

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

总结

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

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

原文地址:https://juejin.cn/post/7187976209844666426

查看更多关于React18从0实现dispatch update流程的详细内容...

  阅读:33次