01-实现最简mini-react

39 阅读3分钟

目标:使用和react一样的api在页面渲染字符

先看下react的Api写法

  • main.jsx
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
​
ReactDOM.createRoot(document.querySelector("#root")).render(<App />);
  • App.jsx
function App() {
    return <div>app</div>
}
export default App;

开始我们的写法

  • 从最简单的版本开始

    • 新建my-react项目文件夹
  • 新建index.html

...
<body> 
    <div id="root"></div>
    <script type="module" src="./main.js"></script>
</body>
...
  • main.js
console.log('fuck')
  • 使用http-server运行起来确保能打印成功

开始写main.js,实现最简单的app展示

  • 第一版main.js
// 1. 创建 div 元素节点
const dom = document.createElement('div');
// 创建了一个 <div> HTML 元素(此时尚未添加到文档中)
// 此时 dom 的内存结构:<div></div>// 2. 设置元素 ID
dom.id = "app";
// 现在 dom 变为:<div id="app"></div>// 3. 将元素添加到 DOM 树
document.querySelector('#root').append(dom);
// 在文档中查找 id="root" 的元素,将新建的 div 作为子元素追加进去
// 假设页面存在 <div id="root"></div>,操作后结构变为:
// <div id="root">
//   <div id="app"></div>
// </div>// 4. 创建空的文本节点
const textNode = document.createTextNode("");
// 创建了一个空的文本节点(此时内容为空字符串)// 5. 设置文本内容
textNode.nodeValue = "app";
// 将文本节点的值设置为 "app"
// 此时 textNode 的内容是 "app"// 6. 添加文本到元素
dom.appendChild(textNode);
// 将文本节点追加到 id="app" 的 div 中
// 最终 DOM 结构:
// <div id="root">
//   <div id="app">app</div>
// </div>
  • 第二个版本,进一步使用v-dom优化main.js创建app
// 注意:type props children属性
const textEl = {
    type: "TEXT_ELEMENT",
    props: {
        nodeValue: "app",
        children: []
    }
}
const el = {
    type: "div",
    props: {
        id: "app",
        children: [
            textEl
        ]
    }
}
​
const dom = document.createElement(el.type);
dom.id = el.props.id;
document.querySelector('#root').append(dom);
const textNode = document.createTextNode("");
textNode.nodeValue = textEl.props.nodeValue;
dom.appendChild(textNode)

继续完善实现动态创建

function createTextNode(text) {
    return {
        type: "TEXT_ELEMENT",
        props: {
            nodeValue: text,
            children: []
        }
    }
}
​
function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children
        }
    }
}
​
const textEl = createTextNode("app")
const App = createElement("div", { id: "app" }, textEl);
​
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.appendChild(textNode)

继续优化代码,编写render函数

// 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.appendChild(textNode)// 将以上两段代码优化成render函数
function render(el, container) {
    // 1. 判断元素类型是文本还是其他类型, 如果是文本类型则创建文本节点,否则创建元素节点
    const dom = el.type === "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(el.type);
    
    // 2.设置元素的属性值为 el.props 中的属性
    Object.keys(el.props).forEach((key) => {
        if (key !== "children") {
            dom[key] = el.props[key];
        }
    });
​
    // 递归的处理子元素
    const children = el.props.children
    children.forEach((child) => {
        render(child, dom);
    });
​
    // 3. 将创建的元素节点添加到指定的容器中
    container.appendChild(dom);
}
​
const textEl = createTextNode("app")
const App = createElement("div", { id: "app" }, textEl);
render(App, document.querySelector("#root"));

继续优化直接给app,而不是textNode

// 实现以下效果
const App = createElement("div", { id: "app" }, 'app1', 'app2');
// 通过修改children实现
function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children: children.map(child => {return typeof child === "string"? createTextNode(child) : child})
        }
    }
}

整合api成react对应api

const ReactDOM = {
    createRoot(container) {
        return {
            render(App) {
                render(App, container);
            }
        }
    }
}
const App = createElement("div", { id: "app" }, 'aaa->', 'bbbb');
ReactDOM.createRoot(document.querySelector("#root")).render(App)

重构下代码分离业务代码和自己写的mini-react框架代码

  • core/react.js
function createTextNode(text) {
    return {
        type: "TEXT_ELEMENT",
        props: {
            nodeValue: text,
            children: []
        }
    }
}
​
​
function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children: children.map(child => {return typeof child === "string"? createTextNode(child) : child})
        }
    }
}
​
function render(el, container) {
    // 1. 判断元素类型是文本还是其他类型, 如果是文本类型则创建文本节点,否则创建元素节点
    const dom = el.type === "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(el.type);
​
    // 2.设置元素的属性值为 el.props 中的属性
    Object.keys(el.props).forEach((key) => {
        if (key !== "children") {
            dom[key] = el.props[key];
        }
    });
​
    // 递归的处理子元素
    const children = el.props.children
    children.forEach((child) => {
        render(child, dom);
    });
​
    // 3. 将创建的元素节点添加到指定的容器中
    container.appendChild(dom);
}
​
const textEl = createTextNode("app")
​
const React = {
    render,
    createElement,
}
​
export default React
  • core/ReactDom.js
import React from './React.js';
const ReactDOM = {
    createRoot(container) {
        return {
            render(App) {
                React.render(App, container);
            }
        }
    }
}
​
export default ReactDOM
  • App.js
import React from './core/React.js';
​
const App = React.createElement("div", { id: "app" }, 'aaa->', 'bbbb');
  • main.js
import ReactDOM from './core/ReactDom.js'
import App from './App.js'ReactDOM.createRoot(document.querySelector("#root")).render(App)  

遗留问题

  • 如何使用JSX