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.mzph.cn/news/884867.shtml

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

相关文章

【AIGC】ChatGPT提示词Prompt高效编写技巧:逆向拆解OpenAI官方提示词

博客主页&#xff1a; [小ᶻZ࿆] 本文专栏: AIGC | ChatGPT 文章目录 &#x1f4af;前言&#x1f4af;OpenAI官方提示词的介绍OpenAI官方提示词的结构与组成如何通过分析提示词找到其核心组件 &#x1f4af;OpenAI官方提示词分析案例一&#xff1a;制定教学计划案例二&…

Ubuntu 22 安装 Apache Doris 3.0.3 笔记

Ubuntu 22 安装 Apache Doris 3.0.3 笔记 1. 环境准备 Doris 需要 Java 17 作为运行环境&#xff0c;所以首先需要安装 Java 17。 sudo apt-get install openjdk-17-jdk -y sudo update-alternatives --config java在安装 Java 17 后&#xff0c;可以通过 sudo update-alter…

多线程生产消费者模型

线程同步 互斥锁(互斥量)条件变量生产/消费者模型 一、互斥锁 C11提供了四种互斥锁&#xff1a; mutex&#xff1a;互斥锁。timed_mutex&#xff1a;带超时机制的互斥锁。recursive_mutex&#xff1a;递归互斥锁。recursive_timed_mutex&#xff1a;带超时机制的递归互斥锁…

理解 WordPress | 第五篇:页面构建器选择指南

WordPress 专题致力于从 0 到 1 搞懂、用熟这种可视化建站工具。 第一阶段主要是理解。 第二阶段开始实践个人博客、企业官网、独立站的建设。 如果感兴趣&#xff0c;点个关注吧&#xff0c;防止迷路。 什么是 WordPress 构建器 WordPress 构建器&#xff08;Page Builder&am…

【Linux系统编程】第四十二弹---多线程编程全攻略:涵盖线程创建、异常处理、用途、进程对比及线程控制

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】【Linux系统编程】 目录 1、线程创建 2、线程异常 3、线程用途 4、进程 VS 线程 5、线程控制 5.1、创建和等待线程 1、线程创建 线程能看到进程的大…

基于SSM的在线作业管理系统 -octopus-master(源码+调试)

项目描述 临近学期结束&#xff0c;还是毕业设计&#xff0c;你还在做java程序网络编程&#xff0c;期末作业&#xff0c;老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。你想解决的问题&#xff0c;今天给大家介绍…

医学影像类和医用电气设备测试标准整理

医学影像类和医用电气设备测试标准整理 1、GB 9706.225-2022 医用电气设备 第2-25部分:心电图机的基本安全和基本性能专用要求 GB 9706.225规定了在201.3.63中定义的通过自身或作为ME系统一部分,提供可供诊断用的心电图报告的心电图机基本安全和基本性能,以下称为ME设备。 …

鸿蒙原生应用开发及部署:首选华为云,开启HarmonyOS NEXT App新纪元

目录 前言 HarmonyOS NEXT&#xff1a;下一代操作系统的愿景 1、核心特性和优势 2、如何推动应用生态的发展 3、对开发者和用户的影响 华为云服务在鸿蒙原生应用开发中的作用 1、华为云ECS C系列实例 &#xff08;1&#xff09;全维度性能升级 &#xff08;2&#xff…

3^100的位数判断

3^100的位数判断 问题来源 字节面试&#xff0c;面试官提问&#xff1a;口算估计3^100的位数&#xff0c;或是给出位数估计范围。 解决方案 方法一&#xff1a; 该方法纯口算&#xff0c;可得一个较为准确的一个范围 2 100 < 3 100 < 4 100 2^{100}<3^{100}<…

ROS2简介与Ubuntu24.04中安装指南

之前安装了一个版本&#xff0c;但是不愿意写blog&#xff0c;现在想想自己就是个沙子立个flag&#xff0c;每次配置项目&#xff0c;写流程blog ROS简介 ROS&#xff08;Robot Operating System&#xff09;是一个开源的机器人软件平台&#xff0c;提供了许多工具和库来帮助…

Linux sudo命令及权限设置

普通用户的权限是有限制的&#xff0c;需要更大的权限&#xff0c;就需要使用 root 用户&#xff0c;但又不想一直使用 root 用户&#xff0c;如普通用户查看 8080 端口的监听情况&#xff1a; netstat -tulnp | grep :8080 只能查看自己的 不想用 root 用户&#xff0c;继续…

微服务网关的认证管理;原理与实践

API安全认证是网关的最重要能力 API 网关为了保护对外提供的API&#xff0c;避免诸如恶意访问、未授权访问、应用漏洞及黑客攻击等导致的数据和资产损失&#xff0c;采用API网关的认证机制显得十分必要。 这种认证机制通过基于token的身份验证来实现&#xff0c;它允许应用程…

STM32 + CubeMX + 硬件SPI + W5500 +TcpClient

这篇文章记录一下STM32W5500TCP_Client的调试过程&#xff0c;实现TCP客户端数据的接收与发送。 目录 一、W5500模块介绍二、Stm32CubeMx配置三、Keil代码编写1、添加W5500驱动代码到工程&#xff08;添加方法不赘述&#xff0c;驱动代码可以在官网找&#xff09;2、在工程中增…

微信小程序中,点击视频,没有跳转播放,可能是因为没有在app.json中正确注册视频播放页面的路径

const customMethodMap {handlePreview(e) {const { item: { url } } e?.currentTarget?.datasetconsole.log(Clicked item URL:, url); // 输出URLconst type url.split(.)[url.split(.)?.length - 1]console.log(File type:, type); // 输出文件类型console.log(isDoc(…

软件体系结构

第一章 构件 具有某种功能的 可复用的软件结构单元,为组装服务,可部署,具有规范的接口规约和显式的语境依赖 构件模型 构件模型是对构件本质特征的抽象描述&#xff0c;可以把它想象成一个类的组合&#xff0c;它封装了多个类&#xff0c;并具有一个或多个服务而提供了简单…

Spark 的Standalone集群环境安装与测试

目录 一、Standalone 集群环境安装 &#xff08;一&#xff09;理解 Standalone 集群架构 &#xff08;二&#xff09;Standalone 集群部署 二、打开监控界面 &#xff08;一&#xff09;master监控界面 &#xff08;二&#xff09;日志服务监控界面 三、集群的测试 &a…

react的antd-mobile使用Steps显示物流

antd-mobile的图标&#xff0c;是需要安装依赖的 step如果只有一个步骤是不会展示的&#xff0c;代码里面的标题那块可以看出来 尝试了很多遍测试发现一直不显示&#xff0c;查询后发现是这个组件的本身设置的原因 那么就算你只展示一个那么也要写两个step&#xff0c;第二个…

基于鸟类AI识别的果园智能物联网解决方案

1. 项目背景 我国拥有广阔的果园种植面积&#xff0c;但每年因鸟类造成的损失高达数亿元。传统的防鸟害措施&#xff0c;如建立防护网和使用物理化学方法&#xff0c;效果并不理想&#xff0c;且成本较高。为了解决这一问题&#xff0c;深圳快瞳科技有限公司的提出基于鸟类AI识…

让Erupt框架支持.vue文件做自定义页面模版

Erupt是什么&#xff1f; Erupt 是一个低代码 全栈类 框架&#xff0c;它使用 Java 注解 动态生成页面以及增、删、改、查、权限控制等后台功能。 零前端代码、零 CURD、自动建表&#xff0c;仅需 一个类文件 简洁的注解配置&#xff0c;快速开发企业级 Admin 管理后台。 提…

如何优雅处理异常?处理异常的原则

前言 在我们日常工作中&#xff0c;经常会遇到一些异常&#xff0c;比如&#xff1a;NullPointerException、NumberFormatException、ClassCastException等等。 那么问题来了&#xff0c;我们该如何处理异常&#xff0c;让代码变得更优雅呢&#xff1f; 1 不要忽略异常 不知…