我的编程空间,编程开发者的网络收藏夹
学习永远不晚

React18之update流程从零实现详解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

React18之update流程从零实现详解

引言

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

仓库地址

具体章节代码3个commit

本章我们主要讲解通过useState状态改变,引起的单节点update更新阶段的流程。

对比Mount阶段

对比我们之前讲解的mount阶段,update阶段也会经历大致的流程, 只是处理逻辑会有不同:

之前的章节我们主要讲了reconciler(调和) 阶段中mount阶段:

  • beginWork:向下调和创建fiberNode树,
  • completeWork:构建离屏DOM树以及打subtreeFlags标记。
  • commitWork:根据placement创建dom
  • useState: 对应调用mountState

这一节的update阶段如下:

begionWork阶段:

  • 处理ChildDeletion的删除的情况
  • 处理节点移动的情况 (abc -> bca)

completeWork阶段:

  • 基于HostText的内容更新标记更新flags
  • 基于HostComponent属性变化标记更新flags

commitWork阶段:

  • 基于ChildDeletion, 遍历被删除的子树
  • 基于Update, 更新文本内容

useState阶段:

  • 实现相对于mountStateupdateState

下面我们分别一一地实现单节点的update更新流程

beginWork流程

对于单一节点的向下调和流程,主要在childFibers文件中,分2种,一种是文本节点的处理reconcileSingleTextNode, 一种是标签节点的处理reconcileSingleElement

复用fiberNode

update阶段的话,主要有一点是要思考如何复用之前mount阶段已经创建的fiberNode

我们先以reconcileSingleElement为例子讲解。

当新的ReactElement的type 和 key都和之前的对应的fiberNode都一样的时候,才能够进行复用。我们先看看reconcileSingleElement是复用的逻辑。

function reconcileSingleElement(
  returnFiber: FiberNode,
  currentFiber: FiberNode | null,
  element: ReactElementType
) {
  const key = element.key;
  // update的情况 <单节点的处理 div -> p>
  if (currentFiber !== null) {
    // key相同
    if (currentFiber.key === key) {
      // 是react元素
      if (element.$$typeof === REACT_ELEMENT_TYPE) {
        // type相同
        if (currentFiber.type === element.type) {
          const existing = useFiber(currentFiber, element.props);
          existing.return = returnFiber;
          return existing;
        }
      }
    }
  }
}
  • 首先我们需要判断currentFiber是否存在,当存在的时候,说明是进入了update阶段。
  • 根据currentFiberelement的tag 和 type判断,如果相同才可以复用。
  • 通过双缓存树(useFiber)去复用fiberNode。

useFiber

复用的逻辑本质就是调用了useFiber, 本质上,它是通过双缓存书指针alternate,它接受已经渲染对应的fiberNode以及新的Props 巧妙的运用我们之前创建wip的逻辑,可以很好的复用fiberNode


function useFiber(fiber: FiberNode, pendingProps: Props): FiberNode {
  const clone = createWorkInProgress(fiber, pendingProps);
  clone.index = 0;
  clone.sibling = null;
  return clone;
}

对于reconcileSingleTextNode

删除旧的和新建fiberNode

当不能够复用fiberNode的时候,我们除了要像mount的时候新建fiberNode(已经有的逻辑),还需要删除旧的fiberNode

我们先以reconcileSingleElement为例子讲解。

beginWork阶段,我们只需要标记删除flags。以下2种情况我们需要额外的标记旧fiberNode删除

  • key不同
  • key相同,type不同
function deleteChild(returnFiber: FiberNode, childToDelete: FiberNode) {
  if (!shouldTrackEffects) {
    return;
  }
  const deletions = returnFiber.deletions;
  if (deletions === null) {
    // 当前父fiber还没有需要删除的子fiber
    returnFiber.deletions = [childToDelete];
    returnFiber.flags |= ChildDeletion;
  } else {
    deletions.push(childToDelete);
  }
}

我们将需要删除的节点,通过数组形式赋值到父节点deletions中,并标记ChildDeletion有节点需要删除。

对于reconcileSingleTextNode, 当渲染视图中是HostText就可以直接复用。整体代码如下:

function reconcileSingleTextNode(
  returnFiber: FiberNode,
  currentFiber: FiberNode | null,
  content: string | number
): FiberNode {
  // update
  if (currentFiber !== null) {
    // 类型没有变,可以复用
    if (currentFiber.tag === HostText) {
      const existing = useFiber(currentFiber, { content });
      existing.return = returnFiber;
      return existing;
    }
    // 删掉之前的 (之前的div, 现在是hostText)
    deleteChild(returnFiber, currentFiber);
  }
  const fiber = new FiberNode(HostText, { content }, null);
  fiber.return = returnFiber;
  return fiber;
}

completeWork流程

当在beginWork做好相应的删除和移动标记后,在completeWork主要是做更新的标记。

对于单一的节点来说,更新标记分为2种,

  • 第一种是文本元素的更新,主要是新旧文本内容的不一样。
  • 第二种是类似div的属性等更新。这个我们下一节进行讲解。

这里我们只对HostText中的类型进行讲解。

case HostText:
  if (current !== null && wip.stateNode) {
    //update
    const oldText = current.memoizedProps.content;
    const newText = newProps.content;
    if (oldText !== newText) {
      // 标记更新
      markUpdate(wip);
    }
  } else {
    // 1. 构建DOM
    const instance = createTextInstance(newProps.content);
    // 2. 将DOM插入到DOM树中
    wip.stateNode = instance;
  }
  bubbleProperties(wip);
  return null;

从上面我们可以看出,我们根据文本内容的不同,进行当前节点wip进行标记。

function markUpdate(fiber: FiberNode) {
  fiber.flags |= Update;
}

commitWork流程

通过beginWorkcompleteWork之后,我们得到了相应的标记。在commitWork阶段,我们就需要根据相应标记去处理不同的逻辑。本节主要讲解更新删除阶段的处理。

更新update

在之前的章节中,我们讲解了commitWorkmount阶段,我们现在根据update的flag进行逻辑处理。

// flags update
if ((flags & Update) !== NoFlags) {
  commitUpdate(finishedWork);
  finishedWork.flags &= ~Update;
}

commitUpdate

对于文本节点,commitUpdate主要是根据新的文本内容,更新之前的dom的文本内容。

export function commitUpdate(fiber: FiberNode) {
  switch (fiber.tag) {
    case HostText:
      const text = fiber.memoizedProps.content;
      return commitTextUpdate(fiber.stateNode, text);
  }
}
export function commitTextUpdate(textInstance: TestInstance, content: string) {
  textInstance.textContent = content;
}

删除ChildDeletion

beginWork过程中,对于存在要删除的子节点,我们会保存在当前父节点的deletions, 所以在删除阶段,我们需要根据当前节点的deletions属性进行对要删除的节点进行不同的处理。

// flags childDeletion
if ((flags & ChildDeletion) !== NoFlags) {
  const deletions = finishedWork.deletions;
  if (deletions !== null) {
    deletions.forEach((childToDelete) => {
      commitDeletion(childToDelete);
    });
  }
  finishedWork.flags &= ~ChildDeletion;
}

如果当前节点存在要删除的子节点的话,我们需要对每一个子节点进行commitDeletion的操作。

commitDeletion

commitDeletion函数的是对每一个要删除的子节点进行处理。它的主要功能有几点:

  • 对于不同类型的fiberNode, 当节点删除的时候,自身和所有子节点都需要执行的不同的卸载逻辑。例如:函数组件的useEffect的return函数执行,ref的解绑,class组件的componentUnmount等逻辑处理。
  • 由于fiberNode和dom节点不是一一对应的,所以要找到fiberNode对应的dom节点,然后再执行删除dom节点的操作。
  • 最后将删除的节点的childreturn指向删掉。

基于上面的2点分析,我们很容易就想到,commitDeletion肯定会执行DFS向下遍历,进行不同子节点的删除逻辑处理。


function commitDeletion(childToDelete: FiberNode) {
  let rootHostNode: FiberNode | null = null;
  // 递归子树
  commitNestedComponent(childToDelete, (unmountFiber) => {
    switch (unmountFiber.tag) {
      case HostComponent:
        if (rootHostNode === null) {
          rootHostNode = unmountFiber;
        }
        // TODO: 解绑ref
        return;
      case HostText:
        if (rootHostNode === null) {
          rootHostNode = unmountFiber;
        }
        return;
      case FunctionComponent:
        // TODO: useEffect unmount 解绑ref
        return;
      default:
        if (__DEV__) {
          console.warn("未处理的unmount类型", unmountFiber);
        }
        break;
    }
  });
  // 移除rootHostNode的DOM
  if (rootHostNode !== null) {
    const hostParent = getHostParent(childToDelete);
    if (hostParent !== null) {
      removeChild((rootHostNode as FiberNode).stateNode, hostParent);
    }
  }
  childToDelete.return = null;
  childToDelete.child = null;
}

commitNestedComponent

commitNestedComponent中主要是完成我们上面说的2点。

  • DFS深度遍历子节点
  • 找到当前要删除的fiberNode对应的真正的DOM节点

接受2个参数。1. 当前的fiberNode, 2. 递归到不同的子节点的同时,需要执行的回调函数执行不同的卸载流程。

function commitNestedComponent(
  root: FiberNode,
  onCommitUnmount: (fiber: FiberNode) => void
) {
  let node = root;
  while (true) {
    onCommitUnmount(node);
    if (node.child !== null) {
      // 向下遍历
      node.child.return = node;
      node = node.child;
      continue;
    }
    if (node === root) {
      // 终止条件
      return;
    }
    while (node.sibling === null) {
      if (node.return === null || node.return === root) {
        return;
      }
      // 向上归
      node = node.return;
    }
    node.sibling.return = node.return;
    node = node.sibling;
  }
}

这里可能比较绕,我们下面通过几个例子总结一下,这个过程的主要流程。

总结

如果按照如下的结构,要删除外层div元素,会经历如下的流程

<div>
   <Child />
   <span>hcc</span>
   yx
</div>
function Child() {
  return <div>hello world</div>
}
  • div的fiberNode的父节的标记ChildDeletion以及存放到deletions中。
  • 当执行到commitWork阶段的时候,遍历deletions数组。
  • 执行的div对应的HostComponent, 然后执行commitDeletion
  • commitDeletion中执行commitNestedComponent向下DFS遍历。
  • 在遍历的过程中,每一个节点都是执行一个回调函数,基于不同的类型执行不同的删除操作,以及记录我们要删除的Dom节点对应的fiberNode。
  • 所以首先是div执行onCommitUnmount, 由于它是HostComponent,所以将rootHostNode赋值给了div
  • 向下递归到Child节点,由于它存在子节点,继续递归到child-div节点,继续遍历到hello world节点。它不存在子节点。
  • 然后找到Child的兄弟节点,以此执行,先子后兄。直到回到div节点。

下一节预告

下一节我们讲解通过useState改变状态后,如何更新节点以及函数组件hooks是如何保存数据的。

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

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

React18之update流程从零实现详解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

React18之update流程从零实现详解

这篇文章主要为大家介绍了React18之update流程从零实现详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-10

React18从0实现dispatchupdate流程

这篇文章主要为大家介绍了React18从0实现dispatchupdate流程示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-16

React18系列reconciler从0实现过程详解

这篇文章主要介绍了React18系列reconciler从0实现过程详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-16

Python实现JavaBeans流程详解

这篇文章主要介绍了Python实现JavaBeans流程,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
2023-01-14

ReactRefs转发实现流程详解

Refs是一个获取DOM节点或React元素实例的工具,在React中Refs提供了一种方式,允许用户访问DOM节点或者在render方法中创建的React元素,这篇文章主要给大家介绍了关于React中refs的一些常见用法,需要的朋友可以参考下
2022-12-03

Vuereactive函数实现流程详解

一个基本类型的数据,想要变成响应式数据,那么需要通过ref函数包裹,而如果是一个对象的话,那么需要使用reactive函数,这篇文章主要介绍了Vuereactive函数
2023-01-04

C++模拟实现vector流程详解

这篇文章主要介绍了C++容器Vector的模拟实现,Vector是一个能够存放任意类型的动态数组,有点类似数组,是一个连续地址空间,下文更多详细内容的介绍,需要的小伙伴可以参考一下
2022-11-13

KotlinstartActivity跳转Activity实现流程详解

在Android当中,Activity的跳转有两种方法,第一个是利用startActivity(Intentintent);的方法,第二个则是利用startActivityForResult(Intentintent,intrequestCode);的方法,从字面上来看,这两者之间的差别只在于是否有返回值的区别,实际上也确实只有这两种区别
2022-12-08

详解Java实现简单SPI流程

这篇文章主要介绍了Java实现简单SPI流程,SPI英文全称为ServiceProviderInterface,顾名思义,服务提供者接口,它是jdk提供给“服务提供厂商”或者“插件开发者”使用的接口
2023-03-02

Mybatis实现SQL存储流程详解

MyBatis作为一款优秀的持久层框架,它支持自定义SQL、存储过程以及高级映射。它免除了几乎所有的JDBC代码以及设置参数和获取结果集的工作
2023-03-10

Python虚拟机字节码教程之控制流实现详解

在本篇文章当中主要给大家分析python当中与控制流有关的字节码,通过对这部分字节码的了解,我们可以更加深入了解python字节码的执行过程和控制流实现原理
2023-05-15

Vue编译优化实现流程详解

编译优化指的是编译器将模板编译为渲染函数的过程中,尽可能多的提取关键信息,并以此指导生成最优代码的过程,优化的方向主要是区分动态内容和静态内容,并针对不同的内容采用不同的优化策略
2023-01-28

编程热搜

目录