[React] 解析 React 框架底层源码(上)

2023 年 2 月 28 日 星期二(已编辑)
12

[React] 解析 React 框架底层源码(上)

温馨提示:在写这篇文章的时候其实是图文并茂的,但由于图片都保存在第三方平台的图床中(notion, juejin),搬运到博客也较为麻烦一些,所以博文中就没有图片,如果对图片感兴趣的小伙伴,可以看我的掘金文章,那里有图文并茂的源码解释

链接: https://juejin.cn/column/7149818417325801503

目录

1. Visual Dom | JSX | Fiber

2. React 初次并发渲染

3. React Update

4. React Diff

5. useState | useReducer

Visual Dom | JSX | Fiber

JSX

JSXECMAScript 的语法拓展,我们可以用 JSX 语法 来更加灵活,自如,声明式的表达 React UI组件JSX最终都会被编译器转译成为 ECMAScript

相信这也是大家喜欢 React 的原因。

旧转换

React.createElement("h1", { id: "title", }, "hello");

新转换 javascript import { jsx as _jsx } from "react/jsx-runtime"; _jsx("h1", { id: "title", children: "hello" }, "title");

Visual Dom

Visual Dom 是一个普通的 JavaScript 对象。Visual Dom 出现主要是用来解决频繁操作真实 DOM, 从而在写法上,性能上,抽象表现上带来的一系列问题。

有了 Visual Dom 之后,有以下几个优点:

  1. 跨平台 -- Visual Dom 是对 UI 及其交互 的一层抽象描述,不像 DOM 一样和平台强相关。所以可以让这层抽象描述可以用在 Android、iOS、VR

  2. 增量更新 -- 可以实现精准的定量更新,通过 Diff 算法,尽可能的复用节点,从而尽可能少的操作/改变真实 Dom

  3. 处理兼容性 -- 处理浏览器的版本对 API 的兼容/polyfill 和支持。
  4. 安全性 -- Visual Dom 内容经过 XSS 处理,可以防范 XSS 攻击。
  5. 轻量级 -- 虚拟 DOM 属性,继承的层级 都比 原生少很多。

Visual Dom 缺点 :

  • 虚拟 DOM 的创建和消费需要消耗额外的内存。
  • 虚拟 DOM 首次渲染其实并不一定会更快。

Fiber

为什么会出现 Fiber

Fiber 之前的 React 的工作架构,就是一气呵成,从根节点出发,遍历整个应用程序,独占浏览器的线程资源。随着前端应用程序的规模不断扩大,之前的工作架构遇到了性能问题,或者说是性能瓶颈。性能瓶颈产生的原因是:

主流屏幕的刷新率为 60HZ (1s 刷新 60次), 也就是浏览器每刷新一帧,花费时间为 16.8ms, 为了保证浏览器的每一帧都能够流畅刷新,那么必须保证浏览器 1 帧的时间不能超过 16.8ms, 超过了这个时间,浏览器不能流畅的刷新下一帧,浏览器就会卡帧,丢帧,用户会感觉到明显卡顿和响应丢失。

所以对于之前的 React 工作架构来说,执行 React 代码时,会一直独占浏览器的线程资源,会导致 浏览器 1 帧的时间超过 16.8ms, 导致卡帧,掉帧,丢顿, 并且 Javascript 引擎和页面渲染引擎在同一个渲染线程, GUI 渲染和 Javascript 执行两者是互斥的。 如果某个任务执行时间过长,浏览器会推迟渲染。除此之外,浏览器线程资源一直被 JavaScript 独占,用户的响应事件,动画会丢失,卡顿。

所以 Fiber 的出现,就是为了解决上述问题。

Fiber 是什么?

Fiber 本质就是一个普通的 JavaScript 对象,也是 React 的最小执行单元。Fiber架构就是由 一个个的 Fiber 对象组成的数据结构。

Fiber 能做什么?

通过Fiber架构/数据结构或者说 Fiber 的出现,React 的协调过程由一气呵成变成可被中断式调度。React 可以适时地让出浏览器线程资源,让浏览器及时地响应用户的交互,渲染 Dom,执行动画。每次执行完一个 Fiber 执行单元, React 就会检查浏览器的 1 帧 现在还剩多少时间,如果没有时间就将控制权让出去。

Fiber 的缺点?

最小执行单元是一个 Fiber,如果单个 Fiber 的计算太复杂,还是会大量占据浏览器一帧的时间。尽管有高优先级打断低优先级,解决饥饿问题等策略。但是硬件设备的瓶颈,软件层面只能是尽可能的优化,尽可能的缓解。

React 初次并发渲染

为什么叫并发渲染?

我们知道同步渲染是 React 早期版本的默认行为。在这种模式下,React 会按照组件的更新顺序,逐步执行整个渲染过程,直到渲染完成。所有的计算和更新会一次性执行,直到当前任务完全完成为止。

并发渲染是 React 16 及以后版本引入的一种新特性,旨在提升渲染过程的响应性和流畅度。在并发渲染模式下,React 会将渲染任务分成小块,通过时间分片(time slicing)逐步执行,避免一次性执行长时间任务,从而避免卡顿。

问题:下面这段 React 代码的初次渲染流程是怎样的?

let App = <h1>Hello World</h1>;
const root = createRoot(document.getElementById("root"));

root.render(<App />);

第一:调用 createRoot 函数, 传入 root 节点。创建 FiberRoot 节点。

export function createRoot(
  container: Element | Document | DocumentFragment,
  options?: CreateRootOptions
): RootType {
  const root = createContainer(
    container,
    ConcurrentRoot,
    null,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onRecoverableError,
    transitionCallbacks
  );

  return new ReactDOMRoot(root);
}

第二:调用 render 函数,在 updateContainer 函数当中 创建 RootFiber 节点。与 FiberRoot 建立如下图所示的关系。

ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render =
  function (children: ReactNodeList): void {
    const root = this._internalRoot;
    updateContainer(children, root, null, null);
  };

第三:在 updateContainer 函数中,创建 根节点的 update 对象。调用 enqueueUpdate函数入队。

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function
): Lane {
  const update = createUpdate(eventTime, lane);
  // 根节点的 update 对象 element 就是 App函数的虚拟 Dom
  update.payload = { element };
  const root = enqueueUpdate(current, update, lane);

  if (root !== null) {
    scheduleUpdateOnFiber(root, current, lane, eventTime);
    entangleTransitions(root, current, lane);
  }

  return lane;
}

第四:然后再 updateContainer 函数中调用 scheduleUpdateOnFiber -> ensureRootIsScheduled函数

export function scheduleUpdateOnFiber(
  root: FiberRoot,
  fiber: Fiber,
  lane: Lane,
  eventTime: number
) {
  ensureRootIsScheduled(root, eventTime);
}

第五:ensureRootIsScheduled函数,以一个 schedulerPriorityLevel (初次渲染,优先级是 16)优先级开始调度执行 performConcurrentWorkOnRootScheduler 返回一个 newCallbackNode 赋值给 根 Fiber 的 callbackNode属性。 这是可以并发的关键。

schedulerPriorityLevel 是根据 lanesToEventPriority(nextLanes) 函数,将 React lane 模型中的优先级转换成了 React 事件优先级, 最后再转换成 React Scheduler 优先级。为什么要转换 ? 因为 React Scheduler 只有 5个优先级,但是 React lane 模型 有 31 个 优先级,要想用这 5 个 Scheduler 优先级调度 这 31 个 lane 优先级的任务, 就应该先将 31 lane 合并成 5 个 React 事件优先级, 最后转换成 5 个 React Scheduler 优先级

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  newCallbackNode = scheduleCallback(
    schedulerPriorityLevel,
    performConcurrentWorkOnRoot.bind(null, root)
  );
  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

第六:在浏览器一帧的空闲时间中(5ms),performConcurrentWorkOnRoot 函数被调度执行。这里先会判断是否有条件去进行 时间切片。如果应该时间切片则并发渲染,那么调用 renderRootConcurrent 函数, 否则,同步渲染调用 renderRootSync 函数。

初次渲染,走时间切片并发渲染。 renderRootConcurrent 函数。因为初次渲染优先级是 16,不是同步优先级,时间片没有过期,不包括阻塞线程任务。

function performConcurrentWorkOnRoot(root, didTimeout) {
  const shouldTimeSlice =
    !includesBlockingLane(root, lanes) &&
    !includesExpiredLane(root, lanes) &&
    (disableSchedulerTimeoutInWorkLoop || !didTimeout);
  let exitStatus = shouldTimeSlice
    ? renderRootConcurrent(root, lanes)
    : renderRootSync(root, lanes);

  ensureRootIsScheduled(root, now());

  return null;
}

第七: renderRootConcurrent 函数中去 调用 workLoopSync 函数。while 循环中 调用 performUnitOfWork 函数。

function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    // $FlowFixMe[incompatible-call] found when upgrading Flow
    performUnitOfWork(workInProgress);
  }
}

function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
  workLoopConcurrent();
}

第八:performUnitOfWork 函数 当中就是我们熟悉的 beginWork completeWork

function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate;
  setCurrentDebugFiberInDEV(unitOfWork);
  let next;
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork);
    next = beginWork(current, unitOfWork, renderLanes);
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    next = beginWork(current, unitOfWork, renderLanes);
  }

  resetCurrentDebugFiberInDEV();
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }

  ReactCurrentOwner.current = null;
}

第九:进入 beginWork 初次挂载阶段, 根据不同的标签去对应挂载不同的 mount 逻辑。

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {
  switch (workInProgress.tag) {
    case IndeterminateComponent: {
      return mountIndeterminateComponent(
        current,
        workInProgress,
        workInProgress.type,
        renderLanes
      );
    }
    case FunctionComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes
      );
    }
  }
}

第十:在不同的挂载逻辑当中执行 reconcilChildren 构建 子 fiber。初次渲染走的是,mountChildFibers函数,任务就是构建子fiber树。

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes
) {
  if (current === null) {
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes
    );
  } else {
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes
    );
  }
}

第十一:当前工作单元的 fiberreconciler 完毕, 假设 5ms 的时间片到期 退出 while 循环。 退出 workLoopConcurrent 函数。

  newCallbackNode = scheduleCallback(
    schedulerPriorityLevel,
    performConcurrentWorkOnRoot.bind(null, root),
  );
}

root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;

第十二: 返回到 performConcurrentWorkOnRoot 函数中,由于 reconciler 过程还未完成,root.callbackNode === originalCallbackNode 成立, 给 Schduler 返回 performConcurrentWorkOnRoot 函数, Schduler 判断是函数之后,不会将该任务弹出优先级队列 ,而是继续下一帧时间继续执行 performConcurrentWorkOnRoot,直至 构建完整棵 fiber树,返回 null, Schduler 判断是 null ,之后将该任务弹出优先级队列。

if (root.callbackNode === originalCallbackNode) {
  if (
    workInProgressSuspendedReason === SuspendedOnData &&
    workInProgressRoot === root
  ) {
    root.callbackPriority = NoLane;
    root.callbackNode = null;
    return null;
  }
  return performConcurrentWorkOnRoot.bind(null, root);
}
  
return null;

第十三:fiber 节点的 beginwork 结束后会走 completeUnitOfWork -> completeWork 函数

function performUnitOfWork(unitOfWork: Fiber): void {
  next = beginWork(current, unitOfWork, renderLanes);
  if (next === null) {
    completeUnitOfWork(unitOfWork);
  }

  ReactCurrentOwner.current = null;
}

completeWork 函数中 还是判断不同的标签,进行 不同的 complete 逻辑, 我们这里用原生标签来举例。 对于 HostComponent 来说,complete 时 会调用 updateHostComponent 函数

React 18.2 之后, 大家在 completeWork 阶段 熟悉的 effectsLists 都被删掉了,都改成了 bubbleProperties 函数。这个我们放到更新的文章里面去说。

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {

    case HostComponent: {
    popHostContext(workInProgress);
    const type = workInProgress.type;
    if (current !== null && workInProgress.stateNode != null) {
      updateHostComponent(current, workInProgress, type, newProps);

      if (current.ref !== workInProgress.ref) {
        markRef(workInProgress);
      }
    } else {
      if (!newProps) {
        // This can happen when we abort work.
        bubbleProperties(workInProgress);
        return null;
      }
    }
  }
}

第十四: updateHostComponent 函数当中挂载 props , 并且通过 appendAllChildren 函数 将所有子节点 DOM 全部插入到自己的 DOM 当中。这样也就意味着,进行最后的提交阶段时,只需要将 App 函数组件的第一个 child 插入到 div#root 容器中就可以完成挂载。因为其余节点在 completeWork 函数中就已经插入到了父 DOM 当中去了。

updateHostComponent = function (
  current: Fiber,
  workInProgress: Fiber,
  type: Type,
  newProps: Props
) {
  appendAllChildren(newInstance, workInProgress, false, false);
};

第十四:在 commit 时, 判断子树上有副作用 或者 根节点上有副作用,就可以提交,否则不提交。 初次渲染只有 根节点的第一个 child 有副作用,其余没有副作用 ,可以提交。 在 mutation 阶段 调用 commitMutationEffects 函数。

 if (subtreeHasEffects || rootHasEffect) {
   commitMutationEffects(root, finishedWork, lanes);
}

为什么其余子节点没有副作用 而根节点的第一个 child有副作用 ? 因为 reconcileChildren 时,其余节点走到是 mountChildFibers 函数,而根节点的第一个 child 走的是 reconcileChildFibers 函数。

为什么根节点的第一个 child 走的是 reconcileChildFibers 函数? 这是因为挂载的时候,rootFiber 已经有 current 属性了。所以 current !== null, 走 reconcileChildFibers 函数,并在根节点的第一个 child 节点上 追加副作用:|= PLACEMENT

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes
) {
  if (current === null) {
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes
    );
  } else {
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes
    );
  }
}

commitMutationEffects 函数中,通过 recursivelyTraverseMutationEffects函数,递归提交子节点的作用, commitReconciliationEffects函数递归提交自己的副作用。 将有副作用的的节点,根据副作用标签进行增删改查。比如本例当中:从根节点开始递归提交,递归到 根节点的第一个 child <h1></h1>时, 有副作用 PLACEMENT 则调用 commitPlacement(finishedWork)函数 通过 appendChild() 插入到根节点当中,完成挂载。

function commitMutationEffectsOnFiber(
  finishedWork: Fiber,
  root: FiberRoot,
  lanes: Lanes
) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;
  switch (finishedWork.tag) {
    // eslint-disable-next-line-no-fallthrough
    case HostComponent: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork)
}

如果是根节点的第一个 child 是一个 App 函数/类/Memo组件,则又是不同的提交逻辑……

自此完成挂载

React Update

学习源码不易,和大家一起共勉!

前文: [[React Origin Code] 2022年来聊聊React Mount-- renderRootSync](https://juejin.cn/post/7152109746571444232)当中,我们刚刚说完 React Mount,今天我们继续来聊聊React update,在聊update之前,我们还需要再次回到mount阶段,这次主要把握和关注两个概念,workInProgress树current树的双缓冲Fiber机制。这在React update阶段起到了至关重要的作用。

关于双缓存树的资料

双缓存

React 双缓存Fiber树

下面我们以React 计数器案例来聊聊React update逻辑

createWorkInProgress

下面这段总结读完本文,可以回过头来看看:

createWorkInProgress函数mount的时候只会走一次,就是创建workInProgress树的根节点,mount的其他节点不进到createWorkInProgress函数,因为此时没有currentfiber树,其他节点的current === null,当执行到reconcileChildren函数的时候,走的是mountChildFibers,只有更新的时候,current !== null ,走reconcileChildrenFibers的时候,才让每个子节点先去递归执行createWorkProgress,复用建立workInProgressfiber节点,建立好workInProgress树current树,并且用alternate属性,对两棵树的节点进行联系。

beginwork前的fiber树

关于mount阶段, beginwork函数的作用和流程,我们在前文已经详细聊过了, 在bedginwork之前,会调用createWorkInProgress函数

mount的时候,在刚进入createWorkInProgreess的函数上打上断点,打印fiber树。结果是这样的。

mount的时候,执行的第一次也是最后一次的createWorkInProgreess函数结果是这样的

然后从workProgreess树的根fiber节点开始 mount,关于mount的流程这篇文章很详细,mount之后结果是这样的。

细节注意:下一次更新之前,刚刚 mount时的workInProgress树会被变成current树,这里的变,也仅仅时需要改变FiberRootNode.current指针而已。这也是双缓存机制的魅力所在。

第一次触发计数器 + 1 -> update

mount结束后,我们触发计数器+1, 开始第一次更新,我们由上图可以知道,mount之后,第一次更新之前只有根节点有自己的alternate,所以在createWorkInProgress函数时,根节点fiber可以复用current树的 fiber,由于复用current fiber之后其child也被复用,所以结果是这样的。

我们发现第一次更新,除了根节点FiberRootNode之外每次先递归来到createWorkInProgress来执行,curretn.alternatte都是null,所以每次都回去createFiber去创建新的workProgress节点,第一次更新之后就会在每次createWorkInProgress的时候,当前workInprogressfiber就会和 current fiber建立alternative相互链接。然后去递归beginwork,reconcileChildren,然后再让children fiber去作为根节点去createWorkInProgress。begin这里有一个更新逻辑,每次我们来到带着cr创建好的workj fiber节点和current的时候,会有一个优化,didReceiveUpdate 如果为true会走优化逻辑。通过新旧props 对比,新旧type对比,以及是否有context的改变来对比,bailoutOnAlreadyFinishWork逻辑,会执行cloneChildFibers 返回子fiber。如果没有命中,继续根据type来updatae不同类型的标签组件。

update阶段 beginwork 和 createWorkInProgress函数调用栈的关系 reconcile也进入了不一样的逻辑,这一次current fiber节点不在是null了,会进入reconcileChildrenFibers而不是 mountChildrenFibers这时shouldTraceEffect 会变为true给一些需要新插入的节点打上effectTag Placement, 或者是删除节点打上 delection

beginwork后创建建立双缓存Fiber树的结果是这样的:

completework老朋友了,workInProgress树回溯阶段,从下至上,对props 进行diff来 删除或者来收集变化属性到fiber节点的updateQueue当中,第i属性为变化的属性,i+ 1属性是属性变化的值,将需要更新的节点打上effectTag, update,收集到effectList的 链表当中,complete 回溯到根节点的时候,从根节点出发的 firstEffect属性 就指向 第一个需要comit 变化的fiber,next下一个,一直到lastEffect最后,最后commitWork 根节点。commit阶段更新渲染视图.这样做的好处是不用再像renderer阶段一样深度优先遍历,都从根节点出发去遍历到每一个子节点去看看fiber节点是否发生变化,来打上相对应的effecttag。

第二次触发计数器 + 1 -> update

第二次更新很关键,因为这时内存当中有两颗react fiber树 一棵树是w,一棵树是 c,而且之前建立了alternate联系,这一次建立新fiber的时候 除了新添加的节点,其他节点都在第一次更新的基础上增加了altenate属性,所以这一次每次不用在重新创建,workINprogrss fiber节点了,我们复用wok 的 al c来 属性来获取fiber节点。这一次和以往不同的地方还有,

React Diff

希望可以帮助到,与我同样纠结在此的前端程序员们。

大家都知道,React真正的Diff逻辑是在reconcileChildFibers,这没有任何问题,那么图片当中的两行代码是什么?与Diff有什么关系,事实上 reconcileChildFibers 被当作闭包函数返回了出来,被React用于两个方面,第一个方面是初次挂载渲染,第二个方面是更新Diff渲染。那我们接下来就从Reconcile阶段初次渲染更新Diff两个方面来说React Diff

初次渲染

我们先对初次渲染做总结,之后看代码会很有感觉:React的初次渲染在reconcile阶段,主要的任务是从根节点FiberRoot开始深度优先遍历多叉树结构的vdom,映射构建单层fiber链表结构和整个fiber多叉树结构(及构建fiber对象的属性,包括但不限于index, key, type,internate, flags……),并且构建fiber结构的child(第一个孩子), return(父亲), sibling(兄弟)关系。

下面是reconcile构建的过程源码

这里我们只是简单聊聊reconcile阶段的初次渲染,暂时不涉及到React的schedulercommitWork阶段。有人可能会问,文章到了这里没出现Diff的过程,那么初次渲染和Diff有什么关系呢?其实答案很简单,大家也都知道,有了初次的渲染,我们下次更新才有Diff的对象呀! 接下来我们就来和大家聊聊更新阶段的Diff

更新Diff渲染

我们先对更新Diff渲染做总结,之后看代码会很有感觉:React的更新Diff渲染在reconcile阶段,主要的任务是将不同类别(type)旧的fiber节点和新的vdom结构进行Diff,可以分为:对普通文本,数字节点进行reconcile,对单节点进行reconcile,对多节点进行reconcile

普通string, number节点的 reconcile

  • 进入这个函数,已经确定newChildren是文本节点了。

    • 这里判断oldFiber是不是文本节点。

    • 如果是文本节点,可以复用节点,就使用useFiber来复用创建新的文本fiber节点。

    • 如果不是文本节点,不可以复用节点,删除当前层级的oldFiber链表,只创建一个新的文本fiber节点。

    单节点的 reconcile

  • 进入这个函数,已经确定newChildren是单节点了。

    • 这个时候判断oldFiberkeytype,看看可不可以复用。

    • 可以复用,删除oldFiber链表剩余的链表节点。

    • 不可以复用,删除oldFiber链表的全部节点,创建新的fiber节点。

多节点的 reconcile

多节点是Diff的精髓,我们还是先总结,等一下看代码会非常有感觉,多节点的diff的主要流程是:三次循环遍历,一次判断,完成oldFiber链表和新的vdom结构的diff,从而完成oldfiber链表节点的删除和移动和newfiber节点的增加,以构造新的fiber结构

第一次循环遍历

  • 如果第一次循环遍历的顺利的话,大家看for循环的退出条件,也就是newChldren 都可以复用oldFIber链表节点或者是oldFiber有的链表节点,newChldren都可以复用。那么newChldren就可以顺利建立起child, sibling的联系,所以在顺利的情况下,第一次循环退出的条件要不就是newChldren的长度太长了,要不就是oldFiber的链表长度太长了。

    • 如果是oldFiber的链表长度太长了,经过第一次判断,会delete掉多余的oldFiber链表节点。
    • 如果是newChldren的长度太长了,在经过第二次循环遍历会构建没有遍历到的newChldren链表节点。
    • 自此结束,如果顺利的话不会进入第三次循环
  • 如果第一次遍历循环不顺利的话,即只要发现一个不能复用的节点,就立马退出第一次循环。从不能复用的节点开始,将所有的oldfiber的链表节点,依次加入到map结构当中。

    • 然后进入第三次循环,继续遍历newChldren链表,遍历到一个newChldren, jsx节点,就拿着key 去由oldFiber节点构成的map当中去找 可以复用的key

      • 没有找到,构建新的fiber节点,flagPLACEMENT
      • 如果newChldrenmap当中找到对应keyfiber了,说明可以复用oldfiber节点。
      • 复用分为两种:一种是移动复用,一种是更新复用。
      • 如果是更新复用:那么就updateElement(oldChildrenVdom, newChildrenVdom)复用构建fiber节点,然后在map当中删除复用的oldFiber
      • 如果是移动复用:那么就利用placedIndex索引来移动节点,flag is MOVE。这里我当时最大的疑问就是怎么样通过placedIndex来移动的。源码当中的 index < placedIndex 为什么会被标注为移动? 原因很简单就是两个节点的相对位置发送了变化,比如原来是 a -> d 现在在是 d -> a, 我们从现在的 d -> a开始出发遍历,遍历到d,发现在原来的1位置,此时placeIndex更新为1,当遍历到a的时候,我们发现a在原来的0位置,如果相对位置没有发送变化,这里a的位置应该在b的后面。大于1 (placeIdex),但是现在是 < 1意味着a到了b前面,所以相对位置发送了变化,标记为移动。然后在map当中删除复用的oldFiber。之后删除map当中剩余的fiber节点。
      • 并循环移动和更新来构建出这一层新的fiber结构。

一次判断

  • 和上文第一次判断那里说的一样,当newFiber链表已经全部顺利遍历完成,oldFiber链表还有节点的时候,删除剩下的oldFiber节点。 #### 第二次循环遍历
  • 第二次循环遍历的两个作用,我们在文章当中已经全部展示了。

    • 作用1:文章开头说的初次渲染。

    • 作用2:第一次循环遍历顺利的时候,当oldFiber链表节点已经遍历完成后,newFiber还有的时候,继续遍历newFiber来创建fiber,建立联系。

      第三次循环遍历

  • 上文从代码的角度聊了节点的移动策略,下面我们以dom的角度来表达移动diff的过程。 >旧节点 a -> b -> c -> d -> e -> f (key为自己) > > 新节点 a -> c -> b -> e -> g (key为自己) > > --- > 遍历新节点到c发现,b不能符合立马跳出第一次循环,将剩下的旧节点放到了map当中(key为item)。 > {"b": b, "c": c, "d": d, "e": e, "f": f}, 遍历新节点c去map当中去找,找到了 placeIndex =2, 更新节节点不移动,删除map当中的c,遍历到b找到了,index < placeIndx -> 1 < 2 ,所以标记b为移动节点,同理e更新,g在map当中找不到,标记为增加。 > > --- > > 遍历完之后要去更新dom了,这里要注意,先删除真实dom节点中要移动和删除的节点,删除之后真实dom变为 a -> c -> e,这时b移动的时候,索引为原来的mountIndex 为2,判断2上有没有元素,这里a -> c -> e 索引为2是e,所以执行parentDom.insertBefore() 将b插入到e之前,完成移动,执行到g新增的时候,索引为原来的mountIndex为4,判断4上有没有元素,这里a -> c -> b -> e,索引为4没有执行parentDom.appendChild(), 完成新增,最终结果 a -> c -> b -> e -> g,移动,更新完毕。

一点总结

我的两点理解,希望可以帮助到与我同样纠结在此的前端程序员们

  • diff过程是一层一层的oldFibernewFiber去diff。
  • 移动不是dom节点的平移,本质上还是复用,只不过不用重新document.createElement(),利用fiber结构上的引用stateNode去将真实DOM,去insertBefore或者appendChild来完成的移动。

useState | useReducer

代码都来自 React 18 源码, 大家可以放心食用

附加面试题:

  • 为什么不能在条件和循环里使用Hooks?
  • 为什么不能在函数组件外部使用Hooks?
  • React Hooks的状态保存在了哪里?

useReducer 原理

问题: 下面这段代码,从挂载到更新发生了什么?怎么挂载的?怎么更新的。

const reducer = (state, action) => {
  if (action.type === "add") return state + 1;
  else return state;
};

function Counter() {
  const [number, dispatch0] = useReducer(reducer, 0);
  const [number1, dispatch1] = useReducer(reducer, 0);
  return (
    <div
      onClick={() => {
        dispatch({ type: "add" });
        dispatch1({ type: "add" });
      }}
    >
      {number}
    </div>
  );
}

> 这里,我们直接进入到 reconciler 阶段,默认已经通过深度优先调度到了 Counter 函数组件的 Fiber节点

useReducer mount 挂载阶段

第一:判断是函数节点的 tag 之后,调用 renderWithHooks.

/* 
workInProgress: 当前工作的 Fiber 节点
Componet:Counter 函数组件
_current: 老 Fiber 节点 也就是 workInProgress.alternate 
*/
let value = renderWithHooks(_current,workInProgress,Component);

第二:在 renderWithHooks 当中调用 Counter 函数

let children = Component();

第三: 调用 Counter 函数 的 useReducer 函数

export function useReducer(reducer, initialArg) {
  return ReactCurrentDispatcher.current.useReducer(reducer, initialArg);
}

第四:挂载阶段 ReactCurrentDispatcher.current.useReducer 实则是调用了 mountReducer,

第五:在 mountReducer 中调用,mountWorkInProgressHook 函数,创建 useReducerHook 对象,构建 fiber.memoizedState 也就是 Hook 链表, 然后将 dispatchAction bind 绑定之后传入 queuefiber 为参数,这点很重要。并且初始化 Hook 的更新对象的队列 queue。

function mountReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = mountWorkInProgressHook();
  let initialState;
  if (init !== undefined) {
    initialState = init(initialArg);
  } else {
    initialState = ((initialArg: any): S);
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue: UpdateQueue<S, A> = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: (initialState: any),
  };
  hook.queue = queue;
  const dispatch: Dispatch<A> = (queue.dispatch = (dispatchReducerAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

如果之后再有 useState useReducer,最终mout阶段的成果是

自此 useReducer 挂载阶段执行完毕


useReducer update 更新阶段

第一:通过 dispatch(action) 触发更新,dispatch 就是 dispatchAction.bind(null, fiber, queue) 返回的绑定函数。所以相当于调用了 dispatchAction(fiber, queue, action)

第二: 更新时 dispatchAction 调用 scheduleUpdateOnFiber , enqueueConcurrentHookUpdate

function dispatchReducerAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
): void {
  const lane = requestUpdateLane(fiber);
  const update: Update<S, A> = {
    lane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: (null: any),
  };

  if (isRenderPhaseUpdate(fiber)) {
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
    if (root !== null) {
      const eventTime = requestEventTime();
      scheduleUpdateOnFiber(root, fiber, lane, eventTime);
      entangleTransitionUpdate(root, queue, lane);
    }
  }
}

enqueueConcurrentHookUpdate 函数 中的 enqueueUpdatehook 更新时产生的对象 update,放入 queue.pending 当中,例如 一个 reducer 的 多次 dispatch, update 会组成队列。

export function enqueueConcurrentHookUpdate<S, A>(
  fiber: Fiber,
  queue: HookQueue<S, A>,
  update: HookUpdate<S, A>,
  lane: Lane,
): FiberRoot | null {
  const concurrentQueue: ConcurrentQueue = (queue: any);
  const concurrentUpdate: ConcurrentUpdate = (update: any);
  enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
  return getRootForUpdatedFiber(fiber);
}
}
`scheduleUpdateOnFiber` 函数,从根节点出发,重新开始调度更新。 第三:我们直接进入到 `reconciler` 阶段,默认已经通过深度优先更新调度到了 `Counter` 函数组件的 `Fiber`节点 第四:判断是函数节点的 tag 之后,调用 renderWithHooks.
/* 
workInProgress: 当前工作的 Fiber 节点
Componet:Counter 函数组件
_current: 老 Fiber 节点 也就是 workInProgress.alternate 
*/
let value = renderWithHooks(_current,workInProgress,Component);

第五:在 renderWithHooks 当中调用 Counter 函数

let children = Component();

第六: 调用 Counter 函数 的 useReducer 函数

export function useReducer(reducer, initialArg) {
  return ReactCurrentDispatcher.current.useReducer(reducer, initialArg);
}

第七:更新阶段 ReactCurrentDispatcher.current.useReducer 实则是调用了 updateReducer

第八:在 updateReducer 中调用 updateWorkInProgressHook 函数,在此函数中最重要的就是通过 alternate 指针复用 currentFiber(老 Fiber) 的 memorizedState, 也就是 Hook 链表,并且按照严格的对应顺序来复用 currentFiber(老 Fiber) Hook 链表当中的 Hook(通过 currentHook 指针结合链表来实现),一个比较重要的复用就是去复用老 Hook 的更新队列 queue,因为 dispatchAction.bind 绑定的就是 currentFiber(老 Fiber), 通过尽可能的复用来创建新的 Hook 对象,构建 fiber.memoizedState 也就是新的 Hook 链表。

注意:这里也就是为什么不能再循环和判断当中使用 Hook 的重要原因。 一句话:要保持严格的顺序一致。

读到这儿我们发现,无论是 Mout Hook 还是 updateHook 都有严格的顺序,如果顺序乱了,更新阶段就不会正确复用到在currentFiber Hook 链表当中的 Hook 的更新队列 queue,也就不能通过更新得到正确的 state. 再严重些,useState的更新逻辑,对应的 currentHook 是 useEffect Hook, 无法兼容复用,导致报错。

function updateWorkInProgressHook(): Hook {
    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null,
    };
 
  return workInProgressHook;
}

第九:在 updateReducer 中调用,遍历整个更新队列 queue.pending,取出 update 对象的 action 通过 newState = reducer(newState, action); 返回 新状态 和 queue.dispatch(还是 dispatchAction.bind(null, Currentfiber, queue)

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  queue.lastRenderedReducer = reducer;
  const current: Hook = (currentHook: any);
  let baseQueue = current.baseQueue;
  // The last pending update that hasn't been processed yet.
  const pendingQueue = queue.pending;

  if (baseQueue !== null) {
    // We have a queue to process.
    const first = baseQueue.next;
    let newState = current.baseState;

    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast = null;
    let update = first;
    do {
        // Process this update.
        const action = update.action;
        if (update.hasEagerState) {
          // If this update is a state update (not a reducer) and was processed eagerly,
          // we can use the eagerly computed state
          newState = ((update.eagerState: any): S);
        } else {
          newState = reducer(newState, action);
        }
      }
      update = update.next;
    } while (update !== null && update !== first);

    if (newBaseQueueLast === null) {
      newBaseState = newState;
    } else {
      newBaseQueueLast.next = (newBaseQueueFirst: any);
    }

    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;
    queue.lastRenderedState = newState;
  }

自此 useReducer 更新完毕


useState 原理

问题: 下面这段代码,从挂载到更新发生了什么?怎么挂载的?怎么更新的。

function Counter() {
  const [number, setNumber] = useState(reducer, 0);
  return (
    <div
      onClick={() => {
        setNumber({ type: "add" });
      }}
    >
      {number}
    </div>
  );
}

> 这里,我们直接进入到 reconciler 阶段,默认已经通过深度优先调度到了 Counter 函数组件的 Fiber节点

useState mount 挂载阶段

let children = Component();

第三: 调用 Counter 函数的第一个 useState 函数

export function useEffect(create, deps) {
  return ReactCurrentDispatcher.current.useState(create, deps);
}

第四:挂载阶段 ReactCurrentDispatcher.current.useState 实则是调用了 moutState, moutState 中调用,mountWorkInProgressHook 函数,创建 useStateHook 对象,构建 fiber.memoizedState 也就是 Hook 链表, 然后将 dispatchAction bind 绑定之后传入 queuefiber 为参数,这点很重要。并且初始化 Hook 的更新对象的队列 queue。

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  };
  hook.queue = queue;
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

如果之后再有 useState useReducer,最终mount阶段的成果如下图:

自此 useState 挂载阶段执行完毕


useState update 更新阶段

第一:通过 dispatch(action) 触发更新,dispatch 就是 dispatchAction.bind(null, fiber, queue) 返回的绑定函数。所以相当于调用了 dispatchAction(fiber, queue, action)

第二: 更新时 dispatchAction 调用 scheduleUpdateOnFiber , enqueueConcurrentHookUpdate

function dispatchReducerAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
): void {
  const lane = requestUpdateLane(fiber);
  const update: Update<S, A> = {
    lane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: (null: any),
  };

  if (isRenderPhaseUpdate(fiber)) {
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
    if (root !== null) {
      const eventTime = requestEventTime();
      scheduleUpdateOnFiber(root, fiber, lane, eventTime);
      entangleTransitionUpdate(root, queue, lane);
    }
  }
}

enqueueConcurrentHookUpdate 函数 中的 enqueueUpdatehook 更新时产生的对象 update,放入 queue.pending 当中,例如 一个 useState 的多次 setStae, update 会组成队列。

export function enqueueConcurrentHookUpdate<S, A>(
  fiber: Fiber,
  queue: HookQueue<S, A>,
  update: HookUpdate<S, A>,
  lane: Lane,
): FiberRoot | null {
  const concurrentQueue: ConcurrentQueue = (queue: any);
  const concurrentUpdate: ConcurrentUpdate = (update: any);
  enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
  return getRootForUpdatedFiber(fiber);
}
}
`scheduleUpdateOnFiber` 函数,从根节点出发,重新开始调度更新。 第三:我们直接进入到 `reconciler` 阶段,默认已经通过深度优先更新调度到了 `Counter` 函数组件的 `Fiber`节点 第四:判断是函数节点的 tag 之后,调用 renderWithHooks.
/* 
workInProgress: 当前工作的 Fiber 节点
Componet:Counter 函数组件
_current: 老 Fiber 节点 也就是 workInProgress.alternate 
*/
let value = renderWithHooks(_current,workInProgress,Component);

第五:在 renderWithHooks 当中调用 Counter 函数

let children = Component();

第六: 调用 Counter 函数 的 useState 函数

export function useState(reducer, initialArg) {
  return ReactCurrentDispatcher.current.useState(reducer, initialArg);
}

第七:更新阶段 ReactCurrentDispatcher.current.useState 实则是调用了 updateReducer

useState 更新的时候,调用的还是 updateReducer, 说明 useState 本质就是 useReducer. 也是 useReducer 的语法糖。 将 basicStateRecuer 作为 reducer 函数。


function updateState<S>() {
  return updateReducer(basicStateReducer, (initialState: any));
}

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

第八:在 updateReducer 中调用 updateWorkInProgressHook 函数,在此函数中最重要的就是通过 alternate 指针复用 currentFiber(老 Fiber) 的 memorizedState, 也就是 Hook 链表,并且按照严格的对应顺序来复用 currentFiber(老 Fiber) Hook 链表当中的 Hook(通过 currentHook 指针结合链表来实现),一个比较重要的复用就是复用老 Hook 的更新队列 queue,因为 dispatchAction.bind 绑定的就是 currentFiber(老 Fiber), 通过尽可能的复用来创建新的 Hook 对象,构建 fiber.memoizedState 也就是 新的 Hook 链表。

function updateWorkInProgressHook(): Hook {
    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null,
    };
 
  return workInProgressHook;
}

第九:在 updateReducer 中调用,遍历整个更新队列 queue.pending,取出 update 对象的 action 通过 newState = reducer(newState, action); 返回 新状态 和 queue.dispatch(还是 dispatchAction.bind(null, Currentfiber, queue)

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  queue.lastRenderedReducer = reducer;
  const current: Hook = (currentHook: any);
  let baseQueue = current.baseQueue;
  // The last pending update that hasn't been processed yet.
  const pendingQueue = queue.pending;

  if (baseQueue !== null) {
    // We have a queue to process.
    const first = baseQueue.next;
    let newState = current.baseState;

    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast = null;
    let update = first;
    do {
        // Process this update.
        const action = update.action;
        if (update.hasEagerState) {
          // If this update is a state update (not a reducer) and was processed eagerly,
          // we can use the eagerly computed state
          newState = ((update.eagerState: any): S);
        } else {
          newState = reducer(newState, action);
        }
      }
      update = update.next;
    } while (update !== null && update !== first);

    if (newBaseQueueLast === null) {
      newBaseState = newState;
    } else {
      newBaseQueueLast.next = (newBaseQueueFirst: any);
    }

    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;
    queue.lastRenderedState = newState;
  }

自此 useState 更新完毕

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...