实现自己的mini-react

news2024/11/21 0:18:28

实现自己的mini-react

  • 创建运行环境
  • 实现最简单mini-react
    • 渲染dom
    • 封装创建虚拟dom节点
    • 封装函数
    • 封装render函数
    • 对齐react 调用方式
    • 使用 jsx
  • 任务调度器&fiber架构
    • 封装一个workLoop方法
  • 统一提交&实现 function component
    • 统一提交
    • 实现支持 function component
  • 进军 vdom 的更新
    • 实现绑定事件
    • 更新props
  • 击杀 update children
  • 搞定 useState
  • 搞定 useEffect

创建运行环境

pnpm create vite
  • 选择Vanilla创建项目 选择javascript就行
    创建运行环境
  • 删除多余文件 保留最简单目录
    目录

实现最简单mini-react

渲染dom

  • index.html
    <div id="root"></div>
    <script type="module" src="./main.js"></script>
  • main.js代码
	const dom = document.createElement("div");
	dom.id="app"
	document.querySelector("#root").appendChild(dom);
	 
	const text = document.createTextNode("");
	text.nodeValue = "hello mini react";
	dom.append(text)

这样就可以在浏览器上看到hello mini react了

封装创建虚拟dom节点

  • 首先抽离节点
const textNode = {
    type: "TEXT_ELEMENT",
    props: {
        nodeValue: "hello mini react",
        children: []
    }
}
const el = {
    type: "div",
    props: {
        id: "app",
        children: [textNode]
    }
}
  • 渲染dom
	const dom = document.createElement(el.type);
	dom.id=el.props.id
	document.querySelector("#root").appendChild(dom);
	 
	const text = document.createTextNode("");
	text.nodeValue = textNode.props.nodeValue;
	dom.append(text)

可以看到结果是一样的

封装函数

  • 把上面的el和textNode封装一下方便调用
function createTextNode(nodeValue) {
    return {
        type: "TEXT_ELEMENT",
        props: {
            nodeValue,
            children: [],
        },
    };
}
function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children,
        },
    };
}
  • 渲染dom
    const textNode=createTextNode("hello mini react");
    const el = createElement("div",{id:"app"},textNode)
	const dom = document.createElement(el.type);
	dom.id=el.props.id
	document.querySelector("#root").appendChild(dom);
	 
	const text = document.createTextNode("");
	text.nodeValue = textNode.props.nodeValue;
	dom.append(text)

可以看到结果是一样的

封装render函数

  1. 创建一个DOM节点,根据el的类型来决定是创建一个文本节点还是一个元素节点
  2. 遍历el的props属性,将除了children之外的属性都赋值给dom节点
  3. 获取el的children属性 遍历children,对每个子元素调用render函数进行递归渲染
  4. 把子节点添加到父节点中
function render(el, container) {
    // 创建一个DOM节点,根据el的类型来决定是创建一个文本节点还是一个元素节点
    const dom =
        el.type === "TEXT_ELEMENT"
            ? document.createTextNode("")
            : document.createElement(el.type);
    // 遍历el的props属性,将除了children之外的属性都赋值给dom节点
    Object.keys(el.props).forEach(key => {
        if (key !== "children") {
            dom[key] = el.props[key];
        }
    })
    // 获取el的children属性
    const children = el.props.children
    // 遍历children,对每个子元素调用render函数进行递归渲染
    children.forEach(child => {
        render(child, dom)
    })
    // 将dom添加到container中
    container.append(dom);
}
  • 重构createElement函数 之前我们传递节点是createTextNode(“hello mini react”) 现在想之间写"hello mini react" 需要修改函数
function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children: children.map(child => {
                return typeof child === "string" ? createTextNode(child) : child
            }),
        },
    };
}
  • 渲染dom
    const el = createElement("div",{id:"app"},"hello mini react")
	render(el,document.querySelector("#root"))

对齐react 调用方式

  1. 创建core 文件夹 里面包含React.js和ReactDOM.js两个文件
  2. 创建src文件夹 里面包含App.js文件
  • React.js
function createTextNode(nodeValue) {
    return {
        type: "TEXT_ELEMENT",
        props: {
            nodeValue,
            children: [],
        },
    };
}
function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children: children.map(child => {
                return typeof child === "string" ? createTextNode(child) : child
            }),
        },
    };
}

function render(el, container) {
    const dom =
        el.type === "TEXT_ELEMENT"
            ? document.createTextNode("")
            : document.createElement(el.type);
    Object.keys(el.props).forEach(key => {
        if (key !== "children") {
            dom[key] = el.props[key];
        }
    })
    const children = el.props.children
    children.forEach(child => {
        render(child, dom)
    })
    container.append(dom);
}
const React={
    render,
    createElement
}
export default React
  • ReactDOM.js
import React from './React.js'
const ReactDOM = {
    createRoot(container) {
        return {
            render(App){
                React.render(App, container)
            }
        }
    }
}

export default ReactDOM
  • App.js
import React from '../core/React.js'
const App =<div>Hello mini react! <span>Hi React</span></div>

export default App
  • main.js
import App from './src/App.js'
import ReactDOM from './core/ReactDOM.js'
ReactDOM.createRoot(document.querySelector("#root")).render(App)

运行项目发现效果是一样的

使用 jsx

因为刚开始使用vite创建的项目 所以把App.js和main.js改成App.jsx和main.jsx 然后在index.hrml script引用 在运行项目即可

以上就是我们对mini-react的基本搭建

任务调度器&fiber架构

使用了 requestIdleCallback
为什么使用requestIdleCallback
因为 render 函数中执行大量dom 渲染的时候 会导致卡顿,我们需要对任务进行拆分,拆分成一个个小任务,然后依次执行,从而避免卡顿

封装一个workLoop方法

  • React.js
// 工作循环函数
let nextWorkOfUnit = {};
function workLoop(deadline) {
  // 工作循环函数,用于不断执行任务直至满足条件
  let shouldDeadline = false; // 初始化一个变量用于判断是否需要满足截止时间
  while (!shouldDeadline && nextWorkOfUnit) {
    // 循环执行任务,直到满足截止时间条件或者没有任务可执行
    nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit); // 使用任务处理函数处理当前任务对象
    shouldDeadline = deadline.timeRemaining() < 1; // 判断当前任务的截止时间是否小于1
  }
  // 请求下一次执行该函数的时间间隔,并递归调用该函数
  requestIdleCallback(workLoop);
}
  requestIdleCallback(workLoop);
  • 实现 filber 架构(把树结构转变成链表结构)
  • 首现判断当前子节点(child)中有没有子节点(child)
  • 如果当前节点没有子节点,就找当前节点的兄弟节点(sibling)
  • 如果当前节点没有兄弟节点(sibling),就找当前节点的父节点(parent) 的兄弟节点(sibling)
  • 如果当前节点的父节点(parent) 没有兄弟节点(sibling),就在往上找
  • 如果当前节点没有 parent 那么就结束

结构描述

  • 实现performWorkOfUnit
function createDom(type) {
  return type === "TEXT_ELEMENT"
    ? document.createTextNode("")
    : document.createElement(type);
}

function updateProps(dom,props){
    Object.keys(props).forEach((key) => {
        if (key !== "children") {
          dom[key] = props[key];
        }
      })
}
function initChildren(fiber){
    const children = fiber.props.children;
    let prvChild = null;
    children.forEach((child, index) => {
      const newFiber = {
        type: child.type,
        props: child.props,
        parent: fiber,
        child: null,
        sibling: null,
        dom: null,
      };
      if (index === 0) {
        fiber.child = newFiber;
      } else {
        prvChild.sibling = newFiber;
      }
      prvChild = newFiber;
    });
}
function performWorkOfUnit(fiber) {
  if (!fiber.dom) {
    const dom = (fiber.dom =createDom(fiber.type));
    fiber.parent.dom.append(dom);
    updateProps(dom,fiber.props)
  }
  initChildren(fiber)
  if (fiber.child) {
    return fiber.child;
  }
  if (fiber.sibling) {
    return fiber.sibling;
  }
  return fiber.parent?.sibling;
}
  • 修改render
function render(el, container) {
  nextWorkOfUnit = {
    dom: container,
    props: {
      children: [el],
    },
  };
}

统一提交&实现 function component

统一提交

我们使用 requestIdleCallback实现任务调度,但是它只有等待浏览器有空闲时间才会执行任务,如果任务很多,那页面渲染就只能看到一半渲染。

  • React.js
let nextWorkOfUnit = {}
let root = null
function render(el, container) {
  nextWorkOfUnit = {
    dom: container,
    props: {
      children: [el],
    },
  };
  root=nextWorkOfUnit
}
function workLoop(deadline) {
  let shouldDeadline = false;
  while (!shouldDeadline && nextWorkOfUnit) {
    nextWorkOfUnit = sunWorkFun(nextWorkOfUnit);
    shouldDeadline = deadline.timeRemaining() < 1;
  }
  if(!nextWorkOfUnit&&root){
    commitRoot()
  }
  requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);

function commitRoot(){
  commitWork(root.child)
}

function commitWork(fiber){
  if(!fiber) return;
  if(fiber.dom){
    fiber.parent.dom.append(fiber.dom);
  }
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

实现支持 function component

现在我们渲染dom是这样

  • ReactDOM.createRoot(document.querySelector("#root")).render(App)
    我们想改成
  • ReactDOM.createRoot(document.querySelector("#root")).render(<App />)
  • React.js
// 创建元素节点
/**
 * 创建一个元素
 * @param {string} type - 元素的类型
 * @param {Object} props - 元素的属性
 * @param {...any} children - 元素的子元素
 * @returns {Object} - 创建的元素对象
 */
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) => {
        /**
         * 判断子元素是否为文本节点
         * @type {boolean}
         */
        const isTextNode =
          typeof child === "string" || typeof child === "number";
        return isTextNode ? createTextNode(child) : child;
      }),
    },
  };
}
// 提交节点
function commitWork(fiber) {
  // 检查fiber是否存在
  if (!fiber) return;

  // 初始化fiber的父级节点
  let fiberParent = fiber.parent;

  // 循环找到有dom节点的父级节点
  while (!fiberParent.dom) {
    fiberParent = fiberParent.parent;
  }
  fiberParent.dom.append(fiber.dom);

  // 递归调用commitWork函数处理fiber的子节点
  commitWork(fiber.child);

  // 递归调用commitWork函数处理fiber的兄弟节点
  commitWork(fiber.sibling);
}
/**
 * 更新函数组件
 * 
 * @param {Object} fiber - Fiber对象
 */
function updateFunctionComponent(fiber) {
  const children = [fiber.type(fiber.props)];
  initChildren(fiber, children);
}

function updateHostComponent(fiber) {
  // 如果fiber没有关联的dom节点
  if (!fiber.dom) {
    // 创建一个新的dom节点
    const dom = (fiber.dom = createDom(fiber.type));
    // 更新dom节点的属性
    updateProps(dom, fiber.props, {});
  }
  // 获取子元素
  const children = fiber.props.children;
  // 初始化子元素
  initChildren(fiber, children);
}
/**
 * 函数:performWorkOfUnit
 * 描述:用于渲染节点的函数
 * 参数:
 * - fiber:fiber对象,包含节点的信息
 */
function performWorkOfUnit(fiber) {
  /**
   * 变量:isFunctionComponent
   * 类型:boolean
   * 描述:判断fiber.type是否为函数节点
   */
  const isFunctionComponent = typeof fiber.type === "function";
  /**
   * 判断不是函数节点且fiber.dom不存在时,创建dom节点并更新属性
   */
  if (isFunctionComponent) {
    updateFunctionComponent(fiber);
  } else {
    updateHostComponent(fiber);
  }

  /**
   * 判断fiber是否有子节点,返回子节点
   */
  if (fiber.child) {
    return fiber.child;
  }
  /**
   * 变量:nextFiber
   * 类型:fiber对象
   * 描述:遍历fiber对象的父级节点
   */
  let nextFiber = fiber;
  while (nextFiber) {
    /**
     * 判断nextFiber是否有兄弟节点,返回兄弟节点
     */
    if (nextFiber.sibling) return nextFiber.sibling;
    nextFiber = nextFiber.parent;
  }
}

进军 vdom 的更新

实现绑定事件

  • App.jsx
function App() {
 
 function handleClick(){
    console.log("🚀 ~ App ~ App:")
  }
  
  return (
    <div>
      <button onClick={handleClick}>click</button> 
    </div>
  );
}
  • 修改updateProps函数
function updateProps(dom,props){
    Object.keys(props).forEach(key=>{
        if(key.startsWith('on')){
            // 事件名
            const eventType = key.slice(2).toLowerCase();// 或.substring(2);
            dom.addEventListener(eventType,props[key]);
        }else{
            dom[key] = props[key];
        }
    })
}

更新props

对比 new vdom tree VS old vdom tree,找出差异,更新dom

在这里插入图片描述
后续更新…

击杀 update children

后续更新…

搞定 useState

后续更新…

搞定 useEffect

后续更新…

github

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

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

相关文章

Python语法进阶——类

Python中的数据类型都属于类。int、str、list都是Python定义好的数据类型类。 print(type(list))#<class type> print(type(list()))#<class list> 一、自定义数据类型 一、语法 class 类名():pass #类名 要求首字母大写 #()可写可省略。 #pass在这里只是用来保证…

一文详解 Berachain 测试网:全面介绍与教程,bitget wallet教程

什么是Berachain&#xff1f; Berachain&#xff08;web3.bitget.com/zh-CN/assets/berachain-wallet&#xff09;是一种尖端区块链技术&#xff0c;使用 Cosmos SDK 构建的 Layer-1&#xff0c;兼容以太坊虚拟机&#xff08;EVM&#xff09;。它基于一种独特的概念&#xff0c…

Docker(九)Docker Buildx

作者主页&#xff1a; 正函数的个人主页 文章收录专栏&#xff1a; Docker 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&#xff01; Docker Buildx Docker Buildx 是一个 docker CLI 插件&#xff0c;其扩展了 docker 命令&#xff0c;支持 [Moby BuildKit] 提供的功能。提…

用于垃圾回收的运行时配置选项

反馈 本文内容 指定配置的方法垃圾回收的风格管理资源使用情况大型页面 显示另外 4 个 此页面包含有关 .NET 运行时垃圾回收器 (GC) 设置的信息。 如果你要尝试让正在运行的应用达到最佳性能&#xff0c;请考虑使用这些设置。 然而&#xff0c;在特定情况下&#xff0c;默认…

Linux指令补充和权限简单介绍

一.tar指令 形式&#xff1a;tar [-cxtzjvf] 文件与目录 ....
 参数&#xff1a;
 -c &#xff1a;建立一个压缩文件的参数指令(create 的意思)&#xff1b; -x &#xff1a;解开一个压缩文件的参数指令&#xff01; -t &#xff1a;查看 tarfile 里面的文件&#xff01; -…

Kafka常见指令及监控程序介绍

kafka在流数据、IO削峰上非常有用&#xff0c;以下对于这款程序&#xff0c;做一些常见指令介绍。 下文使用–bootstrap-server 10.0.0.102:9092,10.0.0.103:9092,10.0.0.104:9092 需自行填写各自对应的集群IP和kafka的端口。 该写法 等同 –bootstrap-server localhost:9092 …

2024 前端高频面试题之 JS 篇

JS 篇&#xff08;持续更新中&#xff09; 1、什么是原型、原型链&#xff1f;2、什么是继承&#xff1f;说一说有哪些&#xff1f;继承组合的原理及优点&#xff1f;3、new 操作符具体干了什么&#xff1f;4、js 有哪些方法改变 this 指向&#xff1f;5、bind 有哪些实现的注意…

【C++ | 数据结构】从哈希的概念 到封装C++STL中的unordered系列容器

文章目录 一、unordered系列容器的底层结构 - 哈希1. 哈希概念2. 哈希冲突 二、解决哈希冲突方法一&#xff1a;合理设计哈希函数&#x1f6a9;哈希函数设计原则&#x1f6a9;常见哈希函数 方法二&#xff1a;开闭散列&#x1f6a9;闭散列线性探测法&#xff08;实现&#xff0…

利用Anaconda安装pytorch和paddle深度学习环境+pycharm安装后不能调用pytorch和paddlepaddle框架

问题现象&#xff1a; 之前安装后不能在添加pytorch和paddlepaddle框架 原因&#xff08;疑似&#xff09;&#xff1a; 在终端中显示pytorch和paddle在C盘但是安装是安装在J盘 解决办法&#xff1a; 卸载、删除文件重新安装后可以看到文件位置在J盘中 但是选择时还是显示C…

Tomcat的maxParameterCountmaxPostSize参数

Tomcat的maxParameterCount&maxPostSize参数 Tomcat的maxParameterCount&maxPostSize参数1.问题1.1问题现象1.2 参数总结1.3 问题总结 2 Tomcat官网的解释2.1 到https://tomcat.apache.org/找到文档入口2.2 找到文档的Reference2.3 查看配置文件的参数 3 文档看不明白&…

用的到的linux-Day1

前言&#xff1a; 从入门IT开始我们知道Linux操作系统与其他操作系统不同&#xff0c;Linux因为其独特的优势&#xff0c;被广泛应用在服务器领域&#xff0c;而且是一个近乎完美的操作系统&#xff0c;运行稳定、功能强大、安全性高、开源、可定制等等。 因此我打算从24年开始…

Rocky Linux 9. 3安装图解

风险告知 本人及本篇博文不为任何人及任何行为的任何风险承担责任&#xff0c;图解仅供参考&#xff0c;请悉知&#xff01;本次安装图解是在一个全新的演示环境下进行的&#xff0c;演示环境中没有任何有价值的数据&#xff0c;但这并不代表摆在你面前的环境也是如此。生产环境…

Proxmox VE 8 试装Oracle 23c

作者&#xff1a;田逸&#xff08;formyz&#xff09; Oracle 当前的最新版本是23c&#xff0c;虽然官方网站下载不了它的正式版本&#xff0c;但是却提供了一个性能受限的免费版本“Oracle Database 23.3 Free”&#xff08;存储容量受限、内存使用受限&#xff09;。这里就只…

PLC-IoT 网关开发札记(5):将本地数据库作为资产打包发布到 App

App需求&#xff1a;保存物模型 什么是物模型 在项目开发中&#xff0c;用到了本地数据库&#xff0c;这个本地数据库记录了系统的物模型。所谓物模型就是对某一个设备的可操纵属性的定义&#xff0c;每一个设备包括了一个或者多个属性&#xff0c;通过获取这些属性的当前值可…

【Web实操11】定位实操_照片墙(无序摆放)

设置一个板块&#xff0c;将照片随意无序摆放在墙上&#xff0c;从而形成照片墙。本来效果应该是很唯美好看的&#xff0c;就像这种&#xff0c;但是奈何本人手太笨&#xff0c;只好设置能达到照片墙的效果就可。 代码如下&#xff1a; <!DOCTYPE html> <html lang&…

2023 年,我患上了 AI 焦虑症!

【作者有话说】2023 年对我来说是神奇的一年&#xff0c;我意外地从一个程序员变成了一个 AI 资讯届的“网红”&#xff0c;到年底时我在 X 平台的阅读量超过 1 亿&#xff0c;微博上的阅读量则超过 10 亿&#xff0c;很多人通过我的微博或者 X 了解最新的 AI 资讯、教程和 Pro…

快速排序(三)——hoare法

目录 ​一.前言 二.快速排序 hoare排法​ 三.结语 一.前言 本文给大家带来的是快速排序&#xff0c;快速排序是一种很强大的排序方法&#xff0c;相信大家在学习完后一定会有所收获。 码字不易&#xff0c;希望大家多多支持我呀&#xff01;&#xff08;三连&#xff0b;关…

【GitHub项目推荐--智能家居项目】【转载】

如果你具备硬件、软件知识&#xff0c;这个项目肯定符合你的胃口。 物美智能是一套软硬件结合的开源项目&#xff0c;该系统可助你快速搭建自己的智能家居系统。你可以学习到设备的集成和软硬件交互。 PC 端或者手机与服务端通信&#xff0c;单片机可以接受遥控设备和服务器的…

【iOS】——基于Vision Kit框架实现图片文字识别

文章目录 前言一、文本识别的分类二、实现步骤1.导入Vision Kit框架2.创建请求处理器3.在请求处理器中设置文字识别功能4.将图片添加到请求处理器中5.发起文字识别请求6.处理识别结果 三、运行结果测试1.纯英文环境2.中英文混合环境 前言 根据苹果的官方文档&#xff0c;Visio…

flutter 实现定时滚动的公告栏的两种不错方式

相同的部分 自定义一个类继承StatefulWidget 所有公告信息存放在list里 第一种 scrollControllerAnimatedContainer 逻辑如下 我们可以发现启动了一个timer计时器计时5秒&#xff0c;hasClients检查其目标对象&#xff08;我们用的是listview&#xff09;是否被渲染&#x…