从零到一实现最最最基本的 mini-react
最最最基本,指乜都无,只有数据展示功能。
催学社 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 抽取标签创建流程
由上一小节可以看到,完整的标签创建过程为:
- 创建 DOM 节点
- 普通标签 =>
document.createElement(vnode.type)
- 文本节点 =>
document.createTextNode('')
- 普通标签 =>
- 赋值 props
- 挂载自身到容器(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。
-
将 js 文件后缀名改为
.jsx
- 原因:jsx 愈发只能在
.jsx
文件中使用
- 原因:jsx 愈发只能在
-
将
App
的写法改为 jsx 写法const App = <div id="app">app</div>;
-
创建
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
的格式
- jsx 语法解析自动调用
1.3.6 实现 function component
- 将
App
变量改写const App = function () { return <div id="app">app</div>; };
- 界面不报错也不显示,打印
render
函数中的vnode
,可见{ props: {children: []}, type: ∫ () }
- type 为函数,其返回值为原来的
App
- type 为函数,其返回值为原来的
- 在
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 />);
转载自:https://juejin.cn/post/7352438029200719891