译者 | Video++极链信息技术后端Team非凡
重新整理 | 手袋
序言
React 源于 Facebook 的外部工程项目,是两个用作构筑界面的 Javascript 库。其保有较低的操控性,标识符方法论十分简
责任编辑期望透过参照 React 源标识符,辛克地顺利完成React的雏型。来协助认知其外部的同时实现基本原理,索韦泰而要博奈。
交互式DOM(Virtual DOM)
介绍React的都晓得,其高效率的其原因,其原因在于React依照网页的DOM内部结构,借助Javascript在缓存中构筑了两套完全相同内部结构的交互式缓存树数学模型,那个缓存数学模型就称作Virtual DOM。每每网页造成了变动,React的diff演算法林美珠在缓存数学模型中展开对照,抽取出差别点,在将Virtual DOM转化成为原生植物DOM输入时,依照差别点,只patch出有发生变动的部份。
上面是VirtualDOM结点的表述:
出口处
一切都是从 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。
上面是不同类型组件初始化渲染方法论的各自同时实现。
• TextComponent
作为纯展示类型组件,TextComponent 只是简单地将需要展示的内容,使用标签包装并返回就可以了。
• DomComponent
DomComponent类型在处理原生植物DOM时,需要额外注意一下原生植物事件部份的处理。
• CompositeComponent
在同时实现CompositeComponent类型的初始化渲染方法论之前,先看一下React组件的表述语法。
声明语法中,App继承自React.Component,因此我们先来同时实现Component那个类。
这里的 React.Component 不要与上面的 Component 混淆, Component 是不同组件类型的基类,抽象了组件渲染与更新;而React.Component则是Composite这种类型组件声明时的基类。
在 React.Component 中,简单地声明了控制数据流向的props属性,以及组件实例外部用作触发更新的setState()函数。
在介绍了 React.Component 的表述之后,我们回到 CompositeComponent ,已经开始同时实现mountComponent()的方法论。
首先要介绍的是,在composite类型组件中,vDom对象中的type,指向的是组件类的表述, 因此 mountComponent() 函数要做的工作,就是使用vDom的props属性来创建两个type的实例。
思考一下,在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函数的同时实现。
我们梳理一下更新流程:
组件在初始化时,记录下了render组件的实例,即this._renderedComponent;在更新环节,重新render()得到新的VDomnextRenderVDom;透过对照前后两个VDom的type和key,来判断是执行原来_renderedComponent的updateComponent函数,还是重新生成新的组件;
上面使用到了shouldUpdateReactComponent那个对照函数,来对vDom的type和key展开对照,其同时实现如下:
上面那个处理方法论,就是diff演算法的第两个规则: 当两个VDom结点的类型不一致时,重新构筑该组件的Virtual DOM树内部结构。
• TextComponent Text类型组件作为颗粒度最小的组件,更新方法论十分简单,展示新的文本内容即可。
• DomComponent
因为diff演算法的介入,Dom类型的处理方法论相对复杂。 可以分两步来处理,第一步更新组件输入的容器DOM上面的属性;第二步处理子级DOM。
_updateProperties()函数对比新旧props,顺利完成属性及事件的处理。 特别注意一下事件处理部份,需要注销掉原来DOM上注册的事件。
_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的更新;
上面的_diff()中,同时实现了更新步骤中的1 和2。
值得注意的是_diff过程中lastIndex变量的作用,其记录在遍历过程中,每次访问到的prevChildrenComponent中位置最靠后的组件,这是组件更新的一种排序上面的优化策略,可以参见这一篇文章当中的详细介绍:不可思议的react diff。
在计算出diffQueue的差别队列后,在_patch()函数中顺利完成最终HTML DOM的更新:
总结
至此,我们同时实现了两个简易版本的React框架,顺利完成了组件类的表述、初始化及更新; 并且梳理了核心diff演算法。
上面简单做一下总结:
• 组件分为3种类型来处理组件的初始化渲染和更新:TextComponent、DomComponent和CompositeComponent;
• virtualDom对象中,记录了组件类型type,唯一标识key和属性集合props;
• 组件是由virtual Dom创建而来,vDom上的type和key用来标识组件实例的唯一性;
• diff演算法的核心,是对比新旧vDom对象,来顺利完成部份组件实例的复用,并加入了排序优化策略。 透过javascript大量计算的代价,来换取减少网页DOM重排的消耗,从而提高了渲染操控性;