实现简易的 mini-react,并在页面中呈现内容

下面是实现的基本思路:

先实现最简单的 mini-react

在浏览器中展示一个简单的字符串 "app"。

index.html 代码如下:

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>

main.js 实现了简单的 DOM 操作:

js 复制代码
const dom = document.createElement("div");
dom.id = "app";
document.querySelector("#root").append(dom);

const textNode = document.createTextNode("");
textNode.nodeValue = "app";
dom.append(textNode);

这里使用了 document.createTextNode("") 创建一个空的文本节点 textNode,然后通过 textNode.nodeValue = "app" 将文本节点的值设置为 "app",这样就可以在页面上就可以看到"app"了

接下来,引入vdom概念,对代码进行优化。

引入虚拟 DOM(vdom)

第一步,将vdom写死以及dom渲染也写死 main.js 文件更新如下:

js 复制代码
// type props children
const textEl = {
    type: "TEXT_ELEMENT",
    props: { nodeValue: "app", children: [] }
}
const el = {
    type: "div",
    props: {
        id: "app", children: [textEll]
    }
}
const dom = document.createElement(el.type);
dom.id = el.props.iddocument.querySelector("#root").append(dom);
const textNode = document.createTextNode("");
textNode.nodeValue = textEl.props.nodeValuedom.append(textNode)

此时在页面上也可以显示字符串'app',接着继续优化代码,向官方API的形式靠拢

第二步,将vdom改成动态生成,dom渲染还是写死 此时可以抽取出两个函数,一个是创建元素节点的 createElement,另一个是创建文本节点的 createTextNode; main.js 文件更新如下:

js 复制代码
function createTextNode(text) {
    return {
        type: "TEXT_ELEMENT",
        props: {
            nodeValue: text,
            children: []
        },
    }
}
function createElement(type, props, ...children) {
  return {
    type: type,
    props: {
      ...props,
      children: children.map((child) => {
        // 如果是字符串,则处理成文本节点
        return typeof child === "string" ? createTextNode(child) : child;
      }),
    },
  };
}

const textEl = createTextNode("app")
const App = createElement("div", { id: "app" }, createTextNode("app"));
const dom = document.createElement(App.type);
dom.id = App.props.id;
document.querySelector("#root").append(dom);
const textNode = document.createTextNode("");
textNode, nodeValue = textEl.props.nodeValue;
dom.append(textNode)

第三步,将vdom以及dom都改成动态生成,这时候需要添加 render 函数;同时为了更接近 React 的写法,将 render 封装成 ReactDOM 的调用形式, main.js 文件更新如下:

javascript 复制代码
function createTextNode(text) {
    return {
        type: "TEXT_ELEMENT",
        props: {
            nodeValue: text,
            children: []
        },
    }
}
function createElement(type, props, ...children) {
  return {
    type: type,
    props: {
      ...props,
      children: children.map((child) => {
        // 如果是字符串,则处理成文本节点
        return typeof child === "string" ? createTextNode(child) : child;
      }),
    },
  };
}
/** 
 * 渲染函数,将虚拟DOM元素渲染到实际DOM容器中 
 * @param {object} el - 虚拟DOM元素 
 * @param {Element} container - 实际要添加进DOM容器 
 */
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];
        }
    });

    // 递归渲染子节点
    el.props.children.forEach((child) => {
        render(child, dom);
    });

    // 将DOM节点添加到容器中
    container.append(dom);
}
const ReactDOM = {
    createRoot(container) {
        return {
            render(App) {
                return render(App, container);
            },
        };
    },
};

// 使用
const App = createElement(
    "div",
    { id: "app" },
    "app",
    createElement("p", { id: "text" }, "Hello, App!")
);

ReactDOM.createRoot(document.getElementById("root")).render(App);

现在运行下项目,查看效果

最后,对代码结构进行整理。将 main.js 中的方法抽取到 core 目录下的 React.js 和 ReactDOM.js 文件中,然后再添加一个 App.js 文件。

目录结构如下:

css 复制代码
├─ App.js
├─ index.html
├─ main.js
├─ core
|  ├─ React.js
|  └- ReactDOM.js

React.js 文件:

javascript 复制代码
function createTextNode(text) {
    return {
        type: "TEXT_ELEMENT",
        props: {
            nodeValue: text,
            children: [],
        },
    };
}

function createElement(type, props, ...children) {
    return {
        type: type,
        props: {
            ...props,
            children: children.map((child) => {
                // 如果是字符串,则处理成文本节点
                return typeof child === "string" ? createTextNode(child) : child;
            }),
        },
    };
}

/** 
 * 渲染函数,将虚拟DOM元素渲染到实际DOM容器中 
 * @param {object} el - 虚拟DOM元素 
 * @param {Element} container - 实际要添加进DOM容器 
 */
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];
        }
    });

    // 递归渲染子节点
    el.props.children.forEach((child) => {
        render(child, dom);
    });

    // 将DOM节点添加到容器中
    container.append(dom);
}

const React = { createElement, render };
export default React;

ReactDOM.js 文件:

javascript 复制代码
import React from "./React.js";

const ReactDOM = {
    createRoot(container) {
        return {
            render(App) {
                return React.render(App, container);
            },
        };
    },
};

export default ReactDOM;

App.js 文件:

javascript 复制代码
import React from "./core/React.js";

const App = React.createElement(
    "div",
    { id: "app" },
    "app",
    React.createElement("p", { id: "text" }, "Hello, App!")
);

export default App;

main.js 文件:

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

ReactDOM.createRoot(document.getElementById("root")).render(App);

这时候,代码结构和 React 官方的 API 比较接近了。 按照文章开头的实现思路,将大任务拆解为小任务,更清晰的去完善代码。 当然,还有许多需要继续完善的,可以带着问题继续优化代码,例如支持jsx的写法以及dom树大的时候渲染卡顿需要实现任务调度器等

继续努力💪

相关推荐
子兮曰18 小时前
Chrome 权限元素实战:144 与 151 的渐进增强写法
前端·javascript·chrome
强尼银手18 小时前
我花一天做了个格斗游戏原型,判定全对,然后被「美术」劝退了
前端·javascript
总要学点什么20 小时前
第98篇:三维特效-材质线效果——给一条线换十种材质,虚线的图案只填了一半
开发语言·前端·javascript·ecmascript·gis·webgl·材质
OpsEye20 小时前
混合云 AI 架构,如何统一做敏感内容拦截?
javascript·ai编程
Funlang1 天前
给机器写的 JavaScript 造个笼子
开发语言·javascript·ecmascript
古茗前端团队1 天前
由 key 引发的思考
前端·javascript·react.js
kyriewen1 天前
我扫了 422 个仓库,近一半 CI 在裸装依赖
前端·javascript·程序员
vipxieliang1 天前
JavaScript 迭代器与生成器:控制异步与惰性求值
前端·javascript
Hilaku1 天前
Event Loop 这篇你看完就不用再看别的了!
前端·javascript·程序员
晓得迷路了1 天前
栗子前端技术周刊第 153 期 - Vitest 5.1、Remix v3.2、Node.js 27 Alpha...
前端·javascript·vite