实现简易的 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.jsReactDOM.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树大的时候渲染卡顿需要实现任务调度器等

继续努力💪

相关推荐
灵感__idea3 小时前
Hello 算法:贪心的世界
前端·javascript·算法
killerbasd6 小时前
牧苏苏传 我不装了 4/7
前端·javascript·vue.js
橘子编程7 小时前
JavaScript与TypeScript终极指南
javascript·ubuntu·typescript
叫我一声阿雷吧8 小时前
JS 入门通关手册(45):浏览器渲染原理与重绘重排(性能优化核心,面试必考
javascript·前端面试·前端性能优化·浏览器渲染·浏览器渲染原理,重排重绘·reflow·repaint
大家的林语冰8 小时前
《前端周刊》尤大开源 Vite+ 全家桶,前端工业革命启动;尤大爆料 Void 云服务新产品,Vite 进军全栈开发;ECMA 源码映射规范......
前端·javascript·vue.js
jiayong239 小时前
第 8 课:开始引入组合式函数
前端·javascript·学习
天若有情6739 小时前
【C++原创开源】formort.h:一行头文件,实现比JS模板字符串更爽的链式拼接+响应式变量
开发语言·javascript·c++·git·github·开源项目·模版字符串
yuki_uix10 小时前
重排、重绘与合成——浏览器渲染性能的底层逻辑
前端·javascript·面试
止观止10 小时前
拥抱 ESNext:从 TC39 提案到生产环境中的现代 JS
开发语言·javascript·ecmascript·esnext
时寒的笔记11 小时前
js逆向7_案例惠nong网
android·开发语言·javascript