实现简易的 mini-react,并在页面中呈现内容
下面是实现的基本思路:
先实现最简单的 mini-react
在浏览器中展示一个简单的字符串 “app”。
<!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>
const dom = document.createElement("div"); dom.id = "app"; document.querySelector("#root").append(dom); const textNode = document.createTextNode(""); textNode.nodeValue = "app"; dom.append(textNode);
这里使用了
接下来,引入vdom概念,对代码进行优化。
引入虚拟 DOM(vdom)
第一步,将vdom写死以及dom渲染也写死
// 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渲染还是写死
此时可以抽取出两个函数,一个是创建元素节点的
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都改成动态生成,这时候需要添加
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);
现在运行下项目,查看效果
最后,对代码结构进行整理。将
目录结构如下:
├─ App.js ├─ index.html ├─ main.js ├─ core | ├─ React.js | └- ReactDOM.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;
import React from "./React.js"; const ReactDOM = { createRoot(container) { return { render(App) { return React.render(App, container); }, }; }, }; export default ReactDOM;
import React from "./core/React.js"; const App = React.createElement( "div", { id: "app" }, "app", React.createElement("p", { id: "text" }, "Hello, App!") ); export default App;
import ReactDOM from "./core/ReactDOM.js"; import App from "./App.js"; ReactDOM.createRoot(document.getElementById("root")).render(App);
这时候,代码结构和 React 官方的 API 比较接近了。
按照文章开头的实现思路,将大任务拆解为小任务,更清晰的去完善代码。
当然,还有许多需要继续完善的,可以带着问题继续优化代码,例如支持jsx的写法以及dom树大的时候渲染卡顿需要实现任务调度器等
继续努力吧??