【React源码实现】元素渲染的实现原理

这篇具有很好参考价值的文章主要介绍了【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源码实现】元素渲染的实现原理,React专栏,react.js,javascript,前端
React通过Babel将JSX语法的文件转译成React.createElement函数,调用React.createElement函数将JSX转变成虚拟Dom(也就是一个Vnode对象),再通过ReactDOM.render函数将虚DOM变成真实DOM挂载到页面上

  • 实现React.createElement函数
  • 实现Render函数
  • 完成渲染展示到页面上

初始化项目

当你通过上面的方式创建出一个React项目,不妨先删除多余的文件,把他变成最简单的一个jsx文件
在这里,我仅仅保留一个文件
【React源码实现】元素渲染的实现原理,React专栏,react.js,javascript,前端

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 
);

【React源码实现】元素渲染的实现原理,React专栏,react.js,javascript,前端
可以看到,他的本质就是一个对象,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属性会从一个字符串类型变成数组类型,这一点很重要!

【React源码实现】元素渲染的实现原理,React专栏,react.js,javascript,前端

【React源码实现】元素渲染的实现原理,React专栏,react.js,javascript,前端

注意点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源码实现】元素渲染的实现原理,React专栏,react.js,javascript,前端

初始化函数

我们新建一个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属性中

【React源码实现】元素渲染的实现原理,React专栏,react.js,javascript,前端
第二步,就是将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源码实现】元素渲染的实现原理,React专栏,react.js,javascript,前端

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是如何实现元素渲染到视图的流程文章来源地址https://www.toymoban.com/news/detail-683131.html

到了这里,关于【React源码实现】元素渲染的实现原理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • React16源码: React中的completeUnitOfWork的源码实现

    completeUnitOfWork 1 )概述 各种不同类型组件的一个更新过程对应的是在执行 performUnitOfWork 里面的 beginWork 阶段 它是去向下遍历一棵 fiber 树的一侧的子节点,然后遍历到叶子节点为止,以及 return 自己 child 的这种方式 在 performUnitOfWork 里面,还有一个方法叫做 completeUnitOfWork 在

    2024年01月23日
    浏览(42)
  • React16源码: React中的beginWork的源码实现

    beginWork 1 )概述 在 renderRoot 之后,要对我们的 Fiber 树每一个节点进行对应的更新 更新节点的一个入口方法,就是 beginWork 这个入口方法会有帮助我们去优化整棵树的更新过程 react 它的节点其实是非常多的,如果每一次子节点的一个更新 就需要每一个节点都执行一遍更新的话

    2024年01月20日
    浏览(45)
  • React16源码: React中的unwindWork的源码实现

    unwindWork 1 )概述 在 renderRoot 的 throw Exception 里面, 对于被捕获到错误的组件进行了一些处理 并且向上去寻找能够处理这些异常的组件,比如说 class component 里面具有 getDerivedStateFromError 或者 componentDidCatch 这样的生命周期方法 这个class component 就代表它可以处理它的子树当中渲

    2024年01月25日
    浏览(36)
  • React16源码: React中的performWork的源码实现

    performWork 1 )概述 performWork 涉及到在调度完成,或者同步任务进来之后整个 root 节点链条如何更新 怎么更新一棵 Fiber 树,它的每一个节点是如何被遍历到,以及如何进行更新操作 A. 在执行 performWork 时候,是否有 deadline 的区分 deadline 是通过 reactschedule 它的一个时间片,更新

    2024年01月17日
    浏览(39)
  • React16源码: React中的updateHostRoot的源码实现

    HostRoot 的更新 1 )概述 HostRoot 是一个比较特殊的节点, 因为在一个react应用当中 它只会有一个 HostRoot , 它对应的 Fiber 对象是我们的 RootFiber 对象 重点在于它的更新过程 2 )源码 定位到 packages/react-reconciler/src/ReactFiberBeginWork.js#L612 HostRoot 创建更新的过程就是在 ReactFiberReconcile

    2024年01月22日
    浏览(43)
  • React16源码: React中的updateClassComponent的源码实现

    ClassComponent 的更新 1 ) 概述 在 react 中 class component,是一个非常重要的角色 它承担了 react 中 更新整个应用的API setState forceUpdate 在react当中,只有更新了state之后,整个应用才会重新进行渲染 在 class component 中, 它的逻辑相对复杂 2 )源码 在 packages/react-reconciler/src/ReactFiberB

    2024年01月21日
    浏览(38)
  • React16源码: React中的renderRoot的源码实现

    renderRoot 1 )概述 renderRoot 是一个非常复杂的方法 这个方法里处理很多各种各样的逻辑, 它主要的工作内容是什么? A. 它调用 workLoop 进行循环单元更新 遍历整个 Fiber Tree,把每一个组件或者 dom 节点对应的 Fiber 节点拿出来单一的进行更新,这是一个循环的操作 把整棵 Fiber T

    2024年01月17日
    浏览(42)
  • React16源码: React中的reconcileChildren的源码实现

    reconcileChildren 1 )概述 在更新了一个节点之后,拿到它的props.children 要根据这个children里面的 ReactElement 来去创建子树的所有的 fiber 对象 要根据 props.children 来生成 fiber 子树,然后判断 fiber 对象它是否是可以复用的 因为我们在第一次渲染的时候,就已经渲染了整个 fiber 子树

    2024年01月20日
    浏览(40)
  • React16源码: React中调度之requestWork的源码实现

    requestWork 1 )概述 在 scheduleWork 中,找到了创建更新的fiber对应的root节点 然后对它进行了一些操作之后,调用了 requestWork ,开始请求工作 在 requestWork 里面它会做哪些东西呢? 首先我们要把这个root节点加入到调度队列当中 然后判断是否是批量更新 最后再根据 expirationTime 的

    2024年01月17日
    浏览(40)
  • React16源码: React中调度之batchedUpdates的源码实现

    batchedUpdates 1 )概述 在 requestWork 在中间, 会判断一个 isBatchingUpdates 做一些特定的操作 batchedUpdates 是一个批量更新的操作, 什么是批量更新呢?先来看一个例子 2 )示例 index.js demos/batchedUpdates 在上述 countNumber 中定义了更改 state的方法 当点击 按钮时,里面的 num 会更改,回调函

    2024年01月17日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包