【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是如何实现元素渲染到视图的流程

相关推荐
虾球xz21 分钟前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇26 分钟前
HTML常用表格与标签
前端·html
疯狂的沙粒30 分钟前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员1 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐1 小时前
前端图像处理(一)
前端
程序猿阿伟1 小时前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
疯狂的沙粒1 小时前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript
瑞雨溪1 小时前
AJAX的基本使用
前端·javascript·ajax
力透键背1 小时前
display: none和visibility: hidden的区别
开发语言·前端·javascript
程楠楠&M1 小时前
node.js第三方Express 框架
前端·javascript·node.js·express