mini-react 一、createElement和render

首先感谢崔学社

经过7天的学习跟着阿崔通过拆分任务,小步迭代,完成了一个mini-react,了解了Fiber、Reconcile等执行原理。

Day1

1. 实现最简 mini-react

在根目录下创建 index.htmldiv#root 为根节点,在浏览器中通过module导入模块main.js

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>mini-react</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="main.js"></script>
  </body>
</html>

根目录下main.js,导入ReactDomApp

// /main.js
import ReactDom from "./core/ReactDom.js";
import App from "./App.js";

ReactDom.createRoot(document.querySelector("#root")).render(App);

根目录下 App.js

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

const App = React.createElement("div", { id: "app" }, "hello world");

export default App;

根目录下 /core/React.js

createElement 接收三个参数,type用于判断元素类型和创建元素用,props传入id,class等attribute,children传入一个或多个子元素。

// /core/React.js
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) => {
        return typeof child === "string" ? createTextNode(child) : child;
      }),
    },
  };
};

React.createElement("div", { id: "app" }, "hello world") 返回的对象:

{
  "type": "div",
  "props": {
      "id": "app",
      "children": [
          {
              "type": "TEXT_ELEMENT",
              "props": {
                  "nodeValue": "hello world",
                  "children": []
              }
          }
      ]
  }
}

render接收两个参数,el是通过createElement创建出来的,container就是dom节点。

通过el.type 判断是创建文本节点还是创建元素标签。

循环将props中key的值,对应赋值给dom[key]。

单独处理children,递归处理子节点。

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];
    }
  });

  const children = el.props.children;
  children.forEach((child) => {
    render(child, dom);
  });

  container.append(dom);
}

core/React.js 完整代码:

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) => {
        return typeof child === "string" ? createTextNode(child) : child;
      }),
    },
  };
};

function createTextNode(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  };
};

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];
    }
  });

  const children = el.props.children;
  children.forEach((child) => {
    render(child, dom);
  });

  container.append(dom);
}

const React = {
  render,
  createElement,
};

export default React;

根目录下 /core/ReactDom.jscreateRoot 传入一个容器并返回render,render中接收createElement创建的Appcontainer

import React from "./React.js";

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

export default ReactDom;

在根目录通过http-server 启动:

到这里我们就实现了最简版本

2. 引入vite,使用JSX

在根目录下创建vite项目

pnpm create vite vite-runner --template vanilla

将之前的/core/React.js /core/ReactDom.js /main.js /App.js /index.html 依次复制到vite-runner目录下

main.jsApp.js 文件改为 main.jsx App.jsx

// /vite-runner/main.jsx
import ReactDom from "./core/ReactDom.js";
import App from "./App.jsx";

ReactDom.createRoot(document.querySelector("#root")).render(App);

别忘了修改index.html中的引用

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

这样我们就可以通过vite启动项目了,别忘了在vite-runner下安装依赖

cd vite-runner 

pnpm dev

仓库地址:https://github.com/zhuzhux/mini-react