mini-react 1. 39行代码实现虚拟dom的渲染

98 阅读5分钟

1. React 原理

React 是一个用于构建用户界面的 JavaScript 库。它的核心原理包括以下几个关键概念:

  • 声明式编程:React 允许您以声明式的方式描述界面,您只需声明界面应该如何根据不同的状态展示,而无需关心具体的实现细节。
  • 组件化:React 的界面是由多个独立、可复用的组件构成的。每个组件管理自己的状态,并描述它们应该如何渲染。
  • 虚拟 DOM(Virtual DOM) :React 为每个 DOM 对象维护了一个轻量级的虚拟 DOM 表示。任何状态变更都会首先反映在虚拟 DOM 上,然后 React 通过比较新旧虚拟 DOM 的差异,来决定如何高效地更新实际的 DOM。
  • 响应式更新:当组件的状态改变时,React 会自动重新渲染组件及其子组件,以确保显示内容与状态同步。

2. 任务:实现基础数据渲染结构

一个index.html页面 image.png App.jsx页面

image.png

main.js页面

image.png

3. 实现步骤

为了方便验证,不管代码怎么变化,"app" 这个字符串是会一直显示在网页上的

image.png

步骤 1: 创建基本 HTML 文档

首先,创建一个基本的 index.html 文件,作为应用的入口点。

index.html: 这个 HTML 文档包含一个空的 <div id="root"></div>,这是我们 React 应用的挂载点。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <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>  // 引入main.js
</body>
</html>

步骤 2: 创建和渲染基本 DOM 元素

接着,创建 main.js 文件,并编写代码来手动创建和渲染 DOM 元素。

main.js:

// 这段代码创建了一个 `div` 元素并给它设置了 ID,然后创建了一个文本节点,并将这些元素添加到 `root` 元素内。
const dom = document.createElement('div');
dom.id = 'app';
const getRoot = document.getElementById('root');
getRoot.append(dom);

const el = document.createTextNode('');
el.nodeValue = 'app';
dom.append(el);

步骤 3: 用对象表示元素结构

将 DOM 元素的创建过程转换为使用对象表示。

main.js:

// 这个对象 (`reacteEl`) 描述了一个 `div` 元素,其中包含一个文本节点。这是 React 元素的简化表示。
const reacteEl = {
    type: 'div',
    props: {
        id: "app",
        children:[
            {
                type:"TEXT_ELEMENT",
                props:{
                    nodeValue:'app',
                    children:[]
                }
            }
        ]
    }
};

步骤 4: 提取子元素

为了更清晰地组织代码,将子元素提取为独立的变量。

main.js:

// `textEl` 是一个文本节点的描述,被作为子元素包含在 `reacteEl` 中。
const textEl = {
    type:"TEXT_ELEMENT",
    props:{
        nodeValue:'app',
        children:[]
    }
};

const reacteEl = {
    type: 'div',
    props: {
        id: "app",
        children:[textEl]
    }
};

步骤 5: 使用变量来创建和渲染 DOM

接下来,使用这些对象来创建和渲染实际的 DOM 元素。

main.js:

// 在这一步,`reacteEl` 和 `textEl` 对象被用来指导 DOM 元素的创建和渲染。
const dom = document.createElement(reacteEl.type);
dom.id = reacteEl.props.id;
const getRoot = document.getElementById('root');
getRoot.append(dom);

const el = document.createTextNode('');
el.nodeValue = textEl.props.nodeValue;
dom.append(el);

步骤 6: 封装创建文本节点的函数

为了提高代码的复用性和清晰性

  1. 封装一个 creatTextNode函数来创建文本节点对象。并且封装一个createElement用于创建任何类型的元素(包括文本元素)的对象表示。
  2. 创建App元素 :使用 createElement 函数创建一个 div 元素,其 ID 为 'app',并包含一个文本节点 'app',根据 App 元素的类型创建一个真实的 DOM 元素,然后将其添加到页面的 root 元素内。

main.js:

function creatTextNode(text) {
    return {
        type: "TEXT_ELEMENT",
        props: {
            nodeValue: text,
            children: []
        }
    };
}

function createElement(type, props, ...children) {
    return {
        type: type,
        props: {
            ...props,
            children
    };
}
const App = createElement('div', { id: 'app' }, creatTextNode('app'));
const dom = document.createElement(App.type);
dom.id = App.props.id;
const getRoot = document.getElementById('root');
getRoot.append(dom);

const el = document.createTextNode('');
el.nodeValue = creatTextNode('app').props.nodeValue;
dom.append(el);


步骤 7: 继续改进

  • 目前的代码结构中,文本节点被处理了两次:一次在 createElement 函数中,一次在手动渲染时。为了优化,我们应该实现一个 render 函数来处理整个虚拟 DOM 树的渲染,而不是手动创建和添加每个节点。

main.js完整代码:

function creatTextNode(text) {
    return {
        type: "TEXT_ELEMENT",
        props: {
            nodeValue: text,
            children: []
        }
    };
}
function createElement(type, props, ...children) {
    return {
        type: type,
        props: {
            ...props,
            children: children.map(child => 
                typeof child === 'object' ? child : creatTextNode(child)) // 判断类型以便于可以直接传进来字符串
        }
    };
}
function render(virtualDom, container) {
    const dom = virtualDom.type === 'TEXT_ELEMENT' 
        ? document.createTextNode(virtualDom.props.nodeValue) 
        : document.createElement(virtualDom.type);

    Object.keys(virtualDom.props).forEach(name => {
        if (name !== 'children') {
            dom[name] = virtualDom.props[name];
        }
    });

    virtualDom.props.children.forEach(child => render(child, dom));

    container.appendChild(dom);
}
const App = createElement('div', { id: 'app' }, 'app');  
const rootDiv = document.getElementById('root');
render(App, rootDiv);

步骤 8: 模仿结构

  • 目前的代码结构中,已经完成了基本的功能,但是为了保持和原react的结构一致,我们需要新增一个 createRoot 方法,类似于 React 17+ 的新 API。
  • 使用 ReactDOM.createRoot 方法创建一个根节点,并渲染 App 到这个根节点上。
const ReactDOM = {
    createRoot(container) {
        return {
            render(app) {
                render(app, container);
            }
        };
    }
};
const App = createElement('div', { id: 'app' }, 'app');
ReactDOM.createRoot(document.getElementById('root')).render(App);

main.js完整代码如下:

function creatTextNode(text){
    return{
        type:"TEXT_ELEMENT",
        props:{
            nodeValue:text,
            children:[]
        }
    }
}
function createElement(type, props, ...children) {
    return{
        type: type,
        props: {
            ...props,
            children: children.map(child => 
                typeof child === 'object' ? child : creatTextNode(child))
        }
    }
}
function render(virtualDom, container){
    const dom = virtualDom.type === 'TEXT_ELEMENT'?document.createTextNode(virtualDom.props.nodeValue):document.createElement(virtualDom.type)
    Object.keys(container).forEach((child)=>{
        virtualDom[child] = virtualDom[child]
    })
    virtualDom.props.children.forEach((child)=>render(child, dom))
    container.appendChild(dom);
}
const ReactDOM ={
    createRoot(constainer){
        return {
            render(App){
            render(App, constainer)
        }
      }
    }
}
const App = createElement('div', { id: 'app' }, 'app');
ReactDOM.createRoot(document.getElementById('root')).render(App)

到现在代码的部分已经好了 我们需要模仿数据结构把代码都提取出去

  1. 新建一个 cor 文件夹,下面新建一个react.js和reactDOM.js文件
  2. 新建App.js,和react之前的结构保持一致

结构如下:

image.png

React.js 代码:

function createNodeValue(text){
    return {
        type:'TEXT_ELEMENT',
        props:{
            nodeValue:text,
            children:[]
        }
    }
}
function createEl(type, props, ...children){
    return {
        type: type,
        props:{
           ...props,
            children: children.map(child => 
                typeof child === 'object' ? child : creatTextNode(child))
           ) 
        }
    }
}


function render(virtualDom, container){
    const dom = virtualDom.type === 'TEXT_ELEMENT'?document.createTextNode(virtualDom.props.nodeValue):document.createElement(virtualDom.type)
    Object.keys(virtualDom.props).forEach((key)=>{
        if(key !== "children"){
            dom[key] = virtualDom.props[key]
        }
    })
    if(virtualDom.props.children.length !== 0){
        virtualDom.props.children.forEach((child)=>{
            render(child, dom)
        })
    }
    container.append(dom)
}

const React ={
    render,
    createEl
}
export default React

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.createEl("div", { id: 'app'}, 'app','777')

export default App

main.js

import ReactDom from "./core/ReactDOM.js"
import App from './App.js'
ReactDom.createRoot(document.getElementById("root")).render(App)

index.html 代码不变

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <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>

把 js 变成支持jsx语法

  1. 安装 vite : npm create vite 重新命名:

ITaXm1RfiK.jpg

2.安装依赖: npm install

3.清空vite文件的内容,把之前的内容放进去,App.js和mian.js文件改成 jsx格式

image.png

4. 一些小的改动: App.jsx:

import React from './cor/React.js'
// const App = React.createElement('div', { id: 'app' }, 'app');
const App = <div>hi-mini-react</div>
export default App

有个遗留问题: 我们现在还没有实现支持App的function