0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

React源码解析

张康康 2019-07-29 18:21 次阅读

作者 | Video++极链科技前端Team超凡

整理 | 包包

前言

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

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

虚拟DOM(Virtual DOM)

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

下面是VirtualDOM节点的定义:

653c1c5046c3420db699815d3dfc4368.png

入口

一切都是从 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。

cbb4e4d8aa424c038249bd67b03b1f41.png

下面是不同类型组件初始化渲染逻辑的各自实现。

• TextComponent

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

15304e462a5946f5a2893fcd5aaec1ea.png

• DomComponent

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

de533d3fe8334d148ec1ced5ee4e7cdb.jpeg

• CompositeComponent

在实现CompositeComponent类型的初始化渲染逻辑之前,先看一下React组件的定义语法。

cb51a41c4bdc45ecbf34faac4f5cb6fe.png

声明语法中,App继承自React.Component,所以我们先来实现Component这个类。

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

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

4db60e6ab95242fb811bb32385bb38db.png

在了解了 React.Component 的定义之后,我们回到 CompositeComponent ,开始实现mountComponent()的逻辑。

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

c78efdd70e424012a2dd3c1b81cd3841.jpeg

思考一下,在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类型组件的更新函数,需要处理两种流程:

  1. 当被定义在其它组件的render函数中时,其包裹组件会构建出新的vDom对象,根据传入新的vDom来处理更新;

  2. 当组件内部使用setState()触发时,根据新的state来更新;

了解这两种方式的区别,可以帮助我们理解下面updateComponent函数的实现。

67f7b30f22e3481baa69bb078005760a.jpeg

我们梳理一下更新流程:

  1. 组件在初始化时,记录下了render组件的实例,即this._renderedComponent;

  2. 在更新环节,重新render()得到新的VDomnextRenderVDom;

  3. 通过比对前后两个VDom的type和key,来判断是执行原来_renderedComponent的updateComponent函数,还是重新生成新的组件;

上面使用到了shouldUpdateReactComponent这个比对函数,来对vDom的type和key进行比对,其实现如下:

ea6030e4ab03457d92ec5f482bb33d30.png

上面这个处理逻辑,就是diff算法的第一个规则: 当两个VDom节点的类型不一致时,重新构建该组件的Virtual DOM树结构。

• TextComponent Text类型组件作为颗粒度最小的组件,更新逻辑非常简单,展示新的文本内容即可。

beb0ebaf8c4547ed9ec11bcf7235ff94.png

• DomComponent

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

cdd7a90a1efb4b4b890bf1ff0bdfe9d8.png

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

822e388e3e40407384564a46fe0c09fe.jpeg

_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的更新;

54fbb8f38ad24b07b5fc28ff5968097c.png

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

2d2aa48a60db4af9b815baff7691eeac.jpeg

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

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

66c98874e27c4b4aaf174da6158f5db3.png

总结

至此,我们实现了一个简易版本的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


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
收藏 人收藏

    评论

    相关推荐

    SSM框架的源码解析与理解

    的核心是控制反转(IoC)和面向切面编程(AOP)。 源码解析: Spring的源码主要分为以下几个部分: Bean容器: 负责实例化、配置和组装对象。核心接口是 B
    的头像 发表于 12-17 09:20 275次阅读

    使用SSR构建React应用的步骤

    使用SSR(Server-Side Rendering,服务器端渲染)构建React应用的步骤通常包括以下几个阶段: 一、项目初始化与配置 创建React项目 : 可以使用Create React
    的头像 发表于 11-18 11:30 341次阅读

    Taro鸿蒙技术内幕系列(一):如何将React代码跑在ArkUI上

    基于 Taro 打造的京东鸿蒙 APP 已跟随鸿蒙 Next 系统公测,本系列文章将深入解析 Taro 如何实现使用 React 开发高性能鸿蒙应用的技术内幕。
    的头像 发表于 10-25 17:24 333次阅读
    Taro鸿蒙技术内幕系列(一):如何将<b class='flag-5'>React</b>代码跑在ArkUI上

    ESP-BOX 智能药盒源码解析(续)

    药盒基本概述请参考上一篇文章源码解析主函数部分___main/main.c●初始化NVS:初始化非易失性存储(NVS),如果需要擦除和重新初始化NVS,会进行相应处理。●检查百度API密钥:调用
    的头像 发表于 10-10 08:01 262次阅读
    ESP-BOX 智能药盒<b class='flag-5'>源码</b><b class='flag-5'>解析</b>(续)

    如何在NXP源码基础上适配ELF 1开发板的PWM功能

    本次源码适配项目是在NXP i.MX6ULL EVK评估板所搭载的Linux内核源码(版本为Linux-imx_4.1.15)基础上进行的,主要目标是通过调整功能接口引脚配置,使其适应ELF 1开发板。为了深入阐述这一适配过程,我们将以PWM功能的适配作为具体示例,深入
    的头像 发表于 09-10 10:00 949次阅读
    如何在NXP<b class='flag-5'>源码</b>基础上适配ELF 1开发板的PWM功能

    ESP32 崩溃后调试信息定位到源码方法

    arduino 通过调试信息定位出错源码
    的头像 发表于 08-27 14:29 956次阅读

    ElfBoard技术贴|在NXP源码基础上适配ELF 1开发板的按键功能

    ,将以按键功能的适配作为具体示例,深入解析整个适配的流程。一、准备工作NXP源码路径:ELF1开发板资料包\07-NXP原厂资料\07-1NXP官方源码\linux-
    的头像 发表于 07-10 09:54 626次阅读
    ElfBoard技术贴|在NXP<b class='flag-5'>源码</b>基础上适配ELF 1开发板的按键功能

    UCGUI单片机源码

    UCGUI单片机源码
    发表于 07-04 17:11 1次下载

    浙大博导开源飞控planner源码

    浙大博导开源飞控planner源码
    发表于 06-12 11:43 4次下载

    labview实例源码之控压取样系统

    labview源码,包含报表、曲线、通讯等
    发表于 06-06 11:23 1次下载

    什么是源码源码有什么作用?源码组件是什么?源码可二次开发吗?

    源码,也称为源程序,是指未编译的按照一定的程序设计语言规范书写的文本文件,是一系列人类可读的计算机语言指令。
    的头像 发表于 05-25 14:55 1.6w次阅读
    什么是<b class='flag-5'>源码</b>?<b class='flag-5'>源码</b>有什么作用?<b class='flag-5'>源码</b>组件是什么?<b class='flag-5'>源码</b>可二次开发吗?

    HarmonyOS开发:【基于命令行(获取源码)】

    在Ubuntu环境下通过以下步骤获取OpenHarmony源码
    的头像 发表于 04-25 22:08 407次阅读
    HarmonyOS开发:【基于命令行(获取<b class='flag-5'>源码</b>)】

    OpenHarmony开发学习:【源码下载和编译】

    本文介绍了如何下载鸿蒙系统源码,如何一次性配置可以编译三个目标平台(`Hi3516`,`Hi3518`和`Hi3861`)的编译环境,以及如何将源码编译为三个目标平台的二进制文件。
    的头像 发表于 04-14 09:36 957次阅读
    OpenHarmony开发学习:【<b class='flag-5'>源码</b>下载和编译】

    基于Android13的AOSP源码下载及编译指南

    AOSP(Android Open Source Project)是Android操作系统的开源项目,通过下载和编译AOSP源码,您可以获得原始的Android系统,并进行定制和开发。本教程将向您介绍如何下载AOSP源码并进行编译的步骤。
    的头像 发表于 01-17 09:49 4019次阅读
    基于Android13的AOSP<b class='flag-5'>源码</b>下载及编译指南

    Apache Doris聚合函数源码解析

    笔者最近由于工作需要开始调研 Apache Doris,通过阅读聚合函数代码切入 Apache Doris 内核,同时也秉承着开源的精神,开发了 array_agg 函数并贡献给社区。笔者通过这篇文章记录下对源码的一些理解,同时也方便后面的新人更快速地上手源码开发。
    的头像 发表于 01-16 09:52 1054次阅读
    Apache Doris聚合函数<b class='flag-5'>源码</b><b class='flag-5'>解析</b>