【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文件

在这里,我仅仅保留一个文件

bash 复制代码
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元素

bash 复制代码
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,里面有几个关键的值

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

bash 复制代码
	React.createElement("h1",{className:"title",style:{color:'red'}},"hello")

这个函数接受三个参数,

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

关键键值

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

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

  • 一个是元素的类型
  • 第二个是元素的配置
  • 第三个是元素的内容(可能不止是文本,也可能是一个元素节点)
bash 复制代码
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:如果你不是一个文本,而是一个元素对象,则是一个对象,如果是多个元素对象,则变成一个数组,里面是元素对象

bash 复制代码
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

bash 复制代码
//接受三个参数,元素的类型、元素的配置、元素的节点

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

    }
}


const React = {
    createElement
}

export default React;

处理key和ref

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

bash 复制代码
    //第一步,处理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中

bash 复制代码
    let props =  {...config}

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

  • 没有子节点
  • 有一个子节点 ------ 文本节点 / 元素节点
  • 有多个子节点
bash 复制代码
    //第二步,处理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文件,用于暴露所有的标识类型

bash 复制代码
//用于标识元素
export const REACT_ELEMENT = Symbol('react.element')

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

优化

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

bash 复制代码
import { REACT_TEXT } from "./stants";


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

整体代码

react.js

bash 复制代码
//实现以下:
// 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,第二个是挂载节点,也就是实现这个函数

bash 复制代码
 ReactDOM.render(
   element2,
  document.getElementById('root')
 );

初始化函数

bash 复制代码
//将虚拟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:元素的唯一标识

bash 复制代码
{
	type:REACT_TEXT,
	content:element
}
bash 复制代码
    //将虚拟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

}

处理属性

bash 复制代码
//初始化和更新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)
            }

处理子节点

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

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


}

整体代码

bash 复制代码
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是如何实现元素渲染到视图的流程

相关推荐
拜晨5 分钟前
用流式 JSON 解析让 AI 产品交互提前
前端·javascript
浩男孩8 分钟前
🍀vue3 + Typescript +Tdesign + HiPrint 打印下载解决方案
前端
andwhataboutit?9 分钟前
LANGGRAPH
java·服务器·前端
无限大610 分钟前
为什么"Web3"是下一代互联网?——从中心化到去中心化的转变
前端·后端·程序员
cypking12 分钟前
CSS 常用特效汇总
前端·css
程序媛小鱼16 分钟前
openlayers撤销与恢复
前端·js
Thomas游戏开发17 分钟前
如何基于全免费素材,0美术成本开发游戏
前端·后端·架构
若梦plus19 分钟前
Hybrid之JSBridge原理
前端·webview
chilavert31819 分钟前
技术演进中的开发沉思-269 Ajax:拖放功能
前端·javascript·ajax
xiaoxue..20 分钟前
单向数据流不迷路:用 Todos 项目吃透 React 通信机制
前端·react.js·面试·前端框架