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

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

下面是实现的基本思路:

在这里插入图片描述

先实现最简单的 mini-react

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

index.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 操作:

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 文件更新如下:

// 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 文件更新如下:

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 文件更新如下:

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 文件。

目录结构如下:

├─ App.js
├─ index.html
├─ main.js
├─ core
|  ├─ React.js
|  └- ReactDOM.js

React.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;
            }),
        },
    };
}

/** 
 * 渲染函数,将虚拟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 文件:

import React from "./React.js";

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

export default ReactDOM;

App.js 文件:

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 文件:

import ReactDOM from "./core/ReactDOM.js";
import App from "./App.js";

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

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