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

猜你喜欢

转载自blog.csdn.net/m0_46983722/article/details/132475777