【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) {
    //返回一个虚拟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.mfbz.cn/a/97806.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

csp认证真题——重复局面——Java题解

目录 题目背景 问题描述 输入格式 输出格式 样例输入 样例输出 样例说明 子任务 提示 【思路解析】 【代码实现】 题目背景 国际象棋在对局时&#xff0c;同一局面连续或间断出现3次或3次以上&#xff0c;可由任意一方提出和棋。 问题描述 国际象棋每一个局面可以…

01JVM_内存结构

一、什么是JVM 1.JVM的定义 Java程序的运行环境&#xff0c;java二进制字节码的运行环境 2.JVM的好处 ①一次编写&#xff0c;到处运行 ②自动内存管理&#xff0c;垃圾回收功能 ③数组下标越界检查 ④多态 3.jvm&#xff0c;jre&#xff0c;jdk的比较 3.常见的JVM 主…

Mac下Docker Desktop安装命令行工具、开启本地远程访问

Mac系统下&#xff0c;为了方便在terminal和idea里使用docker&#xff0c;需要安装docker命令行工具&#xff0c;和开启Docker Desktop本地远程访问。 具体方法是在设置-高级下&#xff0c; 1.将勾选的User调整为System&#xff0c;这样不用手动配置PATH即可使用docker命令 …

说说我最近筛简历和面试的感受。。

大家好&#xff0c;我是鱼皮。 都说现在行情不好、找工作难&#xff0c;但招人又谈何容易&#xff1f;&#xff01; 最近我们公司在招开发&#xff0c;实习社招都有。我收到的简历很多&#xff0c;但认真投递的、符合要求的却寥寥无几&#xff0c;而且都是我自己看简历、选人…

对于uts namespace共享的测试

前言 单单以下列命令运行虽然是root&#xff0c;还不行&#xff0c;我们需要加--privileged&#xff0c;不然会报 hostname: you must be root to change the host name docker run -it --utshost ubuntu:latest /bin/bash 如果加上--privileged后 docker run -it --priv…

基于AI智能分析网关EasyCVR视频汇聚平台关于能源行业一体化监控平台可实施应用方案

随着数字经济时代的到来&#xff0c;实体经济和数字技术深度融合已成为经济发展的主流思路。传统能源行业在运营管理方面也迎来了新的考验和机遇。许多大型能源企业已开始抓住机遇&#xff0c;逐步将视频监控、云计算、大数据和人工智能技术广泛应用于生产、维护、运输、配送等…

hp惠普光影精灵5笔记本HP Pavilion Gaming-15-dk0135tx原装出厂Win10系统

原厂系统自带所有驱动、出厂主题壁纸LOGO、Office办公软件、惠普电脑管家等预装程序 适用型号&#xff1a; 15-dk0011tx,15-dk0018tx,15-dk0019tx,15-dk0020tx,15-dk0021tx,15-dk0038tx 15-dk0039tx,15-dk0040tx,15-dk0041tx,15-dk0125tx,15-dk0126tx,15-dk0127tx 15-dk012…

排序 Bubble Sort(提取函数)

C自学精简教程 目录(必读) 1 前驱知识点 3.5 for循环语句 3.6 if语句 3.7 函数 3.8 动态内存 2 排序 是将元素按照从小到大的顺序存放的方法。 一开始元素可能并不是按照从小到大的顺序存放的。 这时候我们需要找到需要调整的元素对&#xff0c;并交换这两个元素的值&…

Flutter实现动画列表AnimateListView

由于业务需要&#xff0c;在打开列表时&#xff0c;列表项需要一个从右边飞入的动画效果&#xff0c;故封装一个专门可以执行动画的列表组件&#xff0c;可以自定义自己的动画&#xff0c;内置有水平滑动&#xff0c;缩放等简单动画。花里胡哨的动画效果由你自己来定制吧。 功…

springboot实战(二)之将项目上传至远程仓库

目录 环境&#xff1a; 背景&#xff1a; 操作&#xff1a; 1.注册码云账号 2.创建仓库 步骤&#xff1a; 1.注册完码云账号后&#xff0c;点击加号&#xff0c;新建仓库 2.输入项目名称和介绍&#xff0c;点击创建 3.复制仓库地址&#xff0c;你可以选择https协议或者…

VSAN硬盘出现resetremoved

原创作者&#xff1a;运维工程师 谢晋 VSAN硬盘出现reset&removed 客户环境有8台服务器dell R740和R740XD服务器组成了一套VSAN集群&#xff0c;但R740那四台的物理机老是出现硬盘故障需进行硬盘更换&#xff0c;后发现刚换完的硬盘没过几天又坏了&#xff0c;先开始怀疑…

C++学习笔记总结练习:运算符重载两种方式

运算符重载的两种方式 1 基本概念 基础 运算符时具有特殊名字的函数&#xff1a;由关键字operator和气候定义的运算符共同组成。 可以被重载的运算符 方式 将运算符重载为类的成员函数。重载运算符函数&#xff0c;并声明为类的友元。 规则 重载后的运算符必须至少有一个…

vscode html使用less和快速获取标签less结构

扩展插件里面搜索 css tree 插件 下载 使用方法 选择你要生成的标签结构然后按CTRLshiftp 第一次需要在输入框输入 get 然后选择 Generate CSS tree less结构就出现在这个里面直接复制到自己的less文件里面就可以使用了 在html里面使用less 下载 Easy LESS 插件 自己创建…

深圳产品展示视频拍摄一站式服务

产品展示视频拍摄一站式服务是指一家专业的拍摄制作公司或团队提供从策划、拍摄到后期制作的全方位服务&#xff0c;以满足客户的产品展示需求。这种服务通常包括以下方面&#xff0c;由产品展示视频制作公司老友记小编从以下几个方面为您整理&#xff1a; 1.策划和预制阶段&a…

【期末复习笔记】计算机操作系统

计算机操作系统 进程的描述与控制程序执行进程进程的定义与特征相关概念定义特征进程与程序的区别 进程的基本状态和转换PCBPCB中的信息作用PCB的组织方式 线程进程与线程的比较 处理机调度与死锁处理机调度处理机调度的层次 调度算法处理机调度算法的目标处理机调度算法的共同…

PhpStorm安装篇

PhpStorm安装篇: 下载地址 : 进入官网下PhpStorm: PHP IDE and Code Editor from JetBrains 下载完之后&#xff0c;安装包 安装目录建议不要放C盘&#xff0c;放其他盘&#xff0c;其他直接一直点 next &#xff0c;到结束 安装完&#xff0c;打开编辑器 注册账号并登录后…

继续绷紧油市神经,市场预计沙特10月继续自愿减产

KlipC报道&#xff1a;据了解&#xff0c;市场参与者大都认为沙特阿拉伯将会把自愿额外减产的措施延长至10月底&#xff0c;以寻求在经济低迷的背景下提振油价。 KlipC的合伙人Andi D表示&#xff1a;“今年5月起&#xff0c;沙特就自愿减产日均50万桶原油&#xff0c;今年6月初…

苹果使用3D打印技术制造Apple Watch Series 9手表外壳

据彭博社的马克・古尔曼报道&#xff0c;苹果公司正在使用 3D 打印技术来制造即将推出的部分Apple Watch Series 9 的外壳。这种制造工艺可以节省传统数控加工所需的大量金属材料&#xff0c;同时缩短生产时间。这与之前苹果分析师郭明錤的说法相吻合。 苹果公司自2021年推出Ai…

Linux 虚拟机同步时间crontab以及crond详解

目录 一 Linux 虚拟机同步时间设置 1. 检查是否安装cron服务&#xff08;即时间同步器&#xff09; 2. 下载时间同步器 3. 编辑crontab 内容 4. 同步更新电脑网络时间 5.设置 reload 6. 查看 crond 状态 二 crond 详解 1. 启动/关闭cron服务 2. crontab命令格式 3. …

【LeetCode-中等题】138. 复制带随机指针的链表

文章目录 题目解题核心思路&#xff1a;找random指针指向思路一&#xff1a;哈希思路二&#xff1a;迭代构造新链表 方法一&#xff1a;哈希递归方法二&#xff1a;纯哈希方法三&#xff1a;迭代 节点拆分 题目 解题核心思路&#xff1a;找random指针指向 这里的拷贝属于深拷…
最新文章