React源码解析

2023-05-25 0 936

译者 | Video++极链信息技术后端Team非凡

重新整理 | 手袋

序言

React 源于 Facebook 的外部工程项目,是两个用作构筑界面的 Javascript 库。其保有较低的操控性,标识符方法论十分简

责任编辑期望透过参照 React 源标识符,辛克地顺利完成React的雏型。来协助认知其外部的同时实现基本原理,索韦泰而要博奈。

交互式DOM(Virtual DOM)

介绍React的都晓得,其高效率的其原因,其原因在于React依照网页的DOM内部结构,借助Javascript在缓存中构筑了两套完全相同内部结构的交互式缓存树数学模型,那个缓存数学模型就称作Virtual DOM。每每网页造成了变动,React的diff演算法林美珠在缓存数学模型中展开对照,抽取出差别点,在将Virtual DOM转化成为原生植物DOM输入时,依照差别点,只patch出有发生变动的部份。

上面是VirtualDOM结点的表述:

React源码解析

出口处

一切都是从 React.render(, document.body) 已经开始的,因此先来看一看 React是是不是表述的?

React中主要就主要就包括:

• render(virtualDom, container) 命令式调用,一般用作应用出口处,将交互式DOM渲染在container容器中;

• createElement(name, props, children) 创建组件时使用,JSX是其语法糖;

• Component 以ES6中的类式语法声明时使用。

createElement(type, props, children)

createElement()的主要就作用是根据给定type创建Virtual DOM结点,JSX是它的语法糖形式;其type参数可以是原生植物的html标签名(如:div、tag等),也可以是React组件类或函数。

组件的同时实现

React的所有组件,依照类型可以分为三种:

• 文本展示类型 (TextComponent)

• 原生植物DOM类型 (DomComponent)

• 自表述类型 (CompositeComponent)

每种类型的组件,都需要处理初始化更新两种方法论,具体会在上面两个函数中同时实现:

• mountComponent(rootNodeId) 用作处理初始化方法论

• updateComponent() 用作处理更新方法论

初始化mountComponent()的同时实现

mountComponent() 的同时实现思路是,根据virtual Dom对象生成HTML标识符并返回。

首先表述类型组件的基类 Component ,它只是简单地记录了传入的virtualDom对象,并初始化了组件结点ID。

React源码解析

上面是不同类型组件初始化渲染方法论的各自同时实现。

• TextComponent

作为纯展示类型组件,TextComponent 只是简单地将需要展示的内容,使用标签包装并返回就可以了。

React源码解析

• DomComponent

DomComponent类型在处理原生植物DOM时,需要额外注意一下原生植物事件部份的处理。

React源码解析

• CompositeComponent

在同时实现CompositeComponent类型的初始化渲染方法论之前,先看一下React组件的表述语法。

React源码解析

声明语法中,App继承自React.Component,因此我们先来同时实现Component那个类。

这里的 React.Component 不要与上面的 Component 混淆, Component 是不同组件类型的基类,抽象了组件渲染与更新;而React.Component则是Composite这种类型组件声明时的基类。

在 React.Component 中,简单地声明了控制数据流向的props属性,以及组件实例外部用作触发更新的setState()函数。

React源码解析

在介绍了 React.Component 的表述之后,我们回到 CompositeComponent ,已经开始同时实现mountComponent()的方法论。

首先要介绍的是,在composite类型组件中,vDom对象中的type,指向的是组件类的表述, 因此 mountComponent() 函数要做的工作,就是使用vDom的props属性来创建两个type的实例。

React源码解析

思考一下,在JSX语法中,导出器碰到标签后,就会去查找到 MyInput 的表述,上面说过JSX只是createElement的语法糖,因此背后调用的是 React.createElement(MyInput) 。在React规范中,可以使用类或函数来声明组件,因此在 mountComponent() 中使用 new type() ,就可以构造出MyInput的实例了。

更新流程updateComponent()的同时实现

同时实现完组件的初始化之后,接下来要同时实现组件的更新方法论。

React开放了 setState() 用作组件更新,回顾上面 React.Component 中 setState() 的表述, 实际调用的是 this._reactInternalInstance.updateComponent(null, newState) 那个函数。而 this._reactInternalInstance指向CompositeComponent,困此更新方法论交回CompositeComponent.updateComponent()来顺利完成。

• CompositeComponent

Composite类型组件的更新函数,需要处理两种流程:

当被表述在其它组件的render函数中时,其包裹组件会构筑出新的vDom对象,根据传入新的vDom来处理更新;当组件外部使用setState()触发时,根据新的state来更新;

介绍这两种方式的区别,可以协助我们认知上面updateComponent函数的同时实现。

React源码解析

我们梳理一下更新流程:

组件在初始化时,记录下了render组件的实例,即this._renderedComponent;在更新环节,重新render()得到新的VDomnextRenderVDom;透过对照前后两个VDom的type和key,来判断是执行原来_renderedComponent的updateComponent函数,还是重新生成新的组件;

上面使用到了shouldUpdateReactComponent那个对照函数,来对vDom的type和key展开对照,其同时实现如下:

React源码解析

上面那个处理方法论,就是diff演算法的第两个规则: 当两个VDom结点的类型不一致时,重新构筑该组件的Virtual DOM树内部结构。

• TextComponent Text类型组件作为颗粒度最小的组件,更新方法论十分简单,展示新的文本内容即可。

React源码解析

• DomComponent

因为diff演算法的介入,Dom类型的处理方法论相对复杂。 可以分两步来处理,第一步更新组件输入的容器DOM上面的属性;第二步处理子级DOM。

React源码解析

_updateProperties()函数对比新旧props,顺利完成属性及事件的处理。 特别注意一下事件处理部份,需要注销掉原来DOM上注册的事件。

React源码解析

_updateDOMChildren() 用作处理children部份的更新, 这部份的方法论相对复杂,也是diff演算法的优化点所在。

注:上面的说明中,以名称中含children来标识 集合,child指代 集合项。

i. 使用 nextChildrenVDoms 数据生成新的nextChildrenComponent;

•  DomComponent在初始化流程中,_mountComponent()函数会将组件集合保存下来,存入实例的_renderedChildrenComponent属性中, 透过遍历该属性,可以取得childComponent实例上的_vDom;

• 使用vDom来生成标识索引key,并以childComponent作为索引值,生成childrenComponent的Map内部结构; (对于Compotite类型,使用vDom.key作为标识索引key; 对于Text和Dom类型,使用childComponent在childrenComponent中所处的索引位置作为标识索引key);

• 使用nextChildrenVDoms生成新nextChildrenComponent的Map内部结构; 在遍历vDom集合的过程中,会使用上面的标识索引key生成规则,来展开判定,看是复用之前的组件实例触发更新,还是创建两个新的组件;

ii. 经过上面一步得到Map内部结构的prevChildren和nextChildren之后, 会使用深度遍历演算法,递归地对照树内部结构中,完全相同层级和位置的两个组件,将差别点保存为特定的diff标识内部结构,存入diffQueue队列中;

iii. 遍历diffQueue,依照差别的类型,顺利完成最终HTML DOM的发生变动;

首先是_updateDOMChildren()里的的表述。由于在递归组件树的结点时,存在多次触发_updateDOMChildren()的情况; 因此使用_updateDepth变量,在对照操作前+1,顺利完成后-1,来判定整个树的更新是否全部顺利完成,继而调用_patch()顺利完成HTML DOM的更新;

React源码解析

上面的_diff()中,同时实现了更新步骤中的1 和2。

React源码解析

值得注意的是_diff过程中lastIndex变量的作用,其记录在遍历过程中,每次访问到的prevChildrenComponent中位置最靠后的组件,这是组件更新的一种排序上面的优化策略,可以参见这一篇文章当中的详细介绍:不可思议的react diff。

在计算出diffQueue的差别队列后,在_patch()函数中顺利完成最终HTML DOM的更新:

React源码解析

总结

至此,我们同时实现了两个简易版本的React框架,顺利完成了组件类的表述、初始化及更新; 并且梳理了核心diff演算法。

上面简单做一下总结:

• 组件分为3种类型来处理组件的初始化渲染和更新:TextComponent、DomComponent和CompositeComponent;

• virtualDom对象中,记录了组件类型type,唯一标识key和属性集合props;

• 组件是由virtual Dom创建而来,vDom上的type和key用来标识组件实例的唯一性;

• diff演算法的核心,是对比新旧vDom对象,来顺利完成部份组件实例的复用,并加入了排序优化策略。 透过javascript大量计算的代价,来换取减少网页DOM重排的消耗,从而提高了渲染操控性;

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务