【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.hqwc.cn/news/81628.html

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

相关文章

0101prox-shardingsphere-中间件

1 启动ShardingSphere-Proxy 1.1 获取 目前 ShardingSphere-Proxy 提供了 3 种获取方式&#xff1a; 二进制发布包DockerHelm 这里我们使用Docker安装。 1.2 使用Docker安装 step1&#xff1a;启动Docker容器 docker run -d \ -v /Users/gaogzhen/data/docker/shardings…

【学习日记】【FreeRTOS】FreeRTOS 移植到 STM32F103C8

前言 本文基于野火 FreeRTOS 教程&#xff0c;内容是关于 FreeRTOS 官方代码的移植的注意事项&#xff0c;并将野火例程中 STM32F103RC 代码移植到 STM32F103C8。 一、FreeRTOS V9.0.0 源码的获取 两个下载链接&#xff1a; 官 网 代码托管 二、源码文件夹内容简介 Source…

DevExpress WinForms数据编辑器组件,提供丰富的数据输入样式!(二)

DevExpress WinForms超过80个高影响力的WinForms编辑器和多用途控件&#xff0c;从屏蔽数据输入和内置数据验证到HTML格式化&#xff0c;DevExpress数据编辑库提供了无与伦比的数据编辑选项&#xff0c;包括用于独立数据编辑或用于容器控件(如Grid, TreeList和Ribbon)的单元格。…

POI groupRow 折叠分组,折叠部分不显示问题

折叠组是什么&#xff1f;如图就是用POI 实现的&#xff0c;代码很简单&#xff1a;sheet.groupRow(开始行&#xff0c;结束行)即可 但是万万没想到&#xff0c;最终实现出的结果&#xff0c;合并的组&#xff0c;有一部分并没有渲染出来&#xff0c;如下图&#xff1a; 因为我…

【分享】小型园区组网场景

小型园区组网图 在小型园区中&#xff0c;S2700&S3700通常部署在网络的接入层&#xff0c;S5700&S6700通常部署在网络的核心&#xff0c;出口路由器一般选用AR系列路由器。 接入交换机与核心交换机通过Eth-Trunk组网保证可靠性。 每个部门业务划分到一个VLAN中&#…

英特尔开始加码封装领域 | 百能云芯

在积极推进先进制程研发的同时&#xff0c;英特尔正在加大先进封装领域的投入。在这个背景下&#xff0c;该公司正在马来西亚槟城兴建一座全新的封装厂&#xff0c;以加强其在2.5D/3D封装布局领域的实力。据了解&#xff0c;英特尔计划到2025年前&#xff0c;将其最先进的3D Fo…

SocketTools.NET 11.0.2148.1554 Crack

添加新功能以简化使用 URL 建立 TCP 连接的过程。 2023 年 8 月 23 日 - 12:35新版本 特征 添加了“HttpGetTextEx”函数&#xff0c;该函数在返回字符串缓冲区中的文本内容时提供附加选项。添加了对“FileTransfer”.NET 类和 ActiveX 控件中的“GetText”和“PutText”方法的…

前端工程化之模块化

模块化的背景 前端模块化是一种标准&#xff0c;不是实现理解模块化是理解前端工程化的前提前端模块化是前端项目规模化的必然结果 什么是前端模块化? 前端模块化就是将复杂程序根据规范拆分成若干模块&#xff0c;一个模块包括输入和输出。而且模块的内部实现是私有的&…

精准高效农业作业,植保无人机显身手

中国作为农业大国&#xff0c;拥有约18亿亩的农田&#xff0c;每年都需要进行种子喷洒和农药施用等农业作业&#xff0c;对于普通农户来说&#xff0c;这是一项耗时耗力的工程&#xff0c;同时&#xff0c;人工喷洒农药极易造成农药慢性中毒&#xff0c;对农民的身体健康产生极…

横扫“盲区”、“看透”缺陷,维视智造推出短波红外相机

在可见光领域&#xff0c;工业相机的视觉应用已经十分成熟&#xff0c;但在日常的客户咨询中&#xff0c;我们也经常接到一些“超纲需求”——客户想要检测“白底上的白色缺陷”、“不透明包装内的透明物体有无”等&#xff0c;均属于可见光无法实现的检测&#xff0c;而市面上…

飞天使-k8s基础组件分析-安全

文章目录 名称空间解释访问kubernetes API的控制RBAC的介绍 kubeconfig用户的创建集群默认角色 给组创建授权针对pod配置服务账户参考文档 名称空间解释 名字是啥&#xff1f; 答&#xff1a;集群中每个对象的名称对于该类型的资源都是唯一的。并且每一个对象在整个集群中也有…

Postman测WebSocket接口

01、WebSocket 简介 WebSocket是一种在单个TCP连接上进行全双工通信的协议。 WebSocket使得客户端和服务器之间的数据交换变得更加简单&#xff0c;允许服务端主动向客户端推送数据。在WebSocket API中&#xff0c;浏览器和服务器只需要完成一次握手&#xff0c;两者之间就直…