上周参与了(催学社 (cuixueshe.com))的 《mini-react 游戏副本》 课程,老师喜欢通过对问题的拆分及小步骤的开发方式来授课,展现出完整的解决编程问题背后的思考过程。这也是对我收获最大的地方。
Day 1
01 实现最简 mini-react
小步快步,在浏览器显示一个 Hello, mini-React!
index.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
代码
const dom = document.createElement("div");
dom.id = "app";
document.querySelector("#root").append(dom);
const textNode = document.createTextNode("");
textNode.nodeValue = "Hello, mini-React!";
dom.append(textNode);
很简单的 dom 操作。这里的注意点就是document.createTextNode("")
创建了一个空的文本节点 textNode
,通过设置 textNode.nodeValue = "Hello, mini-React!"
,将文本节点的值设置为 “Hello, mini-React!”。
为什么要用 createTextNode
来添加文本节点?innerText不行吗?
createTextNode
创建一个文本节点对象,可以保留对它们的引用,以便有选择地从元素中删除文本,而不删除构成该对象的其他文本。这样的文本的操作在逻辑上更加清晰,因为它是作为一个节点来处理的。这在处理复杂的 dom 结构或者动态内容时可能会更有助于代码的组织和理解。
02 引入虚拟 dom (vdom)
现在使用 vdom
来表达 dom
结构,例如要实现如下的一个dom 结构,要怎么处理呢?
const el = {
type: 'div', // 元素类型,可以是HTML标签名或React组件
props: {
id: 'app', // 元素的属性,如class、id等
children: [ // 子节点
{
type: 'TEXT_ELEMENT',
props: {
nodeValue: 'Hello, mini-React!',
children: []
}
}
]
}
}
我们可以分成两个方法,一个是创建元素节点的 createElement
方法,和创建文本的节点的createTextNode
方法。
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;
}),
},
};
}
那有了这些,就差在页面上显示出来了,现在来实现 render
方法。
/**
* 渲染函数,将虚拟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);
}
使其尽量和React 的写法一致,我们把render
封装成ReactDOM
的调用形式
const ReactDOM = {
createRoot(container) {
return {
render(App) {
return render(App, container);
},
};
},
};
// 使用
const App = createElement(
"div",
{ id: "app" },
"Hello, mini-React!",
createElement("p", { id: "world" }, "Hello, world!")
);
ReactDOM.createRoot(document.getElementById("root")).render(App);
现在用 Visual Studio Code 的 Live Server 跑起来看看吧。
O K 😃 !
下面我们来重构一下代码,先将main.js
中的方法抽离到core目录下的 React.js
和 ReactDOM.js
,然后再添加一个App.js
文件。
先看目录结构
├─ App.js
├─ index.html
├─ main.js
├─ core
| ├─ React.js
| └- ReactDOM.js
React.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;
}),
},
};
}
/**
* 渲染函数,将虚拟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
文件
import React from "./React.js";
const ReactDOM = {
createRoot(container) {
return {
render(App) {
return React.render(App, container);
},
};
},
};
export default ReactDOM;
App.js
文件
import React from "./core/React.js";
const App = React.createElement(
"div",
{ id: "app" },
"Hello, mini-React!",
React.createElement("p", { id: "world" }, "Hello, world!")
);
export default App;
main.js
文件
import ReactDOM from "./core/ReactDOM.js";
import App from "./App.js";
ReactDOM.createRoot(document.getElementById("root")).render(App);
到这里就与React官方的API 写法差不多了。
03 使用JSX
使用vite
来构建一个空项目
pnpm create vite mini-react --template vanilla
pnpm i
将多余文件删除,只保留 package.json
index.html
main.js
先将main.js
和App.js
文件名修改成 main.jsx
和 App.jsx
。要注意 index.html
中的引入文件名也要修改哦。
修改App.jsx
内容
import React from "./core/React.js";
const App = <div id="app">Hello, mini-React!</div>
export default App;
运行项目 pnpm run dev
会发现居然能直接显示出来
为什么会直接显示出来呢?按正常理解不是应该通过 babel
来编译吗?
因为 Vite(功能 | Vite 官方中文文档 (vitejs.dev)) 对
.jsx
和.tsx
文件是开箱即用。JSX 的转译是通过 esbuild 来实现的。
今天就先学习到这里。