likes
comments
collection
share

从零到一实现最最最基本的 mini-react

作者站长头像
站长
· 阅读数 3

最最最基本,指乜都无,只有数据展示功能。

催学社 mini-rect 副本输出文章。

零、项目准备

  • node 版本: 20.11.0
  • 包管理工具:pnpm
    • 初始化: pnpm init
  • vite
    • 命令: pnpm add vite -D
    • 选择的原因:配置简单,适合个人项目。
    • 在 package.json 中配置脚本命令
      {
        "scripts": {
          "start": "vite"
        }
      }
      

一、虚拟 DOM

从原生 JS 实现到虚拟 DOM 实现。

1.1 原生代码

const container = document.querySelector("#root");
const appEl = document.createElement("div");
appEl.id = "app";
container.appendChild(appEl);

const textNode = document.createTextNode("");
textNode.nodeValue = "app";
appEl.appendChild(textNode);

1.2 React 代码

import React from "react";
import { createRoot } from "react-dom/client";

const App = () => {
  return <div id="app">app</div>;
};

const root = createRoot(document.getElementById("root"));
root.render(<App />);

1.3 搭建简单的 React API

1.3.1 抽象节点

根据已有的 appEl 元素与 textNode 节点,将其抽取成 jsx 的标签解析格式,即一个包含 type 和 props(props 必须存在 children 属性) 属性的对象。

const TextNode = {
  type: "ELEMENT_TEXT",
  props: {
    nodeValue: "app",
    children: []
  }
};
const App = {
  type: "div",
  props: {
    id: "app",
    children: [TextNode]
  }
};

1.3.2 使用抽象节点创建对应的标签

const TextNode = {
  type: "ELEMENT_TEXT",
  props: {
    nodeValue: "app",
    children: []
  }
};
const App = {
  type: "div",
  props: {
    id: "app",
    children: [TextNode]
  }
};
const container = document.querySelector("#root");
const appEl = document.createElement(App.type);
appEl.id = App.props.id;
container.appendChild(appEl);

const textNode = document.createTextNode("");
textNode.nodeValue = TextNode.props.nodeValue;
appEl.appendChild(textNode);

1.3.3 抽取标签创建流程

由上一小节可以看到,完整的标签创建过程为:

  1. 创建 DOM 节点
    • 普通标签 => document.createElement(vnode.type)
    • 文本节点 => document.createTextNode('')
  2. 赋值 props
  3. 挂载自身到容器(container)上
const TextNode = {
  type: "ELEMENT_TEXT",
  props: {
    nodeValue: "app",
    children: []
  }
};
const App = {
  type: "div",
  props: {
    id: "app",
    children: [TextNode]
  }
};

const container = document.querySelector("#root");
const appEl = render(App, container);
render(TextNode, App);

function render(vnode, container) {
  // 1. 创建 DOM 节点
  const dom =
    vnode.type === "ELEMENT_TEXT"
      ? document.createTextNode("")
      : document.createElement(vnode.type);

  // 2. 赋值 props
  for (const key in vnode.props) {
    if (key === "children") continue;
    dom[key] = vnode.props[key];
  }

  // 3. 挂载
  container.appendChild(dom);

  // 用于子节点的挂载
  return dom;
}

1.3.4 处理 props.children

由上一小节可以看出,[vnode].props.children 挂载到 vnode 对应的 dom 节点的操作,与 vnode 挂载到 container 是一样的,因此可以使用递归实现该流程。

const TextNode = {
  type: "ELEMENT_TEXT",
  props: {
    nodeValue: "app",
    children: []
  }
};
const App = {
  type: "div",
  props: {
    id: "app",
    children: [TextNode]
  }
};

const container = document.querySelector("#root");
render(App, container);

function render(vnode, container) {
  // 1. 创建 DOM 节点
  const dom =
    vnode.type === "ELEMENT_TEXT"
      ? document.createTextNode("")
      : document.createElement(vnode.type);

  // 2. 赋值 props
  for (const key in vnode.props) {
    if (key === "children") continue;
    dom[key] = vnode.props[key];
  }

  // 3. 处理 props.children
  vnode.props.children.forEach((child) => render(child, dom));

  // 4. 挂载
  container.appendChild(dom);
}

1.3.5 实现 jsx

此处夸一下 vite,已内置了 jsx,无需手动集成 babel。

  1. 将 js 文件后缀名改为 .jsx

    • 原因:jsx 愈发只能在 .jsx 文件中使用
  2. App 的写法改为 jsx 写法 const App = <div id="app">app</div>;

  3. 创建 React.js 文件,并引入 React 对象到 .jsx 文件中

    function createTextNode(text) {
      return {
        type: "ELEMENT_TEXT",
        props: {
          nodeValue: text,
          children: []
        }
      };
    }
    function createElement(type, props, ...children) {
      return {
        type,
        props: {
          ...props,
          children: children.map((child) =>
            typeof child === "object" ? child : createTextNode(child)
          )
        }
      };
    }
    
    const React = {
      createElement
    };
    
    export default React;
    
    • jsx 语法解析自动调用 React.createElement 方法
    • render 函数无法处理字符串,而 app 文本节点被 jsx 解析后为字符串格式,所以需要手动组装成前文中 TextNode 的格式

1.3.6 实现 function component

  1. App 变量改写
    const App = function () {
      return <div id="app">app</div>;
    };
    
  2. 界面不报错也不显示,打印 render 函数中的 vnode,可见
    {
      props: {children: []},
      type: ∫ ()
    }
    
    • type 为函数,其返回值为原来的 App
  3. render 函数中以 type 的类型,来区分到底是普通组件还是函数组件
    • 函数组件 => updateFunctionComponent(vnode, container)
      • 将其 vnode.type() 的返回值作为 child,再次进行 render 操作
    • 普通组件 => updateHostComponent(vnode, container)
      • 原来的 render 代码复制粘贴即可
function render(vnode, container) {
  const isFunctionComponent = vnode.type instanceof Function;
  if (isFunctionComponent) {
    updateFunctionComponent(vnode, container);
  } else {
    updateHostComponent(vnode, container);
  }
}

/**
 * 函数组件
 * @param {Object} vnode
 */
function updateFunctionComponent(vnode, container) {
  const chid = vnode.type();

  render(chid, container);
}

/**
 * 普通组件
 * @param {Object} vnode
 */
function updateHostComponent(vnode, container) {
  // 1. 创建 DOM 节点
  const dom =
    vnode.type === "ELEMENT_TEXT"
      ? document.createTextNode("")
      : document.createElement(App.type);

  // 2. 赋值 props
  for (const key in vnode.props) {
    if (key === "children") continue;
    dom[key] = vnode.props[key];
  }

  // 3. 处理 props.children
  vnode.props.children.forEach((child) => render(child, dom));

  // 4. 挂载
  container.appendChild(dom);
}

1.3.7 根据 React 代码拆分组织文件

  • React.js

    function createTextNode(text) {
      return {
        type: "ELEMENT_TEXT",
        props: {
          nodeValue: text,
          children: []
        }
      };
    }
    function createElement(type, props, ...children) {
      return {
        type,
        props: {
          ...props,
          children: children.map((child) =>
            typeof child === "object" ? child : createTextNode(child)
          )
        }
      };
    }
    
    function render(vnode, container) {
      const isFunctionComponent = vnode.type instanceof Function;
      if (isFunctionComponent) {
        updateFunctionComponent(vnode, container);
      } else {
        updateHostComponent(vnode, container);
      }
    }
    
    /**
     * 函数组件
     * @param {Object} vnode
     */
    function updateFunctionComponent(vnode, container) {
      const chid = vnode.type();
    
      render(chid, container);
    }
    
    /**
     * 普通组件
     * @param {Object} vnode
     */
    function updateHostComponent(vnode, container) {
      // 1. 创建 DOM 节点
      const dom =
        vnode.type === "ELEMENT_TEXT"
          ? document.createTextNode("")
          : document.createElement(vnode.type);
    
      // 2. 赋值 props
      for (const key in vnode.props) {
        if (key === "children") continue;
        dom[key] = vnode.props[key];
      }
    
      // 3. 处理 props.children
      vnode.props.children.forEach((child) => render(child, dom));
    
      // 4. 挂载
      container.appendChild(dom);
    }
    
    const React = {
      createElement,
      render
    };
    
    export default React;
    
  • ReactDom.js

    import React from "./React";
    
    const ReactDOM = {
      createRoot(container) {
        return {
          render(vnode) {
            React.render(vnode, container);
          }
        };
      }
    };
    
    export default ReactDOM;
    
  • App.jsx

    import React from "../../core/React";
    
    const App = function () {
      return <div id="app">app</div>;
    };
    
    export default App;
    
  • index.jsx

    import React from "../../core/React";
    import ReactDOM from "../../core/ReactDom";
    
    import App from "./App";
    
    const root = ReactDOM.createRoot(document.querySelector("#root"));
    root.render(<App />);