【react框架】如何手写一个超级mini的React,学完后对框架的理解也会更进一步

news2024/11/17 9:56:45

文章目录

  • 前言
  • 起步工作
  • 先看虚拟dom长啥样
  • 写个createElement
  • 写个render
  • 第一次试验
  • 了解Fiber
  • 写个Fiber化函数
  • 改造render
  • 未完待续...

前言

本篇文章只是对https://qcsite.gatsbyjs.io/build-your-own-react/网站内容,做的一个内容的压缩,结合上自己的理解与想法,告诉已经理解虚拟dom的人怎么去一步一步的写一个超级mini的React。

看完后,相信你对框架的理解也会更进一步。

有错误欢迎评论指出!以下“网站”一词指代上面的网站(绕口令哈哈)


起步工作

先自己手起一个初始的react项目,暂时不用管是什么版本的(我们起是18,网站里说的是16),不太重要。

npx create-react-app my-app

先看虚拟dom长啥样

在src下的index.js中打印:

const element = React.createElement(
  "div",
  { id: "box" },
  "盒子",
  React.createElement("a", { id: "link" }, "点击")
);

console.log("虚拟dom", element);

在这里插入图片描述

展开子项:

在这里插入图片描述

其实虚拟dom长啥样想必大家都已经知道了,我们只需要模拟出那个几个关键的属性即可。


写个createElement

可以看到通过React.createElement可以生成虚拟dom,那我们就模拟着写个简单的例子。

首先创建文件src/miniReact/createElement.js,然后写入:

function createElement(type, props, ...children) {
  return {
    type, // 节点类型
    props: {
      ...props, // 节点所有属性
      children: children.map((child) => // 这里你会疑问如果子级还有子级咋办?放心只是不在这里处理
        typeof child === "object" ? child : createTextElement(child)
      ),
    },
  };
}

// 专门用来创建普通标签元素的
function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT", // 自己定个特殊字符标识
    props: {
      nodeValue: text, // 文本内容
      children: [],
    },
  };
}

export default createElement;

可以拿上面React.createElement输入案例来看,把入参都放到我们写的函数里,是不是也是一样的。


写个render

在src下的index.js中,我们可以这样渲染出真实的dom在页面上:

const element = React.createElement(
  "div",
  { id: "box" },
  "盒子",
  React.createElement("a", { id: "link" }, "点击")
);

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(element);

可以看出是通过了render函数去做的真实dom转换,不过使用方式和网站上的16版本不太一样:

const container = document.getElementById("root");
ReactDOM.render(element, container);

咱们这里为了贴合网站的案例,也按照网站上的写一个render的简单实现。

创建文件src/miniReact/render.js,然后写入:

function render(element, container) {
  // 创建对应的真实dom对象
  const dom =
    element.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type);

  // 判断是否为非children属性
  const isProperty = (key) => key !== "children";
  // 把虚拟dom上的children属性赋值在真实dom对象上
  Object.keys(element.props)
    .filter(isProperty)
    .forEach((name) => {
      dom[name] = element.props[name];
    });

  // 这里对子级进行递归处理
  element.props.children.forEach((child) => render(child, dom));

  // 每一层级的dom节点挂载
  container.appendChild(dom);
}

export default render;

第一次试验

可以进行我们的第一个初步的试验了,看能不能替代原来的写法,首先创建src/miniReact/index.js,然后写入:

// 合并导出
import createElement from "./createElement";
import render from "./render";

export default { createElement, render };

这个文件就相当于ReactDOM。

好啦,我们在src下的index.js把我们写的miniReact替换掉原来的ReactDOM:

import miniReact from "./miniReact";

const element = miniReact.createElement(
  "div",
  { id: "box" },
  "盒子",
  miniReact.createElement("a", { id: "link" }, "点击")
);
const container = document.getElementById("root");
miniReact.render(element, container);

看看你的页面是不是正常渲染了!


了解Fiber

这个是为接下来的代码理解做的理论知识铺垫,一定要懂了!!!才好理解下面的代码!!!

这玩意网上讲的太复杂了,可以看我这篇大白话给你讲明白:【react框架】别把Fiber整得那么难理解,来参考下我是咋理解的,用大白话解释


写个Fiber化函数

知道fiber是个啥东东后,咱们可以写个fiber化的函数了,在src/miniReact/render.js里面加入:

// 创建真实dom放在单独一个函数里执行
function createDom(fiber) {
  const dom =
    fiber.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(fiber.type);
  const isProperty = (key) => key !== "children";
  Object.keys(fiber.props)
    .filter(isProperty)
    .forEach((name) => {
      dom[name] = fiber.props[name];
    });

  return dom;
}

// 节点fiber化:把每一个虚拟dom阶段fiber化,最后还要返回下一个节点,因为下一个节点在本次处理中是没有完全fiber化的
function performUnitOfWork(fiber) {
  // 1 先给自己创建真实dom
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }

  // 2 接下来就是理清指向:父级、子级、兄弟

  // 如果有父节点,那么要把这个fiber对象插入父节点下,要不怎么知道每个fiber之间的关系
  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom);
  }

  const elements = fiber.props.children; // 拿到所有子级们
  let index = 0;
  let prevSibling = null; // 上一个兄弟节点

  // 开始循环子级们
  while (index < elements.length) {
    const element = elements[index]; // 拿到每个子级

    const newFiber = { // 创建子级的fiber对象
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null,
    };

    if (index === 0) { // 第一个子级的处理,正常写入到child属性中
      fiber.child = newFiber;
    } else { // 其他的子级就用每个fiber子级的sibling属性记录上一个兄弟节点的指向
      prevSibling.sibling = newFiber;
    }

    prevSibling = newFiber; // 上一个兄弟节点不断移动至下一位
    index++;
  }

  // 3 这个函数要返回一个fiber出来,作为下一个单元小任务
  // 因为本节点已经完全fiber化了,所以可以看看有没有子节点,有的话继续fiber化
  if (fiber.child) {
    return fiber.child;
  }
  // 没有子节点了,就看看兄弟节点,不断的去找兄弟节点
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent; // 如果兄弟节点也没有了,返回到上一个节点
  }
}

第一次看可能有点懵逼,沉住气,慢慢看,你会有收获的。


改造render

知道要有fiber化的过程后,就是要改造render函数了,在src/miniReact/render.js里面改造。

首先我们要驱动fiber化函数,让它自动去遍历我们的虚拟dom,把每个阶段都fiber化:

let nextUnitOfWork = null; // 记录下一个单元任务,单元任务就是performUnitOfWork函数做的事情

// 单元任务轮番触发机制
function workLoop(deadline) { // 默认requestIdleCallback执行的时候会有参数传入
  let shouldYield = false; // 主线程是否空闲
  // 如果有下一个单元任务要执行并且主线程空闲
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork); // 执行当前单元任务并且拿到下一个单元任务
    shouldYield = deadline.timeRemaining() < 1; // 这个方式可以知道主线程是否空闲
  }
  requestIdleCallback(workLoop); // 当主线程空闲时触发,这个api不懂的话去MDN查下,这里就不赘述了
}

requestIdleCallback(workLoop); // 第一次启动

这里方便实现就用requestIdleCallback这个api了。

ok,这里就差不多了,我们接下来就只需要把第一个节点任务塞入到nextUnitOfWork变量里即可(这样workLoop就能正常工作了),这个事情就交给render:

function render(element, container) {
  nextUnitOfWork = {
    dom: container,
    props: {
      children: [element],
    },
  };
}

做完这一切,在页面上看是否一切正常,正常就表示改造成功!


未完待续…

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

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

相关文章

iPhone卫星通信SOS功能如何在灾难中拯救生命

iPhone上的卫星紧急求救信号功能在从毛伊岛野火中拯救一家人方面发挥了至关重要的作用。这是越来越多的事件的一部分&#xff0c;在这些事件中&#xff0c;iPhone正在帮助人们摆脱危及生命的情况。 卫星提供商国际通信卫星组织负责移动的高级副总裁Mark Rasmussen在接受Lifewir…

基于springboot+vue的论坛系统(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目介绍…

EdgeOneToMinIO

省流 使用MinIO作为EdgeOne的源站。 背景介绍 项目中需要一个兼容S3协议的对象存储服务&#xff0c;腾讯云的COS虽然也兼容S3协议&#xff0c;但是也只是支持简单的上传下载&#xff0c;对于上传的时候同时打标签这种需求&#xff0c;就不兼容S3了。所以决定自建一个对象存储…

[技术杂谈]macOS上todesk无法远程操作鼠标键盘

远程到被控Mac后能看到画面&#xff0c;鼠标键盘操作无反应 远程后发现画面显示正常&#xff0c;但是键盘和鼠标的操作没有响应 可能是辅助功能没有勾选ToDesk_Session的权限。 可按以下步骤操作&#xff1a; 1> 在左上角点击苹果图标&#xff0c;选择“系统偏好设置” …

Spring练习-29(角色添加操作)

1、点击新建 2、添加角色名称和信息之后&#xff0c;添加到数据库当中&#xff0c;与此同时你的页面要跳回 3、跳回角色列表页面 4、那是入口&#xff0c;入口就是保存 5、当你保存到表单里就是提交呀&#xff01; 6、流程是我把数据封装给某个方法&#xff1a;------》封装给一…

【Leetcode】108. 将有序数组转换为二叉搜索树

一、题目 1、题目描述 给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。 高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。 示例1: 输入:nums = [-10,-3,0,5,9] 输出:[0,-3,9,-1…

C++实现字符串的逆置

目录 C和C的区别 【1】C对C的扩充 【2】C对C的兼容 第一个C程序 【1】hello world 【2】cout标准输出流对象 i&#xff09;介绍 ii&#xff09;运算 iii&#xff09;cout的使用 iv&#xff09;使用cout指定格式的输出 练习&#xff1a;1、输出斐波那契的前10项。 【3】…

Vuex从入门到精通

文章目录 前言概念使用场景 搭建vue环境创建文件传入配置项 基本使用初始化数据getters的使用map方法使用 模块化&#xff0c;命名空间目的开启命名空间组件中读取state数据组件中读取getters数据开启命名空间后&#xff0c;组件中调用dispatch开启命名空间后&#xff0c;组件中…

聊聊磁悬浮技术

目录 1.磁悬浮的概念 2.磁悬浮的原理 3.磁悬浮的应用领域 4.磁悬浮技术的发展趋势 1.磁悬浮的概念 磁悬浮&#xff08;Magnetic Levitation&#xff09;&#xff0c;简称磁浮&#xff0c;是一种利用磁场力使物体悬浮在空中的技术。它通过在物体上加上一个磁场&#xff0c;与…

三、SQL注入之报错注入

文章目录 1、 xpath语法&#xff08;1&#xff09;extractvalue&#xff08;2&#xff09;updatexml 2、concatrand()group by()导致主键重复 报错注入就是利用了数据库的某些机制&#xff0c;人为地制造错误条件&#xff0c;使得查询结果能够出现在错误信息中。这里主要介绍报…

SOLIDWORKS基准面介绍

SOLIDWORKS是一款广泛应用于机械设计领域的三维建模软件&#xff0c;其中基准面是在建模过程中必不可少的要素。本文将介绍什么是SOLIDWORKS基准面&#xff0c;以及它在设计中的作用。 SOLIDWORKS基准面是指在设计过程中用来确定草图绘制、特征创建的参考平面。 SOLIDWORKS基…

如何在 Ubuntu 中安装最新的 Python 版本

动动发财的小手&#xff0c;点个赞吧&#xff01; Python 是增长最快的主要通用编程语言。其原因有很多&#xff0c;例如其可读性和灵活性、易于学习和使用、可靠性和效率。 目前使用的 Python 有两个主要版本 – 2 和 3&#xff08;Python 的现在和未来&#xff09;&#xff1…

一篇搞懂TCP、HTTP、Socket、Socket连接池

前言&#xff1a;作为一名开发人员我们经常会听到HTTP协议、TCP/IP协议、UDP协议、Socket、Socket长连接、Socket连接池等字眼&#xff0c;然而它们之间的关系、区别及原理并不是所有人都能理解清楚&#xff0c;这篇文章就从网络协议基础开始到Socket连接池&#xff0c;一步一步…

微服务基础知识

文章目录 微服务基础知识一、系统架构的演变1、单体应用架构2、垂直应用架构3、分布式SOA架构&#xff08;1&#xff09;什么是SOA&#xff08;2&#xff09;SOA架构 4、微服务架构5、SOA和微服务的关系&#xff08;1&#xff09;SOA&#xff08;2&#xff09;微服务架构 二、分…

别再排长队了,食堂用餐轻松搞定!

随着科技的不断进步&#xff0c;学校食堂正逐步迎来智能化和数字化的转变。智慧收银系统作为这一转变的重要组成部分&#xff0c;为学校食堂管理带来了全新的可能性和机遇。 在这个数字化时代&#xff0c;智慧收银系统正成为学校食堂迈向未来的关键一步。 客户案例 湖南某大学…

电子器件系列46:气体放电管

C1566785_气体放电管(GDT)_B88069X5010S102_规格书_TDK气体放电管(GDT)规格书 气体放电管指作过电压保护用的避雷管或天线开关管一类&#xff0c;管内有二个或多个电极&#xff0c;充有一定量的惰性气体。气体放电管是一种间隙式的防雷保护元件&#xff0c;它用在通信系统的防雷…

Java详解编译型和解释型语言

在计算机的高级编程语言类型分为两种&#xff0c;分别是编译型和解释型&#xff0c;而Java既有编译型又有解释型 什么是编译型&#xff1f;什么是解释型&#xff1f; 字面上来说编译和解释都有‘翻译’的意思&#xff0c;而她们两个的区别是‘翻译’的时机不同&#xff0c;什…

Flask入门一 ——虚拟环境及Flask安装

Flask入门一 ——虚拟环境及Flask安装 在大多数标准中&#xff0c;Flask都算是小型框架&#xff0c;小到可以称为“微框架”&#xff0c;但是并不意味着他比其他框架功能少。Flask自开发伊始就被设计为可扩展的框架。Flask具有一个包含基本服务的强健核心&#xff0c;其他功能…

leetcode304. 二维区域和检索 - 矩阵不可变(java)

前缀和数组 二维区域和检索 - 矩阵不可变题目描述前缀和代码演示 一维数组前缀和 二维区域和检索 - 矩阵不可变 难度 - 中等 原题链接 - 二维区域和检索 - 矩阵不可变 题目描述 给定一个二维矩阵 matrix&#xff0c;以下类型的多个请求&#xff1a; 计算其子矩形范围内元素的总…

电脑录屏软件哪个好用又免费

在学习工作中或者生活中&#xff0c;经常会遇到需要录屏的情况&#xff0c;那么有哪些好用的录屏软件呢&#xff1f;今天就给大家分享3个好用的电脑录屏软件&#xff0c;有多种录屏模式可选&#xff0c;高清视频随便录制&#xff01; 1、Win10自带的录屏工具 Win10/Win11系统…