下面是实现的基本思路:
先实现最简单的 mini-react
在浏览器中展示一个简单的字符串 "app"。
index.html
代码如下:
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 操作:
js
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
文件更新如下:
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
文件更新如下:
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
文件更新如下:
javascript
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.js
和 ReactDOM.js
文件中,然后再添加一个 App.js
文件。
目录结构如下:
css
├─ App.js
├─ index.html
├─ main.js
├─ core
| ├─ React.js
| └- ReactDOM.js
React.js
文件:
javascript
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
文件:
javascript
import React from "./React.js";
const ReactDOM = {
createRoot(container) {
return {
render(App) {
return React.render(App, container);
},
};
},
};
export default ReactDOM;
App.js
文件:
javascript
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
文件:
javascript
import ReactDOM from "./core/ReactDOM.js";
import App from "./App.js";
ReactDOM.createRoot(document.getElementById("root")).render(App);
这时候,代码结构和 React 官方的 API 比较接近了。 按照文章开头的实现思路,将大任务拆解为小任务,更清晰的去完善代码。 当然,还有许多需要继续完善的,可以带着问题继续优化代码,例如支持jsx的写法以及dom树大的时候渲染卡顿需要实现任务调度器等
继续努力💪