React 探秘(四):手撸 mini-react

文章目录

    • 背景
    • 实现能力
    • 手撸开始
      • demo
      • 流程拆解
      • 实现 render 函数
      • 构建 fiber 树
      • 处理工作单元
        • 实现 create fiber
        • 加入 diff fiber 的逻辑
      • commit 阶段
      • hooks 实现
    • 源码地址
    • 参考文章

背景

前文中学习了 react 中核心的 fiber 架构,时间切片,双缓存等,接下来这篇文章实操实现一个 mini-react,巩固我们学习的这些知识。

React 探秘(一):fiber 架构

React 探秘(二):双缓存技术

React 探秘(三): 时间切片

实现能力

  • fiber 架构
  • 时间切片
  • 双缓存
  • 调和 create/diff fiber
  • hooks-useState

手撸开始

demo

我们以一个简单的 Counter 函数组件为例进行分析:

function Counter() {const [state, setState] = Didact.useState(2);return (<h1 onClick={() => { setState(c => c + 1) }} style="user-select: none">Count: {state}</h1>);
}
const element = <Counter />;
const container = document.getElementById("root");
MiniReact.render(element, container);

流程拆解

  • 入口 render 函数
    • 初始化 workInProgress
    • 开启 workLoop
  • workLoop 函数
    • 时间切片切分任务
    • vdom 转化为 fiber 节点
    • 调和的过程 create/diff
  • commit 阶段
    • 处理 fiber 中不同类型的节点同步真实 dom

实现 render 函数

render 为我们的入口函数,传入组件和根节点,根据这两个数据初始化初始化我们的 workInProgress, 在 commit 完成之后才会进行 current 替换。

let workInProgress = null;
let currentRoot= null;
let deletions = null;
let nextUnitOfWork = null;// 此处的 element 为 { type:function Counter() }
function render(element, container) {workInProgress = {dom: container,props: {children: [element]},alternate: currentRoot};deletions = [];// 开启工作单元nextUnitOfWork = workInProgress;
}

构建 fiber 树

根据上面得到的 fiber 根节点,构建 fiberNode,构建过程需要把 vdom 转化为 fiber , 其中进行 create/diff,子元素通过 child 连接,兄弟节点通过 sibling 连接, return 记录父节点用作遍历。

本文 jsx 转化,采用简单方式带过

每个 fiberNode 为一个工作单元,循环构建 fiberNode,直到没有fiberNode,处理完所有的工作单元之后,进入 commit 阶段。

/** 两种实现方式* 1. 采用 requestIdleCallback 模拟* 2. 宏任务实现 schedule *
*/
// requestIdleCallback 时间切片
function workLoop() {while (nextUnitOfWork) {// 得到下一个工作单元nextUnitOfWork = performUnitOfWork(nextUnitOfWork);}// 没有fiber并且wip存在if (!nextUnitOfWork && workInProgress) {commitRoot();}
}
requestIdleCallback(workLoop)//  2. 宏任务时间切片
//  参考文章 https://juejin.cn/post/7428168209709449268
function workLoop() {// 执行 shouldYieldToHost 来判断本次宏任务的 高频(短间隔)5ms 时间切片是否用尽while (!shouldYieldToHost() && nextUnitOfWork) {performUnitOfWork();}if (nextUnitOfWork) {console.log(`开启下一个宏任务继续执行剩余任务`);return true;} else {return false;}
}

处理工作单元

performUnitOfWork 为核心处理方法, 分为两个步骤:

  • fiber 节点的构建
    • create 阶段
    • diff 阶段
  • Counter 组件的执行
    • 得到 vdom
    • 初始化 hooks
function performUnitOfWork(fiber) {// beginWorkconst isFunctionComponent = fiber.type instanceof Function;if (isFunctionComponent) {// Counter 组件的执行updateFunctionComponent(fiber);} else {// fiberNode的构建updateHostComponent(fiber);}if (fiber.child) {return fiber.child;}let nextFiber = fiber;// fiber 的循环操作 父-子-兄while (nextFiber) {if (nextFiber.sibling) {return nextFiber.sibling;}nextFiber = nextFiber.return;}
}
// Counter 组件的执行
function updateFunctionComponent(fiber) {wipFiber = fiber;hookIndex = 0;wipFiber.hooks = [];const children = [fiber.type(fiber.props)];reconcileChildren(fiber, children);
}
// fiberNode的构建
function updateHostComponent(fiber) {if (!fiber.dom) {fiber.dom = createDom(fiber);}reconcileChildren(fiber, fiber.props.children);
}

第一次先构建根节点,构建完成后 放入 wipFiber.child 中,然后进行下一个工作循环,此时 typeCounterfucntion 执行该方法初始化 hooks 的值和得到 vdom 进行调和,完成该 fiber 节点的构建,一直复该动作直到没有其他节点.

实现 create fiber

接下来我们实现一下核心的调和的过程:
首先是 create fiber,通过elements(vdom)生成我们的 fiber 结构,并打上 PLACEMENT 表示新增

function reconcileChildren(wipFiber, elements) {let index = 0;let prevSibling = null;while (index < elements.length) {const element = elements[index];let newFiber = null;if (element && !sameType) {newFiber = {type: element.type,props: element.props,dom: null,return: wipFiber,alternate: null,effectTag: "PLACEMENT"};}if (index === 0) {wipFiber.child = newFiber;} else if (element) {prevSibling.sibling = newFiber;}prevSibling = newFiber;index++;}
}
加入 diff fiber 的逻辑

create fiber 有了, 接下来我们实现一下 diff 的过程.

diff 首先找到旧的 fiber 判断,旧 fiber type 和 新的 vdom type 是否相同, 相同的话复则用 dom 信息, 打上 UPDATE 的标签。不同的话,创建新的 fiber,打上 PLACEMENT 标签。

如果 old fiber 存在, 但是 type 却不相同则把这个节点放入 deletions 数组,打上 DELETION 标签

function reconcileChildren(wipFiber, elements) {console.log('reconcileChildren',elements);let index = 0;let oldFiber = wipFiber.alternate && wipFiber.alternate.child;let prevSibling = null;// 循环构造 child 和 sibing  while (index < elements.length || oldFiber != null) {const element = elements[index];let newFiber = null;// 判断 type 是否相同const sameType = oldFiber && element && element.type == oldFiber.type;// 相同的话,复用 domif (sameType) {newFiber = {type: oldFiber.type,props: element.props,dom: oldFiber.dom,return: wipFiber,alternate: oldFiber,effectTag: "UPDATE"};}if (element && !sameType) {newFiber = {type: element.type,props: element.props,dom: null,return: wipFiber,alternate: null,effectTag: "PLACEMENT"};}// 老节点塞入deletionsif (oldFiber && !sameType) {oldFiber.effectTag = "DELETION";deletions.push(oldFiber); }if (oldFiber) {oldFiber = oldFiber.sibling;}if (index === 0) {wipFiber.child = newFiber;} else if (element) {prevSibling.sibling = newFiber;}prevSibling = newFiber;// 遍历 props.children 数组节点index++;}
}

commit 阶段

在得到 fiber 树之后,进入我们的同步真实 dom 的过程.
这个阶段是不可暂停的, 采用递归的方式完成 fiber 的同步.

function commitRoot() {deletions.forEach(commitWork);commitWork(workInProgress.child);// 渲染完成后, 双缓存树的替换currentRoot = workInProgress;workInProgress = null;
}// 下面为核心代码
function commitWork(fiber) {// 根据type 判断执行 if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {domParent.appendChild(fiber.dom);} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {updateDom(fiber.dom, fiber.alternate.props, fiber.props);} else if (fiber.effectTag === "DELETION") {commitDeletion(fiber, domParent);}// 递归操作 fiber 树 commitWork(fiber.child);commitWork(fiber.sibling);
}

hooks 实现

根据之前对 fiber 学习我们知道 hooksfiber 上的存储也是以链表的数据结构存储,存储在 memoizedState 上。

const [state, setState] = useState(0) 根据用法我们可以推断出:

  • 该方法返回值 return [state, setState], state 是一个状态,setState 是改变状态的方法。
  • 根据特性 state 变更组件会 reRender 的特性推测出,setState 在计算出最新的值后会重启 workLoop

function useState(initial) {// setState 后初始化 oldHookif (!oldHook) {oldHook = wipFiber.alternate?.memoizedState}// 每次进入重新构建 hookconst hook = {state: oldHook ? oldHook.state : initial,queue: [],next: oldHook ? oldHook.next : null,};// 拿到当前 hook 的任务队列const actions = oldHook ? oldHook.queue : [];// 计算最新的 state actions.forEach(action => {hook.state = action(hook.state);});// 构建 hook 链表if (!workInProgressHook) {workInProgressHook = hookwipFiber.memoizedState = workInProgressHook;} else {workInProgressHook = workInProgressHook.next = hook}// 获取下一个 hook oldHook = oldHook && oldHook.nextconst setState = action => {hook.queue.push(action);// 重新构建 workInProgressworkInProgress = {dom: currentRoot.dom,props: currentRoot.props,alternate: currentRoot};oldHook = nullworkInProgressHook = null// 设置下一个工作单元 reRendernextUnitOfWork = workInProgress;deletions = [];};return [hook.state, setState];
}

至此我们就基本完成了一个简单的 mini-react,上面代码直接截取了关键代码,如果感兴趣的话可以结合下面源码进行本地调试。

源码地址

mini-react: https://github.com/lovelts/mini-react/blob/master/src/index.js

参考文章

react 源码:

workLoop: https://github.com/facebook/react/blob/v18.3.1/packages/react-reconciler/src/ReactFiberWorkLoop.new.js

beginWork: https://github.com/facebook/react/blob/v18.3.1/packages/react-reconciler/src/ReactFiberBeginWork.new.js

schedule: https://github.com/facebook/react/blob/v18.3.1/packages/scheduler/src/forks/Scheduler.js

commitWork: https://github.com/facebook/react/blob/v18.3.1/packages/react-reconciler/src/ReactFiberCommitWork.new.js

hooks: https://github.com/facebook/react/blob/v18.3.1/packages/react-reconciler/src/ReactFiberHooks.new.js

build-your-own-react: https://pomb.us/build-your-own-react/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/9241.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

科大讯飞面经,蛮简单的

先来看面经&#xff1a; 下面我来简单聊聊这些问题。 自我介绍 关于如何自我介绍&#xff0c;这个如果还不会或者还没有准备&#xff0c;请先准备好你要如何向面试官介绍自己。 面试本来就是一个自我推销的方式之一&#xff0c;如果自我介绍都不会说&#xff0c;你如何卖个好价…

ARM64汇编寻址、汇编指令、指令编码方式

版权归作者所有&#xff0c;如有转发&#xff0c;请注明文章出处&#xff1a;https://cyrus-studio.github.io/blog/ ARM64汇编寻址 1. 立即数寻址&#xff08;Immediate Addressing&#xff09; 这种方式直接将立即数作为操作数&#xff0c;适合小数据或常量。ARM64的立即数…

创建者模式之【建造者模式】

建造者模式 概述 将一个复杂对象的构建与表示分离&#xff0c;使得同样的构建过程可以创建不同的表示。 分离了部件的构造(由Builder来负责)和装配(由Director负责)。 从而可以构造出复杂的对象。这个模式适用于&#xff1a;某个对象的构建过程复杂的情况。由于实现了构建和…

什么是CANN和Ascend C

1 CANN是什么 异构计算架构CANN&#xff08;Compute Architecture for Neural Networks&#xff09;是华为针对AI场景推出的异构计算架构&#xff0c;向上支持多种AI框架&#xff0c;包括MindSpore、PyTorch、TensorFlow等&#xff0c;向下服务AI处理器与编程&#xff0c;发挥…

GenAI 用于客户支持 — 第 5 部分:可观察性

作者&#xff1a;来自 Elastic Andy James 本系列将带你深入了解我们如何在客户支持中使用生成式人工智能。加入我们&#xff0c;实时分享我们的历程&#xff0c;本篇文章重点介绍支持助理的可观察性。 本博客系列揭示了我们的现场工程团队如何使用 Elastic stack 和生成式 AI …

python安装selenium,geckodriver,chromedriver,Selenium IDE

安装浏览器 找到浏览器的版本号 chrome 版本 130.0.6723.92&#xff08;正式版本&#xff09; &#xff08;64 位&#xff09; firfox 116.0.3 (64 位)&#xff0c;但是后面运行的时候又自动更新到了 127.0.0.8923 安装selenium > pip install selenium > pip show …

Docker部署SpringBoot项目(镜像部署)

目录 一、在pom.xml 文件中加入依赖 1.依赖内容 2.依赖说明和解释 3.使用流程 4.示例 5.注意 二、执行打包 1.使用命令打包 2.使用IDEA提供快捷方式 三、将jar包上传到服务器 四、创建相关配置 1.创建一个Dockerfile文件 2.添加配置 3.举例 五、生成Docker镜像 1.…

WPF+MVVM案例实战与特效(二十五)- 3D粒子波浪效果实现

文章目录 1、案例效果2、案例实现1、文件创建2. 功能代码实现3、粒子功能应用1、前端布局与样式2、代码解释2、 后端功能代码1、案例效果 2、案例实现 1、文件创建 打开 Wpf_Examples 项目、Models 文件夹下创建 3D粒子模型类 ParticleWaveEffectModel.cs 文件。在Tools 文件…

设计模式之建造者模式(各项装修物料组合套餐选配场景)

前言&#xff1a; 乱码七糟&#xff0c;我时常怀疑这个成语是来形容程序猿的&#xff01; 无论承接什么样的需求&#xff0c;是不是身边总有那么几个人代码写的烂&#xff0c;但是却时常有测试小姐姐过来聊天(求改bug)、有产品小伙伴送吃的(求写需求)、有业务小妹妹陪着改代码(…

ffmpeg视频滤镜:组合两个视频为立体视频- framepack

视频描述 framepack 官方网址 > FFmpeg Filters Documentation 这个滤镜会将两个视频进行组合&#xff0c;有个前提是这两个视频的帧率、分别率必须一样。比如输入的是两个852x480 视频&#xff0c;输出可能是1704*480&#xff08;左右拼接&#xff09;、852*960&#xf…

【K8S问题系列 | 8】K8S集群资源突然爆满导致 Pod 状态变为 Pending 详细解决方案

在 Kubernetes 集群中&#xff0c;当 CPU 突然爆满时&#xff0c;Pod 可能无法获得所需的资源&#xff0c;从而导致其状态变为 Pending。以下是更详细的解决方案描述&#xff0c;有效应对这一问题。 解决方案 1: 扩展集群资源 描述 当集群资源不足以支撑当前的工作负载时&…

第18篇 :深入剖析systemverilog中 randomize 失败案例启示录(一)

经过前面章节的理论学习&#xff0c;我们对systemverilog中的随机约束&#xff0c;有一定的了解&#xff0c;那么&#xff0c;今天开始&#xff0c;着重讲述一些工作中遇到的困惑。主要通过一些例子&#xff0c;层层递进&#xff0c;举一反三&#xff0c;源于实践&#xff0c;剖…

mac端mumu模拟器adb识别不了问题

1.在终端中输入&#xff1a;system_profiler SPUSBDataType,把0x05e3 (Genesys Logic, Inc.)复制 2. 1.cd ~/.android/ 2.open . 3.找到.android/adb_usb.ini文件 将以上格式的Wendor ID放入该文件 3.依次执行 * adb devices* adb kill-server* adb start-server* adb disco…

Ubuntu版本、ROS版本与Python 版本之间的关系

引言 在机器人开发中&#xff0c;ROS&#xff08;机器人操作系统&#xff09;广泛应用于科研和工业领域&#xff0c;支持多个Ubuntu和Python版本。然而&#xff0c;随着不同Ubuntu LTS版本的发布以及Python逐渐从2.x向3.x过渡&#xff0c;ROS的版本选择和兼容性要求也在不断变化…

Linux - 信号

文章目录 一、信号的定义二、查看信号三、产生信号1、指令2、系统调用3、由软件条件产生信号4、异常5、键盘输入 四、保存信号1、补充&#xff1a;信号其他相关概念2、信号保存在哪&#xff0c;怎么保存&#xff1f;3、信号集操作函数 五、捕获信号1、概念2、捕获信号的时机3、…

PMP–知识卡片--项目干系人

项目干系人主要分为两类&#xff1a;参与项目的人和受项目影响的人。按照由近及远&#xff0c;从项目经理、项目团队等逐渐扩充至供应商、客户、监管机构等。 项目往往死在被忽略的干系人手上&#xff0c;作为项目经理&#xff0c;要尽可能地识别出来所有可能影响项目以及受项目…

MATLAB - ROS 2 分析器

系列文章目录 前言 本主题介绍如何连接 ROS 2 网络&#xff0c;分析网络图中所有元素的基本信息&#xff08;如节点名称和节点之间的信息&#xff09;&#xff0c;以及可视化与 ROS 2 节点相关的参数&#xff08;如主题、服务和操作&#xff09;之间的交互。 一、连接并查看 RO…

分组校验在Spring中的应用详解

目录 前言1. 什么是分组校验2. 分组校验的基本原理3. 分组校验的实现步骤3.1 定义分组接口3.2 在校验项中指定分组3.3 校验时指定要校验的分组3.4 默认分组和分组的继承 4. 分组校验的优势和适用场景4.1 优势4.2 适用场景 5. 常见问题与解决方案5.1 校验未生效5.2 无法识别默认…

SDL打开YUV视频

文章目录 问题1&#xff1a;如何控制帧率&#xff1f;问题2&#xff1a;如何触发退出事件&#xff1f;问题3&#xff1a;如何实时调整视频窗口的大小问题4&#xff1a;YUV如何一次读取一帧的数据&#xff1f; 问题1&#xff1a;如何控制帧率&#xff1f; 单独用一个子线程给主线…

[MySQL]索引

索引介绍 索引是帮助数据库高效获取数据的数据结构。在数据之外&#xff0c;数据库系统还维护着满足特定查找算法的数据结构&#xff0c;这些数据结构以某种方式引用数据&#xff0c; 这样就可以在这些数据结构上实现高级查找算法&#xff0c;这种数据结构就是索引。 假设我们有…