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.rhkb.cn/news/467216.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构-并查集专题(1)

一、前言 因为要开始准备年底的校赛和明年年初的ACM、蓝桥杯、天梯赛&#xff0c;于是开始按专题梳理一下对应的知识点&#xff0c;先从简单入门又值得记录的内容开始&#xff0c;并查集首当其冲。 二、我的模板 虽然说是借用了jiangly鸽鸽的板子&#xff0c;但是自己也小做…

博奥龙/诊断原料抗体对

在ELISA中&#xff0c;抗体与抗原的结合精确度依赖于抗体的特异性和灵敏度。特异性较差的抗体可能导致显著的非特异性背景信号&#xff0c;而特异好但亲和力弱的抗体可能会被洗掉&#xff0c;从而产生假阴性数据。因此&#xff0c;选择合适的可避免交叉反应和确保检测结果的准确…

OceanBase详解及如何通过MySQL的lib库进行连接

OceanBase详解及如何通过MySQL的lib库进行连接 一、引言二、OceanBase概述1. 起源与发展2. 核心技术特点3. 应用场景三、OceanBase架构解析1. 系统架构2. 存储引擎3. 分布式架构四、如何使用MySQL的lib库连接OceanBase1. 前提条件2. 安装MySQL Connector/C3. 编写连接代码4. 编…

java导出word文件(手绘)

文章目录 代码细节效果图参考资料 代码细节 使用的hutool的WordUtil&#xff0c;WordUtil对poi进行封装&#xff0c;但是这一块的官方封装的很少&#xff0c;很多细节都没有。代码中是常见的绘制段落&#xff0c;标题、表格等常用api Word07Writer writer WordUtil.getWriter(…

RNN(循环神经网络)详解

1️⃣ RNN介绍 前馈神经网络&#xff08;CNN&#xff0c;全连接网络&#xff09;的流程是前向传播、反向传播和参数更新&#xff0c;存在以下不足&#xff1a; 无法处理时序数据&#xff1a;时序数据长度一般不固定&#xff0c;而前馈神经网络要求输入和输出的维度是固定的&a…

qt QHttpMultiPart详解

1. 概述 QHttpMultiPart是Qt框架中用于处理HTTP多部分请求的类。它类似于RFC 2046中描述的MIME multipart消息&#xff0c;允许在单个HTTP请求中包含多个数据部分&#xff0c;如文件、文本等。这种多部分请求在上传文件或发送带有附件的邮件等场景中非常有用。QHttpMultiPart类…

2-148 基于matlab的铣削动力学仿真

基于matlab的铣削动力学仿真&#xff0c;推导出指导加工的稳定性叶瓣图&#xff0c;并得到各主轴转速下对应的极限切深。输入不同方向刚度和切入角、切削力系数等参数&#xff0c;进行铣削动力学仿真。程序已调通&#xff0c;可直接运行。 下载源程序请点链接&#xff1a;2-14…

使用STM32F407xx的GPIO引脚实现跑马灯效果的详细步骤

1、使用Keil创建一个新工程 2、在弹出的对话框&#xff0c;填写工程的名字&#xff0c;例如工程名字为demo_led 3、为保存的工程&#xff0c;选择对应的芯片 4、为当前工程&#xff0c;添加相应的库函数 5、若库函数添加成功&#xff0c;则显示当前工程目录树 6、在当前工程目录…

_浅谈单片机的gcc优化级别__以双音频信号发生器为例

一、简介 gcc有多种优化级别&#xff0c;一般不选择的情况下&#xff0c;IDE默认是按照-Og或这-O2优化的。 以gcc编译器为例&#xff0c;浅谈一下优化级别&#xff0c;我们常见的优化一般是指gcc的-O2、-Og。除此之外&#xff0c;gcc还有-Os等一系列优化&#xff0c;链接器也有…

用JavaScript、Nodejs写一个本地tcp服务,用于前端WebSocket调试

效果&#xff1a; 准备工作&#xff1a; 新建一个文件夹&#xff0c;在根目录安装依赖&#xff1a; npm install ws express 依赖介绍&#xff1a; WS是一个轻量级、高效的WebSocket库&#xff0c;适用于Node.js环境。 express 是一个流行的Node.js Web应用程序框架。 新…

企业常见的主数据管理挑战及解决方案

在当今高度数字化的商业环境中&#xff0c;数据已成为企业决策、运营和战略规划的核心。主数据管理&#xff08;MDM&#xff09;作为管理核心业务数据的一种方式&#xff0c;帮助企业确保其关键数据在整个组织中保持一致、准确和可信。然而&#xff0c;许多企业在实施主数据管理…

Python http打印(http打印body)flask demo(http调试demo、http demo、http printer)

文章目录 代码解释 代码 # flask_http_printer.pyfrom flask import Flask, request, jsonify import jsonapp Flask(__name__)app.route(/printinfo, methods[POST]) def print_info():# 分隔符separator "-" * 60# 获取请求头headers request.headers# 获取 JS…

从无音响Windows 端到 有音响macOS 端实时音频传输播放

以下是从 Windows 端到 macOS 端传输音频的优化方案&#xff0c;基于上述链接中的思路进行调整&#xff1a; Windows 端操作 安装必要软件 安装 Python&#xff08;确保版本兼容且已正确配置环境变量&#xff09;。安装 PyAudio 库&#xff0c;可通过 pip install pyaudio 命令…

用 Python 从零开始创建神经网络(二)

用 Python 从零开始创建神经网络&#xff08;二&#xff09; 引言1. Tensors, Arrays and Vectors&#xff1a;2. Dot Product and Vector Additiona. Dot Product &#xff08;点积&#xff09;b. Vector Addition &#xff08;向量加法&#xff09; 3. A Single Neuron with …

python爬虫自动库DrissionPage保存网页快照mhtml/pdf/全局截图/打印机另存pdf

目录 零一、保存网页快照的三种方法二、利用打印机保存pdf的方法 零 最近星球有人问如何使用页面打印功能&#xff0c;另存为pdf 一、保存网页快照的三种方法 解决方案已经放在星球内&#xff1a;https://articles.zsxq.com/id_55mr53xahr9a.html当然也可以看如下代码&…

现代Web开发:WebSocket 实时通信详解

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 现代Web开发&#xff1a;WebSocket 实时通信详解 现代Web开发&#xff1a;WebSocket 实时通信详解 现代Web开发&#xff1a;WebS…

Hadoop完全分布式环境搭建步骤

【图书介绍】《Spark SQL大数据分析快速上手》-CSDN博客 大数据与数据分析_夏天又到了的博客-CSDN博客 本文介绍Hadoop完全分布式环境搭建方法&#xff0c;这个Hadoop环境用于安装配置Spark。假设读者已经安装好Visual Box 7.0.6虚拟环境与一个CentOS 7虚拟机&#xff08;如果…

133.鸿蒙基础01

鸿蒙基础 1.自定义构建函数1. 构建函数-[Builder ](/Builder )2. 构建函数-传参传递(单向)3. 构建函数-传递参数(双向)4. 构建函数-传递参数练习5. 构建函数-[BuilderParam ](/BuilderParam ) 传递UI 2.组件状态共享1. 状态共享-父子单向2. 状态共享-父子双向3. 状态共享-后代组…

如何保证kafka生产者数据可靠性

ack参数的设置&#xff1a; 0&#xff1a;生产者发送过来的数据&#xff0c;不需要等数据落盘应答 假如发送了Hello 和 World两个信息&#xff0c;Leader直接挂掉&#xff0c;数据就会丢失 生产者 ---> Kafka集群 一放进去就跑 数据可靠性分析&#xff1a;丢数 1&#…

业务模块部署

一、部署前端 1.1 window部署 下载业务模块前端包。 &#xff08;此包为耐威迪公司发布&#xff0c;请联系耐威迪客服或售后获得&#xff09; 包名为&#xff1a;业务-xxxx-business &#xff08;注&#xff1a;xxxx为发布版本号&#xff09; 此文件部署位置为&#xff1a;……