搜公众号
推荐 原创 视频 Java开发 开发工具 Python开发 Kotlin开发 Ruby开发 .NET开发 服务器运维 开放平台 架构师 大数据 云计算 人工智能 开发语言 其它开发 iOS开发 前端开发 JavaScript开发 Android开发 PHP开发 数据库
Lambda在线 > 极链科技 > 码农手记 | React源码解析

码农手记 | React源码解析

极链科技 2019-02-14
举报



栏目📰:码农手记 💻

撰文✍🏻:Video++极链科技前端Team超凡

整理📚:包包

关键词📌: 虚拟DOM(Virtual DOM)  组件的实现  流程updateComponent()的实现 



✍️

 码农手记 


将会邀请

一直在幕后

用代码和算法改变世界的

技术大佬们

将会不定期推送他们

所写的在技术专业中的

技术经验/研究/论文


为你呈现

/ 更前沿的技术思考 /

 / 更专业的技术干货 /


#码农手记# 第22期

文 | @Video++极链科技前端Team超凡

码农手记 | React源码解析



码农手记 | React源码解析


React 起源于 Facebook 的内部项目,是一个用于构建用户界面的 Javascript 库。其拥有较高的性能,代码逻辑非常简单,越来越多的人已开始关注和使用它。


本文希望通过参考 React 源码,依葫芦画瓢地完成React的雏形。来帮助理解其内部的实现原理,知其然更要知其所以然。



码农手记 | React源码解析


了解React的都知道,其高效的原因,是因为React按照页面的DOM结构,利用Javascript在内存中构建了一套相同结构的虚拟内存树模型,这个内存模型就称为Virtual DOM。每当页面产生了变化,React的diff算法会先在内存模型中进行比对,提取出差异点,在将Virtual DOM转化为原生DOM输出时,按照差异点,只patch出有变动的部分。


下面是VirtualDOM节点的定义:


码农手记 | React源码解析



码农手记 | React源码解析


一切都是从 React.render(<App/>, 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源码解析


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/> 标签后,就会去查找到 MyInput 的定义,上面说过JSX只是createElement的语法糖,因此背后调用的是 React.createElement(MyInput) 。在React规范中,可以使用类或函数来声明组件,因此在 mountComponent() 中使用 new type() ,就可以构造出MyInput的实例了。



码农手记 | React源码解析


实现完组件的初始化之后,接下来要实现组件的更新逻辑。


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源码解析


至此,我们实现了一个简易版本的React框架,完成了组件类的定义、初始化及更新; 并且梳理了核心diff算法。


下面简单做一下总结:


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


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


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


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


相关资料:

https://github.com/Matt-Esch/virtual-dom 

https://zhuanlan.zhihu.com/p/20346379



🔘

贴心送上往期精选

「码农手记」传送门

👇👇👇


1⃣️

Git命令解析 - merge、rebase #


2⃣️

# HPAIC人类蛋白质图谱分类挑战赛金牌经验分享 #


3⃣️

# 智能手记中的视频解码 #




🚀

Video++极链科技集团是

一家专注于新文娱产业的

AI科技企业


开创了AI+新文娱产业的商业化

形成了

AI场景营销平台、视频电商、IP新商业

业务矩阵的规模化商用



👇🏻 商业合作请点击阅读原文了解详情 🎬 

版权声明:本站内容全部来自于腾讯微信公众号,属第三方自助推荐收录。《码农手记 | React源码解析》的版权归原作者「极链科技」所有,文章言论观点不代表Lambda在线的观点, Lambda在线不承担任何法律责任。如需删除可联系QQ:516101458

文章来源: 阅读原文

相关阅读

关注极链科技微信公众号

极链科技微信公众号:videoplspls

极链科技

手机扫描上方二维码即可关注极链科技微信公众号

极链科技最新文章

精品公众号随机推荐

举报