欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

React 框架的设计思想及源码结构

程序员文章站 2022-03-26 13:28:40
当前前端三大框架(vue、react和angular),除了vue之外,国内用得最多的就是react了,之前一直对其实现原理比较好奇,在花了很多时间深入研究了其源码实现后,本篇开始记录一下 同样的功能,用vue和react都能实现,相比较vue,react的学习门槛比较高,但是好处是它非常灵活,执行 ......

当前前端三大框架(vue、react和angular),除了vue之外,国内用得最多的就是react了,之前一直对其实现原理比较好奇,在花了很多时间深入研究了其源码实现后,本篇开始记录一下

同样的功能,用vue和react都能实现,相比较vue,react的学习门槛比较高,但是好处是它非常灵活,执行的效率更高(用到了很多新的技术),我个人觉得react的代码和vue的代码就像linux和windows,前者很注重javascript功底(类似linux的shell命令),后者有很多现成的html扩展标签指令( v-for、v-if等,类似windows的图形界面),所以如果一个人的js语言研究得比较深入,和一个刚刚入门js语言的程序员来说,用react实现了同样的需求,敲出来的代码质量会差很多的

react更加的纯粹,这里的纯粹指的是什么的,在react内部,jsx模板经babel转化后是一个对象,所有的操作都是基于这个对象和其对应的fiber结构来操作的。

vue和react有许多共同点,比如:

  • 都使用了虚拟dom
  • 更新时都使用了diff算法进行了优化

react和vue的不同之处如下

 writer by:大沙漠 qq:22969969

  vue框架 react框架
实现原理 将模板转化成一个render函数来执行 将每个节点转化为fiber对象,最终形成一个fiber树结构,来依次渲染
更新时的原理 通过es5的object.defineproperty()来动态触发的 通过两个fiber的对比来实现更新
是否支持双向绑定 支持,使用v-model实现 不支持,需要手动配置
指定模板的位置  使用el指定dom节点,或者template设置模板,又或者直接通过render来返回子节点vnode 在函数组件返回jsx,或者class组件的render方法内返回jsx
模板的格式 使用html标签格式,只是加了几个vue内置的标签或者属性 用jsx指定模板,jsx类似javascript的扩展
更新过程中可以否被打断 不能被打断 某些流程可以被打断,让优先级更高的任务优先执行(例如浏览器渲染)

举个例子,看看实现同样的功能,实现的效果是,在页面上创建如下两个dom节点:

  • p节点     ;默认显示hello world字符串
  • button              ;一个按钮,点击后会将helllo world变为hello vue或hello react

vue代码如下:

<div id="app"><button @click="test">测试</button><p>{{str}}</p></div>         <!--vue的写法-->
<script>
    new vue({
        el:"#app",
        data:{
            str:"hello world"
        },
        methods:{
            test(){
                this.str="hello vue";
            }
        }
    })
</script>

效果如下:

React 框架的设计思想及源码结构

react代码如下啊:

<div id="root"></div>                       <!--react的写法-->
<script type="text/babel">
    class app extends react.component{
        constructor(props){                
            super(props)
        }         
        state = {str:"hello world"}
        test = () => this.setstate(val=>val.str="hello react")
        render(){
            return  <div>
                        <button onclick={this.test}>测试</button>
                        <p>{this.state.str}</p>
                    </div>
        }
    }
    reactdom.render(<app/>,root)
</script>

 效果如下:

React 框架的设计思想及源码结构

整个react应用从初始化到结束可以分为四个步骤:

  • 创建更新     ;进行react的初始化工作,会将reactelement对象转化为一个fiber对象,然后形成一个fiber树的解构,之后的所有操作都是基于这个fiber树来进行操作的
  • 异步调度               ;react中有同步任务、异步任务、这个过程用于处理异步任务当中的逻辑,当浏览器渲染完后有有空余时间时开始执行这个调度,也就是第三步的render阶段
  • render阶段    ;这个阶段主要用于处理fiber树的更新,所有的事件绑定、css设置、class中大部分的生命周期函数、context、ref等任务,绝大多数可以被中断的任务都会在 这个阶段执行,这个阶段是可以被打断的,最终会形成一个effect链,每个元素是一个fiber对象,供第四步使用
  • commit阶段           ;主要遍历第三步render阶段生成的effect链,依次执行每个fiber元素上的收尾工作,这个阶段是同步任务,不能被打断的,因此大部分可以被中断的任务都在第三步render阶段执行完了

创建更新阶段(初始化阶段)

react用jsx来指定模板, jsx类似于javascript的扩展语法,经过babel转化后会转化为一个react.createelement函数,例子里的reactdom.render(<app/>,root)里的<app/>里的<app/>就是一个jxs对象,经过babel转化后为:

react.createelement(react.createelement(app, null), null)

经过babel转化后为如下函数:

react.createelement(
    "div", 
    null, 
    react.createelement("button", {onclick: (void 0).test}, "\u6d4b\u8bd5"), 
    react.createelement("p", null, (void 0).state.str)
);

react.createelement()函数执行后会返回一个react element对象,这里返回的对象如下:

{
    $$typeof:symbol(react.element),             //表示当前对象是一个reactelement对象
    type:app,
    key:null,
    ref:null,
    props:{},
    _owner:null

另外对于react里的类来说,render函数也需要返回jsx,比如例子里的render返回的如下

<div>
    <button onclick={this.test}>测试</button>
    <p>{this.state.str}</p>
</div>

经过babel转化后转化为如下这个reactelement对象

{
    $$typeof: symbol(react.element),             //表示当前对象是一个reactelement对象
    type: "div",                                 //组件的类型
    key: null,                                     //组件的key
    ref: null,                                     //组件的ref
    props:[                                     //组件的key
        {
            $$typeof: symbol(react.element),
            type: "button",
            key: null,
            ref: null,
            props:{
                children:'测试',onclick:f
            },
            _owner:{...}                             
        },{
            $$typeof: symbol(react.element),
            type: "p",
            key: null,
            ref: null,
            props:{
                children:"hello world",
            },
            _owner:{...}
        }
    ],
    _owner:{...}                                 //记录负责创建此元素的组件,可以是class组件、function组件,或者null
}

回到reactdom.render(<app/>,root),经过babel转化为reactdom.render(react.createelement(app, null),null),由于表达式是由内向外执行的,因此该函数会先执行react.createelement()函数将<app/>转化为一个reactelement对象后,然后再执行reactdom.render()函数,这样就开始了reactdom的逻辑了。

在reactdom.render里会经过一些列的初始化,创建一个fiber树,大致如下:

React 框架的设计思想及源码结构

一般我们执行reactdom.render(<app/>,root)时在第一阶段都会生成这样的数据结构,之后react所有的操作都是基于这个fiber树进行更新的,每个reactdom.render()都会生成一个reactroot对象,reactroot、fiberroot都有其作用,rootfiber就是根节点fiber对象了

app类对应的fiber在render的时候就会把它的render函数返回的jsx(也就是reactelement对象)都转化为fiber对象,具体后面再详解

我们可以把每个fiber理解为一个dom对象的映射,绝大多数dom对象都有一个其对应的fiber对象的(文本节点是没有fiber对象的,react直接通过nodevalue来设置了)。

执行完创建更新阶段,之后就进入了react的任务调度截断了

 

任务调度阶段

react中的任务分为同步和异步任务,如果是同步任务则会直接跳过这个阶段进入render阶段,对于异步任务来说,它会在浏览器渲染完之后利用空余的时间进行更新。

任务调度会利用requestanimationframe这个原生的api接口,requestanimationframe函数会在每次浏览器前执行的,执行的时候react利用了postmessage函数在任务队列里插入了一个函数,这样等到浏览器重绘完成后就会执行这个任务了,然后会触发相应的逻辑,执行第三步骤render阶段的相关操作。

 

render阶段

render阶段会依次遍历在第一步生成的fiber结构,利用深度优先遍历的算法,先遍历整个fiber树最左侧的fiber对象,然后再遍历到右侧的,最终回到最底层的根fiber对象,中间根据不同的组件类型做不同的处理,这个阶段也是整个react最难理解的一个阶段,因为有非常多的处理函数,还可以被高优先级的任务给打断,例如浏览器重绘,自定义事件等。这个阶段完成后在最顶层的fiber的firsteffect和lasteffect上设置一个链表,指向所有需要在commit阶段进行处理的fiber

 

commit阶段

这个阶段比较简单,就是遍历第三步最后生成的effect链,依次在每个fiber上执行收尾的工作。

 

react的流程大致如此,源码大概将近两万行,比较复杂,具体的后面到了某一阶段了再详解。