不只是mini-react第一节:实现最简单mini-react

项目总结构:
复制代码
├─ 📁core
│  ├─ 📄React.js
│  └─ 📄ReactDom.js
├─ 📁node_modules
├─ 📁tests
│  └─ 📄createElement.spec.js
├─ 📄App.js
├─ 📄index.html
├─ 📄main.js
├─ 📄package-lock.json
├─ 📄package.json
└─ 📄pnpm-lock.yaml
原生js怎么写?
javascript 复制代码
const dom = document.createElement("div")
dom.id="app"
document.querySelector('#root').append(dom)

const textNode = document.createTextNode("")
textNode.nodeValue = "app"
dom.append(textNode)
为什么需要虚拟dom?

通过以上原生写法可以看出,虚拟dom可以简化开发,使代码更加框架、结构化,清晰可读易于维护。

框架层面:频繁的dom操作会一直导致浏览器重排和重绘,会严重影响性能。

原生层面:相对于原生层面虚拟dom对性能提示微乎其微,并且在复杂情况会损耗性能。

当然虚拟dom还有个非常重要的作用就是跨端运行。

面试怎么答?

  • 减少浏览器重排和重绘(框架层面、原生层面)
  • 跨平台运行,不局限于浏览器
  • 减少心智负担,提高开发效率

当然不能干巴巴的把这几点甩出去,记得拓展。

极简版React内核

此处代码实现了一个极简的创建虚拟dom和虚拟dom转真实dom

/core/React.js

javascript 复制代码
//创建文本对象虚拟dom
function createTextNode(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  };
}

//创建虚拟dom对象
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) => {
        return typeof child === "string" ? createTextNode(child) : child;
      }),
    },
  };
}

//渲染器,vdom->tdom
function render(el, container) {
  const dom =
    el.type === "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(el.type);

  // id class
  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;

可以看到以上代码中createElement函数就实现了一个极简版的虚拟dom,里面有三个元素分别是:

  • type:类型
  • props:传入的值
  • children:子节点

render函数则实现了一个极简版的渲染器,用于将虚拟dom转化成真实dom,传入的值分别是:

  • el:虚拟dom对象
  • container:具体在哪个真实dom节点渲染
创建虚拟dom对象

/App.js

通过上面定义的createElement函数来创建一个虚拟dom对象app并将其导出

javascript 复制代码
import React from './core/React.js';
const App = React.createElement("div", { id: "app" }, "hi- ", "mini-react");
export default App
极简React渲染组件

定义了一个createRoot函数,通过传入的渲染容器container和虚拟dom对象App来将虚拟dom元素渲染到真实dom上。

/core/ReactDom.js

javascript 复制代码
import React from "./React.js";
const ReactDOM = {
  createRoot(container) {
    return {
      render(App) {
        React.render(App, container);
      },
    };
  },
};

export default ReactDOM;
创建ReactDOM

/main.js

javascript 复制代码
import ReactDOM from "./core/ReactDom.js";
import App from "./App.js";

ReactDOM.createRoot(document.querySelector("#root")).render(App);
使用vitest单元测试验证

控制台运行npm i vitest下载依赖。

然后再package.json中自定义测试脚本

/package.json

javascript 复制代码
{
  "scripts": {
    "test": "vitest"
  },
  "devDependencies": {
    "vitest": "^1.1.3"
  }
}

然后创建tests文件夹并编写测试文件,具体路径于代码如下:

/tests/createElement.spec.js

javascript 复制代码
import React from "../core/React";
import { it, expect, describe } from "vitest";

describe("createElement", () => {
  it("props is null", () => {
    const el = React.createElement("div", null, "hi");

    expect(el).toMatchInlineSnapshot(`
      {
        "props": {
          "children": [
            {
              "props": {
                "children": [],
                "nodeValue": "hi",
              },
              "type": "TEXT_ELEMENT",
            },
          ],
        },
        "type": "div",
      }
    `)

  });

  it("should return element vdom", () => {
    const el = React.createElement("div", {id:"id"}, "hi");

    expect(el).toMatchInlineSnapshot(`
      {
        "props": {
          "children": [
            {
              "props": {
                "children": [],
                "nodeValue": "hi",
              },
              "type": "TEXT_ELEMENT",
            },
          ],
          "id": "id",
        },
        "type": "div",
      }
    `)

  });
});

创建完成后在控制台输入npm test即可运行测试脚本。

在上面代码中,describe用于创建测试组,每个it是测试组内的单个测试单元,而it内就是具体测试内容。

这里使用的是toMatchInlineSnapshot快照测试,用于比对el结果是否与快照函数toMatchInlineSnapshot内容一致,如果一致则测试通过。

总结

createRoot用于将虚拟dom渲染成真实dom

createElement用于创建虚拟dom对象

rendercreateRoot的内核

贴上main.js

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>

  <div id="root"></div>
  <script type="module" src="main.js"></script>
</body>

</html>
相关推荐
万少1 分钟前
HarmonyOS Next 弹窗系列教程(3)
前端·harmonyos·客户端
七灵微1 小时前
【后端】单点登录
服务器·前端
持久的棒棒君5 小时前
npm安装electron下载太慢,导致报错
前端·electron·npm
crary,记忆7 小时前
Angular微前端架构:Module Federation + ngx-build-plus (Webpack)
前端·webpack·angular·angular.js
漂流瓶jz8 小时前
让数据"流动"起来!Node.js实现流式渲染/流式传输与背后的HTTP原理
前端·javascript·node.js
SamHou08 小时前
手把手 CSS 盒子模型——从零开始的奶奶级 Web 开发教程2
前端·css·web
我不吃饼干8 小时前
从 Vue3 源码中了解你所不知道的 never
前端·typescript
开航母的李大8 小时前
【中间件】Web服务、消息队列、缓存与微服务治理:Nginx、Kafka、Redis、Nacos 详解
前端·redis·nginx·缓存·微服务·kafka
Bruk.Liu8 小时前
《Minio 分片上传实现(基于Spring Boot)》
前端·spring boot·minio
鱼樱前端9 小时前
Vue3+d3-cloud+d3-scale+d3-scale-chromatic实现词云组件
前端·javascript·vue.js