手摸手带你实现最简单的mini-react

大家好,我是小左。

本文是 mini-react 专栏第 2 篇,实现最简单的mini-react。

vite创建react

先来看一下 Vite 脚手架创建的 React 项目,终端里输入创建指令,

pnpm create vite

选择 React,然后选择最简单的 JavaScript 即可。

进入项目,src 下查看入口文件 main.jsx,代码如下:

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

以上代码中,react 的入口文件,ReactDOM有一个createRoot方法接收一个根节点作为参数,在根节点上渲染App视图。

修改 App.jsx 代码如下:

function App() {
  return <div>hi, mini-react</div>;
}
export default App;

最终在页面上呈现hi, mini-react,这就是本篇文章需要实现的效果,同时需要实现满足 React 相同的API。

原生JS实现页面渲染

框架的能力最底层的实现还是依托 JS,那通过原生 JS 如何实现在页面上显示hi, mini-react

首先在 index.html 中确定一个idrootdiv作为根节点。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>mini-react</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="./main.js"></script>
  </body>
</html>

创建div容器

通过document.createElement创建 dom 节点,

const dom = document.createElement("div");
dom.id = "app";
document.querySelector("#root").append(dom);

创建文本节点

通过document.createTextNode创建文本节点,

const textNode = document.createTextNode("");
textNode.nodeValue = "hi,mini-react";
dom.append(textNode);

在页面中就可以看到文本显示hi,mini-react

抽离虚拟节点

无论是 React 还是 Vue,其内部实现都有借助虚拟节点的技术。所谓虚拟节点就是 JS 对象,是对 dom节点的数据抽象。

直接定义

例如,上面创建的idappdom,抽离的虚拟节点可以是:

const textEl = {
  type: "TEXT_ELEMENT",
  props: {
    nodeValue: "hi,mini-react",
    children: [],
  },
};
const el = {
  type: "div",
  props: {
    id: "app",
    children: [textEl],
  },
};

虚拟节点结合上面原生 JS 实现的代码,进行关键信息的替换,

const dom = document.createElement(el.type);
dom.id = el.props.id;
document.querySelector("#root").append(dom);

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

这算是进行了一次小重构,再次来到页面中进行验证,文本正常显示。

函数创建

虚拟节点的实现不会是这样直接定义的,一般是通过函数创建生成的,根据两种类型的虚拟节点封装对应的函数。

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

有了对应的创建虚拟节点的函数方法,之前直接定义的虚拟节点el, textEl就可以删除,换成函数调用的形式生成。

const textEl = createTextNode('hi,mini-react')
const el = createElement('div', {id: 'app'}, textEl)

渲染函数

查看整个 main.js 的代码,在创建 dom 和文本节点这儿,有重复地方,可以尝试将这儿的逻辑抽离,单独封装成一个 render 函数,

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

render函数中处理 3 件事:

  1. 创建 dom
  2. 处理 props
  3. 递归处理子节点

模拟React API

再次回到 Vite 创建的 React 应用中,入口文件代码如下:

ReactDOM.createRoot(document.getElementById('root')).render(<App />)

按照对象格式进行创建ReactDOM

const ReactDOM = {
  createRoot(container) {
    return {
      render(el) {
        render(el, container);
      },
    };
  },
};

ReactDOM.createRoot(document.getElementById('root')).render(el)

以上代码中,可以发现已经和原本的 React 入口文件很相似了,唯一不同就是App,是因为还没有实现function component,该功能在后续文章会介绍实现。

按照原本 React 的文件导入,进行代码抽离到不同文件中。

新建 core 文件夹,core/react-dom.js 代码如下:

import React from "./react.js";
const ReactDOM = {
  createRoot(container) {
    return {
      render(el) {
        React.render(el, container);
      },
    };
  },
};
export default ReactDOM;

core/react.js 代码如下:

function createTextNode(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  };
}
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) => {
        return typeof child === "string" ? createTextNode(child) : child;
      }),
    },
  };
}
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);
}
export default {
  render,
  createElement,
};

App.js 代码如下:

import React from "./core/react.js";
const App = React.createElement("div", { id: "app" }, "App");
export default App;

入口文件 main.js 代码如下:

import ReactDOM from "./core/react-dom.js";
import App from "./App.js";

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

至此,查看页面中文本渲染正常,最后的入口文件代码和最初的预期一致。

最后

注:本文首发微信公众号【前端一起学】,里面有持续更新的Vue源码实战专栏,Electron实战,Three.js入门教程等,还有更多前端基础知识超详细总结,欢迎关注。

原文链接:https://juejin.cn/post/7327831782703185971 作者:wendZzoo

(0)
上一篇 2024年1月25日 上午11:15
下一篇 2024年1月25日 下午4:05

相关推荐

发表回复

登录后才能评论