【React源码实现】元素渲染的实现原理

news2025/1/23 2:15:12

前言

本文将结合React的设计思想来实现元素的渲染,即通过JSX语法的方式是如何创建为真实dom渲染到页面上,本文基本不涉及React的源码,但与React的实现思路是一致的,所以非常适合小白学习,建议跟着步骤敲代码,如有错误,请批评指正!

建议:

  1. 如果你不清楚JSX是一个什么东西或者不了解React的话,建议先到React官方文档跟着文档做小游戏的方式大致的了解JSX
  2. 如果你也想学习Vue的源码,也可以看下这篇博客,它与Vue的实现思路也是一致的,都是将虚拟DOM转变成真实DOM
  3. 不要太纠结每个方法是如何实现的,如果过于纠结就会陷入到无限递归循环的地狱中,看React源码也是这样的

官方文档

不妨先创建一个React项目试试:

npx create-react-app my-app

实现思路

这里我们仅探讨元素渲染的实现原理

在这里插入图片描述
React通过Babel将JSX语法的文件转译成React.createElement函数,调用React.createElement函数将JSX转变成虚拟Dom(也就是一个Vnode对象),再通过ReactDOM.render函数将虚DOM变成真实DOM挂载到页面上

  • 实现React.createElement函数
  • 实现Render函数
  • 完成渲染展示到页面上

初始化项目

当你通过上面的方式创建出一个React项目,不妨先删除多余的文件,把他变成最简单的一个jsx文件
在这里,我仅仅保留一个文件
在这里插入图片描述

import React from 'react';
import ReactDOM from 'react-dom/client';




let element = <h1>Hello, world</h1>;


const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  element
);

如果你成功打印出来一个Hello, world,那么第一步就成功了

React.createElement

Babel的转译涉及AST语法树的知识,可以去看我之前的博客,这里不再赘述,我们这里直接讲Babel将jsx语法的文件转变成React.createElement函数调用并生成虚拟DOM的实现步骤。

虚拟Dom的数据结构

这里我们先查看React.createElement生成虚拟Dom的数据结构,这里有利于我们如果手写方法创建虚拟Dom。

我们直接打印虚拟Dom元素

import React from 'react';
import ReactDOM from 'react-dom/client';




let element = <h1>Hello, world</h1>;


console.log(element);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  element 
);

在这里插入图片描述
可以看到,他的本质就是一个对象,Babel转译成createElement函数,调用之后返回了一个对象,这个对象就是虚拟Dom,里面有几个关键的值

也就是变成这个函数的调用

	React.createElement("h1",{className:"title",style:{color:'red'}},"hello")

这个函数接受三个参数,

  • 一个是元素的类型
  • 第二个是元素的配置
  • 第三个是元素的内容(可能不止是文本,也可能是一个元素节点)

关键键值

  • key:用于React实现diff算法的
  • ref:用于获取真实Dom
  • type:元素类型
  • props:元素配置(例如子节点、样式)
  • $$typeof:元素的唯一标识

具体实现

前面说这个方法,接受三个参数

  • 一个是元素的类型
  • 第二个是元素的配置
  • 第三个是元素的内容(可能不止是文本,也可能是一个元素节点)
import React from 'react';
import ReactDOM from 'react-dom';






let element2 = React.createElement("h1", {
  className: "title",
  style: {
    color: 'red'
  }
}, 'hello world','hi');




console.log(element2);

ReactDOM.render(
  element2,
  document.getElementById('root')
);

注意点1:你现在尝试在’hello world’后面再追加一个文本’hi’,你会发现当子节点有多个的时候,他的props中的children属性会从一个字符串类型变成数组类型,这一点很重要!

在这里插入图片描述

在这里插入图片描述

注意点2:如果你不是一个文本,而是一个元素对象,则是一个对象,如果是多个元素对象,则变成一个数组,里面是元素对象

import React from 'react';
import ReactDOM from 'react-dom';






let element2 = React.createElement("h1", {
  className: "title",
  style: {
    color: 'red'
  }
}, React.createElement("span", null, "hello"));




console.log(element2);

ReactDOM.render(
  element2,
  document.getElementById('root')
);

在这里插入图片描述

初始化函数

我们新建一个react.js文件,暴露这一个React对象,里面有一个 createElement函数,我们就是要实现使用这个函数返回一个虚拟dom


//接受三个参数,元素的类型、元素的配置、元素的节点

function createElement(type,config,children) {
    //返回一个虚拟dom
    return {

    }
}


const React = {
    createElement
}

export default React;

处理key和ref

我们的key和ref都写在了config中,因此我们需要单独把key和value单独抽出来,并且把他们从config中删除


    //第一步,处理key和ref
    let key, ref
    
    if (config) {
        key = config.key || null
        ref = config.ref || null
        delete config.key
        delete config.ref
    }

处理props和children

我们通过源码发现,他把children属性以及config中的所有元素都放进了props属性中

在这里插入图片描述
第二步,就是将config中的所有元素都放入到props中

    let props =  {...config}

第三步,就是去处理children节点,这里又有三种情况

  • 没有子节点
  • 有一个子节点 —— 文本节点 / 元素节点
  • 有多个子节点

    //第二步,处理children
    if (props) {
        //有多个儿子
        if (arguments.length > 3) {
           //多个儿子,就把他们变成一个数组
            props.children = Array.prototype.slice.call(arguments, 2)
            //有一个儿子  (1)文本  (2)元素
        }else if(arguments.length === 3){
            props.children = children;
        }
        //没有儿子,不需要去处理
    }

``

处理 $$typeof

这个key是React用于标识元素的,我们创建一个stant.js文件,用于暴露所有的标识类型


//用于标识元素
export const REACT_ELEMENT = Symbol('react.element')

export const REACT_TEXT = Symbol('react.text')

优化

在处理children节点的时候,当我们只有一个子节点并且是一个文本的时候,他是一个字符串类型的,我们统一处理成对象类型有利于后序做更新操作,通过toObject方法

import { REACT_TEXT } from "./stants";


export function toObject(element) {
    return typeof element === 'string' || typeof element === 'number' ? {type:REACT_TEXT,content:element} : element
}

整体代码

react.js



//实现以下:
// let element2 = React.createElement("h1", {
//   className: "title",
//   style: {
//     color: 'red'
//   }
// }, React.createElement("span", null, "hello"));

import { REACT_ELEMENT } from "./stants"
import { toObject } from "./utils"






function createElement(type,config,children) {
    

    if (config == null) { 
        config = {}
    }

    //第一步,处理key和ref
    let key, ref
    
    if (config) {
        key = config.key || null
        ref = config.ref || null
        delete config.key
        delete config.ref
    }





   // 第二步,就是将config中的所有元素都放入到props中
    let props =  {...config}


    //第三步,处理children
    if (props) {
        //有多个儿子
        if (arguments.length > 3) {
           //多个儿子,就把他们变成一个数组
            props.children = Array.prototype.slice.call(arguments, 2).map(toObject)
            //有一个儿子  (1)文本  (2)元素
        }else if(arguments.length === 3){
            props.children =  toObject(children)  ;  //统一转变成对象
        }
        //没有儿子,不需要去处理
    }





    //返回一个虚拟dom
    return {  //vnode
        key,
        ref,
        $$typeof:REACT_ELEMENT,
        props,
        type: type,

    }
}





const React = {
    createElement
}

export default React;

在index.js中引入我们自己的react文件来试试吧,到这里我们就实现了 React.createElement函数,生成了虚拟Dom
在这里插入图片描述

React.render函数

这个函数是将虚拟dom转变成真实dom的关键函数,这里我们接受两个参数,一个是虚拟dom,第二个是挂载节点,也就是实现这个函数

 ReactDOM.render(
   element2,
  document.getElementById('root')
 );

初始化函数


//将虚拟dom转变成真实dom的方法
function createDOM(vnode) { 
	let dom //真实dom


    return dom
}


function render(vnode, container) {
    
    //将虚拟dom转变成真实dom
    let dom = createDOM(vnode)

    //将真实dom挂载到container上
    container.appendChild(dom)


}


const ReactDOM = {
    render
}

export default ReactDOM;

处理type,生成对应的元素节点

请你回头看一下我们生成的虚拟节点的结构

  • key:用于React实现diff算法的
  • ref:用于获取真实Dom
  • type:元素类型
  • props:元素配置(例如子节点、样式)
  • $$typeof:元素的唯一标识

我们在上面做了一个优化,如果是文本的话,我们自己处理成了对象的数据结构

{
	type:REACT_TEXT,
	content:element
}
    //将虚拟dom转变成真实dom的方法
function createDOM(vnode) { 
  
            let { type, props, content } = vnode

            let Ndom;
            //1、判断type是什么类型的,是文本还是元素并生成对应的节点
            if (type === REACT_TEXT) {   //如果是一个文本类型的
                 Ndom = document.createTextNode(content)  //注意:我们在前面已经把所有的文件节点处理为一个对象类型的了
            } else {
                  Ndom = document.createElement(type)  //div
            }


            //2、处理属性   {children  style:{color:red,fontsize:16px} className="title" }
            if (props) { 
                console.log("props",props)
                //为了后续处理更新操作
                updateProps(Ndom, {}, props)
            }





        //3、处理子节点
        
        
        return Ndom

}

处理属性




//初始化和更新props的方法
function updateProps(dom, oldProps, newProps) {
    //初始化
    if (newProps) {
         //遍历新的属性对象
    for (let key in newProps) {
        if (key === 'children') {
            continue
        } else if (key === 'style') {  //如果是style的话就一个个追加进去
            let styleObj = newProps[key]
            for (let attr in styleObj) {
                dom.style[attr] = styleObj[attr]
            }
        } else {   //例如className就直接放上去即可
            dom[key] = newProps[key]
        }

    }
    }
   

    //更新操作,如果有旧节点
    if (oldProps) {
        //旧的属性在新的属性中没有,则删除
        for (let key in oldProps) { 
            if(!newProps[key]){
               dom[key] = null
        }
    }

}

            //2、处理属性   {children  style:{color:red,fontsize:16px} className="title" }
            if (props) { 
                //为了后续处理更新操作
                updateProps(dom, {}, props)
            }

处理子节点

//处理子节点
//接收两个参数,一个是子节点,另一个是挂载节点
function changeChildren(children, dom) {

     //有一个儿子的情况  对象
    if (typeof children == 'object'&& children.type ) {
        render(children, dom)  //递归调用
            //有多个儿子的情况  数组
    } else if (Array.isArray(children)) {
        //循环处理
        children.forEach(child =>  
            render(child, dom)
        )
     }


}

整体代码

import { REACT_TEXT } from "./stants"


    //初始化和更新props的方法
function updateProps(dom, oldProps, newProps) {
        //初始化
        if (newProps) {
            //遍历新的属性对象
            for (let key in newProps) {
                if (key === 'children') {
                    continue
                } else if (key === 'style') {  //如果是style的话就一个个追加进去
                    let styleObj = newProps[key]
                    for (let attr in styleObj) {
                        dom.style[attr] = styleObj[attr]
                    }
                } else {   //例如className就直接放上去即可
                    dom[key] = newProps[key]
                }

            }
        }
   

        //更新操作,如果有旧节点
        if (oldProps) {
            //旧的属性在新的属性中没有,则删除
            for (let key in oldProps) {
                if (!newProps[key]) {
                    dom[key] = null
                }
            }

        }
}
    

//处理子节点
//接收两个参数,一个是子节点,另一个是挂载节点
function changeChildren(children, dom) {

     //有一个儿子的情况  对象
    if (typeof children == 'object'&& children.type ) {
        render(children, dom)  //递归调用
            //有多个儿子的情况  数组
    } else if (Array.isArray(children)) {
        //循环处理
        children.forEach(child =>  
            render(child, dom)
        )
     }


}


    //将虚拟dom转变成真实dom的方法
function createDOM(vnode) { 
  
            let { type, props,content } = vnode
            let Ndom; //新的dom节点
            //1、判断type是什么类型的,是文本还是元素并生成对应的节点
             if (type === REACT_TEXT) {   //如果是一个文本类型的
                Ndom = document.createTextNode(content)  //注意:我们在前面已经把所有的文件节点处理为一个对象类型的了
            } else {
                Ndom = document.createElement(type)  //div
            }


            //2、处理属性   {children  style:{color:red,fontsize:16px} className="title" }
             if (props) {
                //为了后续处理更新操作
                updateProps(Ndom, {}, props)

                
                //3、处理子节点
                let children = props.children
                 if (children) {
                    changeChildren(children, Ndom)
                }

            }




        
        
        return Ndom

}




function render(vnode, container) {

    //将虚拟dom转变成真实dom
    let dom = createDOM(vnode)

    //将真实dom挂载到container上
    container.appendChild(dom)

}



const ReactDOM = {
    render
}

export default ReactDOM;

总结

自此完成我们就基本了解了React是如何实现元素渲染到视图的流程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/924860.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【TI毫米波雷达笔记】CCS雷达工程调试(以IWR6843AOP为例)

【TI毫米波雷达笔记】CCS雷达工程调试&#xff08;以IWR6843AOP为例&#xff09; 先前我们讨论了如何建立工程并编译 包括DSS和MSS部分 也就是DSP部分和cortex-r4f部分 通过编译 可以生成一个.out文件 如图 同样的 也有xer4f格、xe674格式等等 这取决于编译的工程配置 但这…

阿里云配置MySQL-server 8.0远程登录

Ubuntu 22.04 LTS 安装MySQL-Server 8.0 # apt search mysql-server # apt install mysql-server重建服务 # service mysql stop # vi /etc/mysql/mysql.conf.d/mysqld.cnf ... bind-address 0.0.0.0 ... # service mysql start # lsof -i:3306 COMMAND PID USER FD …

国内家庭影院普及率不断提升,洛科威多功能岩棉板发挥重要作用

数据显示&#xff0c;目前欧美发达国家的家庭影院普及率达到75%这一较高的水平上&#xff0c;受我国消费者观念改变以及收入水平提升等因素的综合影响&#xff0c;家庭影院也开始出现了一股小热潮&#xff0c;普及率正在稳步提升中。根据相关机构的预测&#xff0c;未来的几年时…

人工智能的未来---拥有常识及抽象世界

如果像 ChatGPT 这样的大模型人工智能领域最热门的东西&#xff0c;那么世界模型就是旗帜。 历史上三位最有影响力的人工智能研究人员中的两位 Yann LeCun 和 Yoshua Bengio 被誉为通往人工智能超级智能的最有可能的途径&#xff0c;他们代表了人工智能的愿景&#xff0c;即人工…

Unity 之transform.LookAt() 调整一个物体的旋转,使其朝向指定的位置

文章目录 总的介绍补充&#xff08;用于摄像机跟随的场景&#xff09; 总的介绍 transform.LookAt 是 Unity 引擎中 Transform 组件的一个方法&#xff0c;用于调整一个物体的旋转&#xff0c;使其朝向指定的位置。通常情况下&#xff0c;它被用来使一个物体&#xff08;如摄像…

数据降维 | MATLAB实现T-SNE降维特征可视化

数据降维 | MATLAB实现T-SNE降维特征可视化 目录 数据降维 | MATLAB实现T-SNE降维特征可视化降维效果基本描述程序设计参考资料 降维效果 基本描述 T-SNE降维特征可视化&#xff0c;MATLAB程序。 T-分布随机邻域嵌入&#xff0c;主要用途是对高维数据进行降维并进行可视化&…

Qt 自定义菜单 托盘菜单

托盘菜单实现&#xff1a;通过QSystemTrayIconQMenuQAction即可完美实现&#xff01; 实现方式&#xff1a;createActions用于创建菜单、菜单项,translateActions用于设置文本、实现多语化&#xff0c;translateAccount用于设置用户空间配额。 void TrayMenu::createActions(…

LLMs指令微调 Instruction fine-tuning

上周&#xff0c;您被介绍了生成型AI项目的生命周期。您探索了大型语言模型的示例用例&#xff0c;并讨论了它们能够执行的任务类型。 在本课中&#xff0c;您将了解如何提高现有模型在特定用例下的性能的方法。 您还将了解可用于评估您微调后的LLM性能并量化其相对于您开始…

Netty简易聊天室

文章目录 本文目的参考说明环境说明maven依赖日志配置单元测试 功能介绍开发步骤 本文目的 通过一个简易的聊天室案例&#xff0c;讲述Netty的基本使用。同时分享案例代码。项目中用到了log4j2&#xff0c;junit5&#xff0c;同时分享这些基础组件的使用。项目中用到了awt&…

开源的经济影响:商业与社区的平衡

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

Qt 解析XML文件 QXmlStreamReader

如何使用QXmlStreamReader来解析格式良好的XML&#xff0c;Qt的文档中指出&#xff0c;它是一种更快、更方便的Qt自己的SAX解析器&#xff08;QXmlSimpleReader&#xff09;的替代&#xff0c;它也较快&#xff0c;在某种情况下&#xff0c;比DOM&#xff08;QDomDocument&…

原生小案例:如何使用HTML5 Canvas构建画板应用程序

使用HTML5 Canvas构建绘图应用是在Web浏览器中创建交互式和动态绘图体验的绝佳方式。HTML5 Canvas元素提供了一个绘图表面&#xff0c;允许您操作像素并以编程方式创建各种形状和图形。本文将为您提供使用HTML5 Canvas创建绘图应用的概述和指导。此外&#xff0c;它还将通过解释…

密码学与加密通信: 解析密码学基础、加密算法、数字签名和安全通信协议,探讨保护数据传输的技术。

在数字化时代&#xff0c;数据的安全性和隐私保护变得至关重要。随着互联网的普及&#xff0c;人们的个人信息、商业机密以及敏感数据需要在网络传输中得到保护&#xff0c;这就是密码学及其在加密通信中的作用所在。本文将深入探讨密码学的基础知识、常见的加密算法、数字签名…

公网中Linux系统下Redis使用注意事项以及被pnscan病毒攻击的经过

一次惨痛的教训&#xff1a;被pnscan病毒攻击的经过&#xff08;公网中Linux系统下Redis使用注意事项&#xff09; 0.案发情况pnscan病毒感染惨状&#xff1a;>>提示<< 1.案发原因2.排查过程简单排查之后&#xff0c;发现啥都做不了。先百度到了如下文章&#xff1…

8086汇编test指令学习

Test指令将两个操作数进行逻辑与运算&#xff0c;并根据运算结果设置相关的标志位。Test的两个操作数不会被改变。运算结果在设置过相关标记位后会被丢弃。 TEST AX,BX 与 AND AX,BX 命令有相同效果&#xff0c;只是Test指令不改变AX和BX的内容&#xff0c;而AND指令会把结果保…

HTML番外篇(五)-移动端适配

一、媒体查询 1.认识媒体查询 媒体查询是一种提供给开发者针对不同设备需求进行定制化开发的一个接口。 你可以根据设备的类型&#xff08;比如屏幕设备、打印机设备&#xff09;或者特定的特性(比如屏幕的宽度)来修改你的页面。 媒体查询的使用方式主要有三种&#xff1a;…

CPU、MCU、MPU、SOC、SOCPC、概念解释之在嵌入式领域常听到的名词含义

CPU、MCU、MPU、SOC等几个在嵌入式领域学习过程中会涉及到的几个名词。我们来学习一下&#xff0c;资料从网上搜集的&#xff0c;有错的地方可以指出。。。 CPU、MCU、MPU、SOC、SOCPC、 1. CPU2. MPU3.MCUMPU和MCU的区别&#xff1a;4.SOC5. SoPC 1. CPU CPU&#xff0c;即中…

行业追踪,2023-08-24

自动复盘 2023-08-24 凡所有相&#xff0c;皆是虚妄。若见诸相非相&#xff0c;即见如来。 k 线图是最好的老师&#xff0c;每天持续发布板块的rps排名&#xff0c;追踪板块&#xff0c;板块来开仓&#xff0c;板块去清仓&#xff0c;丢弃自以为是的想法&#xff0c;板块去留让…

mysql--数据库的操作

数据库&#xff0c;是数据存储的最大单元。 1 创建数据库 create database mydatabase; 每次创建数据库的时候&#xff0c;都会多一个文件夹&#xff0c;关系型数据库是存储在磁盘当中的&#xff0c;所以这时候可以查看新建的数据库 2 指定字符集 MySQL中的字符集转换过程 制…

浅谈Python网络爬虫应对反爬虫的技术对抗

在当今信息时代&#xff0c;数据是非常宝贵的资源。而作为一名专业的 Python 网络爬虫程序猿&#xff0c;在进行网页数据采集时经常会遭遇到各种针对爬虫行为的阻碍和限制&#xff0c;这就需要我们掌握一些应对反爬机制的技术手段。本文将从不同层面介绍如何使用 Python 进行网…