尝试全解React(一)

大哥大姐,打扰一下,尝试全解React(一)
最新回答
雨薇之恋

2024-11-06 03:56:17

今天开始尝试更加全面深入的了解React这个框架的构造及功能实现、源码分析,今天是第一篇,主要介绍基础概念。

本文主要参考了GitHub中的《图解React源码系列》。

一、宏观包结构

React的工程目录下共有35个包(17.0.2版本),其中比较重要的核心包有4个,他们分别是:

React基础包

提供定义react组件(ReactElement)的必要函数,包括大部分常用的api。

React-dom渲染器

可以将react-reconciler中的运行结果输出到web页面上,其中比较重要的入口函数包括ReactDOM.render(<App/>,document.getElementByID('root'))。

React-reconciler核心包

主要用来管理react应用状态的输入和结果的输出,并且可以将输入信号最终转换成输出信号传递给渲染器。主要的过程如下:

通过scheduleUpdateOnFiber接受输入,封装fiber树的生成逻辑到一个回调函数中,其中会涉及到fiber的树形结构、fiber.updateQueue队列、调用及相关的算法。

利用scheduler对回调函数(performSyncWorkOnRoot或perfromConcurrentWorkOnRoot)进行调度。

scheduler控制回调函数执行的时机,在回调函户执行后形成全新的fiber树。

最后调用渲染器(react-dom、react-native等)将fiber树结构渲染到界面上。

scheduler

是调度机制的核心实现,会控制react-reconciler送入回调函数的执行时机,并且在concurrent模式下可以实现任务分片。主要功能有两点:

执行回调(回调函数由react-reconciler提供)。

通过控制回调函数的执行时机,来实现任务分片、可中断渲染。

二、架构分层

如果按照React应用整体结构来分,可以将整个应用分解为接口层和内核层两个部分。

接口层(api)

包含平时开发所用的绝大多数api,如setState、dispatchAction等,但不包括全部。在react启动之后,可以改变渲染的有三个基本操作:

类组件中调用setState();

函数组件中使用hooks,利用dispatchAction来改变hooks对象;

改变context,实际上也是前二者。

内核层(core)

react的内核可以分成三个部分来看待,他们分别担任不同的功能:

scheduler(调度器)——指责是执行回调。会把react-reconciler提供的回调函数包装到任务对象中,并在内部维护一个任务队列(按照优先级排序),循环消费队列,直至队列清空。

react-reconciler(构造器)。首先它会装载渲染器,要求渲染器必须实现HostConfig协议,保证在需要时能够正确调用渲染器的api并生成相应的节点;接着会接收react-dom包和react包发起的更新请求;最后会把fiber树的构造过程封装进一个回调函数,并将其传入scheduler包等待调度。

react-dom(渲染器)。它会引导react应用的启动(通过render),并且实现HostConfig协议,重点是能够表现出fiber树,生成相对应的dom节点和字符串。

三、工作循环

在不同的方向上看过react的核心包之后,我们可以发现其中有两个比较重要的工作循环,它们分别是任务调度循环和fiber构造循环,分别位于scheduler和react-reconciler两个核心包中。

任务调度循环

位于scheduler中,主要作用是循环调用,控制所有的任务调度。

fiber构造循环

位于react-reconciler中,主要是控制fiber树的构造,整体过程是一个深度优先遍历的过程。

两个工作循环的区别与联系

任务调度循环数据结构为二叉树,循环执行堆的顶点,直到堆被清空;逻辑偏向宏观,调度的目标为每一个任务,具体任务就是执行相应的回调函数;

fiber构造循环数据结构为树,从上至下执行深度优先遍历;其逻辑偏向具体实现,只会负责任务的某一个部分,只负责fiber树的构造;

fiber构造循环可以看作是任务调度循环的一部分,它们类似从属关系,每个任务都会构造一个fiber树。

React主干逻辑

了解了两个工作循环的区别与联系后,可以发现:React的运行主干逻辑其实就是任务调度循环负责调度每个任务,fiber构造循环负责具体实现任务,即输入转换为输出的核心步骤。

也可以总结如下:

输入:每一次节点需要更新就视作一次更新需求;

注册调度任务:react-reconciler接收到更新需求后,会去scheduler调度中心注册一个新的任务,把具体需求转换成一个任务;

执行调度任务(输出):scheduler通过任务调度循环来执行具体的任务,此时执行具体过程在react-reconciler中。而后通过fiber构造循环构造出最新的fiber树,最后通过commitRoot把最新的fiber树渲染到页面上,此时任务才算完成。

四、高频对象

接下来介绍一下从react启动到页面渲染过程中出现频率较高的各个包中的高频对象。

react包

此包中包含react组件的必要函数以及一些api。其中,需要重点理解的是ReactElment对象,我们可以假设有一个入口函数:

ReactDOM.render(<App/>,document.getElementById('root'));

可以认为,App及其所有子节点都是ReactElement对象,只是它们的type会有区别。

ReactElement对象。

可以认为所有采用JSX语法书写的节点都会被编译器编译成React.createElement(...)的形式,所以它们创建出来的也就是一个个ReactElment对象。其数据结构如下:

exporttypeReactElement={|//辨别是否为ReactElement的标志$$typeof:any,//内部属性type:any,key:any,ref:any,props:any,//ReactFiber记录创建本对象的Fiber节点,未关联到Fiber树前为null_owner:any,//__DEV__dev环境下的额外信息_store:{validated:boolean,...},_self:React$Element<any>,_shadowChildren:any,_source:Source,|}

其中值得注意的有:

key:在reconciler阶段中会用到,所有ReactElment对象都有key属性,且默认值为null;

type:决定了节点的种类。它的值可以是字符串,函数或react内部定义的节点类型;在reconciler阶段会根据不同的type来执行不同的逻辑,如type为字符串类型则直接调用,是ReactComponent类型则调用其render方法获取子节点,是function类型则调用方法获取子节点等。

ReactComponent对象

这是type的一种类型,可以把它看作一种特殊的ReactElement。这里也引用原作者的一个简单例子:

classAppextendsReact.Component{render(){return(<divclassName="app"><header>header</header><Content/><footer>footer</footer></div>);}}classContentextendsReact.Component{render(){return(<React.Fragment><p>1</p><p>2</p><p>3</p></React.Fragment>);}}exportdefaultApp;

我们可以观察它编译之后得到的代码,可以发现,createElement函数的第一个参数将作为创建ReactElement的type,而这个Content变量会被命名为App_Content,作为第一个参数传入createElement。

classApp_Appextendsreact_default.a.Component{render(){return/*#__PURE__*/react_default.a.createElement('div',{className:'app',}/*#__PURE__*/,react_default.a.createElement('header',null,'header')/*#__PURE__*/,//此处直接将Content传入,是一个指针传递react_default.a.createElement(App_Content,null)/*#__PURE__*/,react_default.a.createElement('footer',null,'footer'),);}}classApp_Contentextendsreact_default.a.Component{render(){return/*#__PURE__*/react_default.a.createElement(react_default.a.Fragment,null/*#__PURE__*/,react_default.a.createElement('p',null,'1'),/*#__PURE__*/react_default.a.createElement('p',null,'2'),/*#__PURE__*/react_default.a.createElement('p',null,'3'),);}}

自此,可以得出两点结论:

ReactComponent是class类型,继承父类Component,拥有特殊方法setState和forceUpdate,特殊属性context和updater等。

在reconciler阶段,会根据ReactElement对象的特征生成对应的fiber节点。

顺带也可以带出ReactElement的内存结构,很明显它应该是一种类似树形结构,但也具有链表的特征:

class和function类型的组件,子节点要在组件render后才生成;

父级对象和子对象之间是通过props.children属性进行关联的;

ReactElement生成过程自上而下,是所有组件节点的总和;

ReactElement树和fiber树是以props.children为单位先后交替生成的;

reconciler阶段会根据ReactElement的类型生成对应的fiber节点,但不是一一对应的,比如Fragment类型的组件在生成fiber节点的时候就会略过。

react-reconciler包

react-reconciler连接渲染器和调度中心,同时自身也会负责fiber树的构造。

Fiber对象

Fiber对象是react中的数据核心,我们可以在ReactInternalTypes.js中找到其type的定义:

//一个Fiber对象代表一个即将渲染或者已经渲染的组件(ReactElement),一个组件可能对应两个fiber(current和WorkInProgress)//单个属性的解释在后文(在注释中无法添加超链接)exporttypeFiber={|tag:WorkTag,//表示fiber类型key:null|string,//和ReactElement一致elementType:any,//一般来讲和ReactElement一致type:any,//一般和ReactElement一致,为了兼容热更新可能会进行一定的处理stateNode:any,//与fiber关联的局部状态节点return:Fiber|null,//指向父节点child:Fiber|null,//指向第一个子节点sibling:Fiber|null,//指向下一个兄弟节点index:number,//fiber在兄弟节点中的索引,如果是单节点则默认为0ref://指向ReactElement组件上设置的ref|null|(((handle:mixed)=>void)&{_stringRef:?string,...})|RefObject,pendingProps:any,//从`ReactElement`对象传入的props.用于和`fiber.memoizedProps`比较可以得出属性是否变动memoizedProps:any,//上一次生成子节点时用到的属性,生成子节点之后保持在内存中updateQueue:mixed,//存储state更新的队列,当前节点的state改动之后,都会创建一个update对象添加到这个队列中.memoizedState:any,//用于输出的state,最终渲染所使用的statedependencies:Dependencies|null,//该fiber节点所依赖的(contexts,events)等mode:TypeOfMode,//二进制位Bitfield,继承至父节点,影响本fiber节点及其子树中所有节点.与react应用的运行模式有关(有ConcurrentMode,BlockingMode,NoMode等选项).//Effect副作用相关flags:Flags,//标志位subtreeFlags:Flags,//替代16.x版本中的firstEffect,nextEffect.当设置了enableNewReconciler=true才会启用deletions:Array<Fiber>|null,//存储将要被删除的子节点.当设置了enableNewReconciler=true才会启用nextEffect:Fiber|null,//单向链表,指向下一个有副作用的fiber节点firstEffect:Fiber|null,//指向副作用链表中的第一个fiber节点lastEffect:Fiber|null,//指向副作用链表中的最后一个fiber节点//优先级相关lanes:Lanes,//本fiber节点的优先级childLanes:Lanes,//子节点的优先级alternate:Fiber|null,//指向内存中的另一个fiber,每个被更新过fiber节点在内存中都是成对出现(current和workInProgress)//性能统计相关(开启enableProfilerTimer后才会统计)//react-dev-tool会根据这些时间统计来评估性能actualDuration?:number,//本次更新过程,本节点以及子树所消耗的总时间actualStartTime?:number,//标记本fiber节点开始构建的时间selfBaseDuration?:number,//用于最近一次生成本fiber节点所消耗的时间treeBaseDuration?:number,//生成子树所消耗的时间的总和|};

Update与UpdateQueue对象

在fiber对象中有一个属性fiber.updateQueue,是一个链式队列,一样来看一下源码:

exporttypeUpdate<State>={|eventTime:number,//发起update事件的时间(17.0.2中作为临时字段,即将移出)lane:Lane,//update所属的优先级tag:0|1|2|3,//payload:any,//载荷,根据场景可以设置成一个回调函数或者对象callback:(()=>mixed)|null,//回调函数next:Update<State>|null,//指向链表中的下一个,由于UpdateQueue是一个环形链表,最后一个update.next指向第一个update对象|};//===============UpdateQueue==============typeSharedQueue<State>={|pending:Update<State>|null,//指向即将输入的queue队列,class组件调用setState后会将新的update对象添加到队列中来|};exporttypeUpdateQueue<State>={|baseState:State,//队列的基础statefirstBaseUpdate:Update<State>|null,//指向基础队列的队首lastBaseUpdate:Update<State>|null,//指向基础队列的队尾shared:SharedQueue<State>,//共享队列effects:Array<Update<State>>|null,//用于保存有callback函数的update对象,commit后会依次调用这里的回调函数|};

Hook对象

Hook主要用于函数组件中,能够保持函数组件的状态。常用的api有useState、useEffect、useCallback等。一样,我们来看看源码是如何定义Hook对象的数据结构的:

exporttypeHook={|memoizedState:any,//内存状态,用于最终输出成fiber树baseState:any,//基础状态,会在Hook.update后更新baseQueue:Update<any,any>|null,//基础状态队列,会在reconciler阶段辅助状态合并queue:UpdateQueue<any,any>|null,//指向一个Update队列next:Hook|null,//指向该函数组件的下一个Hook对象,使多个Hook构成链表结构|};typeUpdate<S,A>={|lane:Lane,action:A,eagerReducer:((S,A)=>S)|null,eagerState:S|null,next:Update<S,A>,priority?:ReactPriorityLevel,|};typeUpdateQueue<S,A>={|pending:Update<S,A>|null,dispatch:(A=>mixed)|null,lastRenderedReducer:((S,A)=>S)|null,lastRenderedState:S|null,|};

由此我们可以看出Hook和fiber的联系:在fiber对象中有一个属性fiber.memoizedState会指向fiber节点的内存状态。而在函数组件中,其会指向Hook队列。

scheduler包

scheduler内部会维护一个任务队列,是一个最小堆数组,其中存储了任务task对象。

Task对象

task对象的类型定义不在scheduler中,而是直接定义在js代码中:

varnewTask={id:taskIdCounter++,//位移标识callback,//task最核心的字段,指向react-reconciler包所提供的回调函数priorityLevel,//优先级startTime,//代表task开始的时间,包括创建时间+延迟时间expirationTime,//过期时间sortIndex:-1,//控制task队列中的次序,值越小越靠前};

总结

今天主要总结了react包中的宏观结构可以分成scheduler、react-reconciler以及react-dom三个部分、两大工作循环(任务调度循环、fiber构造循环)的区别与联系和一些高频对象的类型定义等,这些都将作为后面源码解读的敲门砖。最后补上整体的工作流程示意图,方便理解记忆~

&copy;本总结教程版权归作者所有,转载需注明出处原文:https://juejin.cn/post/7099103637770600461