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

前言

本文将结合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) {//返回一个虚拟domreturn {}
}const React = {createElement
}export default React;

处理key和ref

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

//第一步,处理key和reflet key, refif (config) {key = config.key || nullref = config.ref || nulldelete config.keydelete config.ref}

处理props和children

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

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

    let props =  {...config}

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

  • 没有子节点
  • 有一个子节点 —— 文本节点 / 元素节点
  • 有多个子节点
//第二步,处理childrenif (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和reflet key, refif (config) {key = config.key || nullref = config.ref || nulldelete config.keydelete config.ref}// 第二步,就是将config中的所有元素都放入到props中let props =  {...config}//第三步,处理childrenif (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)  ;  //统一转变成对象}//没有儿子,不需要去处理}//返回一个虚拟domreturn {  //vnodekey,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 //真实domreturn dom
}function render(vnode, container) {//将虚拟dom转变成真实domlet 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 } = vnodelet 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 } = vnodelet 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.childrenif (children) {changeChildren(children, Ndom)}}return Ndom}function render(vnode, container) {//将虚拟dom转变成真实domlet dom = createDOM(vnode)//将真实dom挂载到container上container.appendChild(dom)}const ReactDOM = {render
}export default ReactDOM;

总结

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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Docker consul的容器服务注册与发现

前言一、服务注册与发现二、consul 介绍三、consul 部署3.1 consul服务器3.1.1 建立 Consul 服务3.1.2 查看集群信息3.1.3 通过 http api 获取集群信息 3.2 registrator服务器3.2.1 安装 Gliderlabs/Registrator3.2.2 测试服务发现功能是否正常3.2.3 验证 http 和 nginx 服务是…

CRM系统如何定制?定制哪些功能?

虽然市场上有许多成熟的CRM系统供企业选择&#xff0c;但很多时候&#xff0c;现有的标准化CRM系统无法满足企业的特殊需求。这时就需要进行CRM系统定制。那么&#xff0c;什么时候需要CRM系统定制&#xff0c;CRM系统定制怎么弄&#xff1f;下面我们就说一说。 什么时候需要C…

缓存的设计方式

问题情况&#xff1a; 当有大量的请求到内部系统时&#xff0c;若每一个请求都需要我们操作数据库&#xff0c;例如查询操作&#xff0c;那么对于那种数据基本不怎么变动的数据来说&#xff0c;每一次都去数据库里面查询&#xff0c;是很消耗我们的性能 尤其是对于在海量数据…

【Winform学习笔记(九)】Winform窗体程序延迟函数

Winform窗体程序延迟函数 前言正文1、具体代码2、使用示例 前言 Winform 窗体程序开发时&#xff0c;有时需要程序延迟或休眠几秒&#xff0c;如果直接使用 Thread.Sleep() 方法&#xff0c;会造成程序的假死&#xff0c;UI 界面停止响应&#xff1b; 本文中主要介绍一种方法&…

VB.NET调用VB6封装在OCX控件中的函数

将功能函数封装于OCX之中在VB6平台上可以简单化&#xff0c;在默认模板中直接考贝贴入那些函数即可。在博文【将《VB6编程IEEE浮点算法实践》中的Function封装成OCX】将《VB6编程IEEE浮点算法实践》中的Function封装成OCX_Mongnewer的博客-CSDN博客中对VB6的OCX封装做了具体实践…

keepalived+lvs(DR)(四十六)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 一、作用 二、调度器配置 三、web节点配置 一、作用 使用keepalived解决lvs的单点故障 高可用集群 二、调度器配置 安装keepalived yum install -y k…

OVRL-V2: A simple state-of-art baseline for IMAGENAV and OBJECTNAV 论文阅读

论文信息 题目&#xff1a;OVRL-V2: A simple state-of-art baseline for IMAGENAV and OBJECTNAV 作者:Karmesh Yadav&#xff0c; Arjun Majumdar&#xff0c; Ram Ramrakhya 来源&#xff1a;arxiv 时间&#xff1a;2023 代码地址&#xff1a; https://github.com/ykarmesh…

whisper 语音识别项目部署

1.安装anaconda软件 在如下网盘免费获取软件&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1zOZCQOeiDhx6ebHh5zNasA 提取码&#xff1a;hfnd 2.使用conda命令创建python3.8环境 conda create -n whisper python3.83.进入whisper虚拟环境 conda activate whisper4.…

vue uniapp 同意验证码滑块验证

前言 &#xff08;vue-puzzle-vcode&#xff09; 发送验证码以及登录的时候会做验证&#xff0c;防止机刷等 效果图 一、安装依赖 npm install vue-puzzle-vcode --save二、使用步骤 1.html使用 <Vcode :show"isShow" success"onSuccess"/>2.j…

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

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

Anolis 8.6 下 Redis 7.2.0 集群搭建和配置

Redis 7.2.0 搭建和集群配置 一.Redis 下载与单机部署1.Redis 下载2.虚拟机配置3.Redis 单机源码安装和测试4.Java 单机连接测试1.Pom 依赖2.配置文件3.启动类4.配置类5.单元测试6.测试结果 二.Redis 集群部署1.主从1.从节点配置2.Java 测试 2.哨兵1.哨兵节点配置2.复制一个哨兵…

SpringCloud学习笔记(六)_Ribbon服务调用

Ribbon介绍 Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具 Ribbon是Netflix发布的开源项目&#xff0c;主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项如连接超时、重试等。简单的说&#xff0c;就是…

java八股文面试[Spring]——如何实现一个IOC容器

什么是IOC容器 IOC不是一种技术&#xff0c;只是一种思想&#xff0c;一个重要的面向对象编程的法则&#xff0c;它能指导我们如何设计出松耦合&#xff0c;更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象&#xff0c;从而导致类与类之间高耦合&#xff0c;难于…

生成式人工智能的潜在有害影响与未来之路(三)

产品责任法的潜在适用 背景和风险 产品责任是整个二十世纪发展起来的一个法律领域&#xff0c;旨在应对大规模生产的产品可能对社会造成的伤害。这一法律领域侧重于三个主要危害&#xff1a;设计缺陷的产品、制造缺陷的产品和营销缺陷的产品。产品责任法的特点有两个要素&…

LinkedList的顶级理解

目录 1.LinkedList的介绍 LinkedList的结构 2.LinkedList的模拟实现 2.1创建双链表 2.2头插法 2.3尾插法 2.4任意位置插入 2.5查找关键字 2.6链表长度 2.7遍历链表 2.8删除第一次出现关键字为key的节点 2.9删除所有值为key的节点 2.10清空链表 2.11完整代码 3.…

聚观早报 | 云鲸扫拖机器人J4体验;芯科科技第三代无线开发平台

【聚观365】8月24日消息 云鲸扫拖机器人J4体验 芯科科技推出第三代无线开发平台 英伟达与VMWare宣布扩大合作 万物新生&#xff08;爱回收&#xff09;2023年二季度财报 充电桩需求增长带动汽车后服务市场 云鲸扫拖机器人J4体验 家庭卫生清洁是每个人都无法回避的事情&am…

Unity 类Scene窗口相机控制

类Scene窗口相机控制 &#x1f354;效果 &#x1f354;效果 传送门&#x1f448;

疫情下社区管理系统的设计与实现(论文+源码)_kaic

疫情下社区管理系统 摘 要&#xff1a;新冠疫情下的社区人员管理系统是基于SpringBoot搭建的一套前后端分离系统。面向疫情下的社区管理人员和社区用户&#xff0c;主要用于进行社区服务&#xff0c;进行高效的社区人员管理。具有一定的经济效益和社会效益。本文分析了新冠疫情…

上门服务系统|上门服务小程序如何提升生活质量?

上门服务其实就是本地生活服务的升级&#xff0c;上门服务包含很多行业可以做的。例如&#xff1a;厨师上门、上门家电维修、跑腿等等。如今各类本地化生活服务越来越受大家的喜爱。基于此市场愿景&#xff0c;我们来谈谈上门服务系统功能。 一、上门服务系统功能 1、预约服务…

美创科技“签”手柠檬文才学堂,共推高校数据安全建设

近日&#xff0c;由柠檬文才学堂联合中国教育在线、东北财经大学网络教育学院共同主办的“三教统筹下高校继续教育数字化转型研讨”顺利召开。 国内高等院校&#xff08;高职院校&#xff09;继续教育分管领导&#xff0c;继续教育学院领导及继续教育信息化、教学教务管理、课程…