阅读 414

React学习十二天---React源码解读之React16架构和使用的技术介绍(二)

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

项目源码

前言

前面我们做好了源码调试环境,学习了JSX转化为createElement的过程,现在我们接下来了解一下:

  • React16所采用的架构,对一个架构的整体了解
  • 从数据结构方面了解Fiber架构
  • 双缓存技术的一个简单介绍

在继续阅读React16源码之前,我们通过今天的学习,我们对React16有个整体的认知,了解React16的核心概念,看看人家是怎么通过技术解决对应需求问题的!!!

React架构

React16 版本的架构可以分为三层: 调度层,协调层,渲染层.

  • Scheduler(调度层): 调度任务的优先级,高优任务可以优先进入协调器
  • Reconciler(协调层): 构建Fiber数据结构,比对Fiber对象找出差异,记录Fiber对象要进行的DOM操作
  • Renderer(渲染层): 负责将发生变化的部分渲染到页面上

Schedule 调度层

1. 面对问题

在React15当中,是没有调度层的。为什么要在React16当中加入调度层呢?因为在React15中,采用了循环加递归的方式进行VirtualDOM的比对,由于递归使用JavaScript自身执行栈。一旦开始就没有办法停止,只能等到任务执行完成。如果VirtualDOM树的层级比较深,VirtualDOM的比对就会长期占用JavaScript主线程,由于JavaScript又是单线程的无法同时执行其他任务,所以在对比的过程中无法响应用户操作,无法即时执行元素动画,造成了页面卡顿的现象

2. 解决方案

在React16的版本中,放弃了JavaScript递归的方式进行VirtualDOM的比对,而是采用循环模拟递归。而且比对的过程是利用浏览器的空闲时间完成的,不会长期占用主线程,这就解决了virtualDOM比对造成页面卡顿的问题。

3. 参考方案

在window对象中提供了requestIdleCallback API,他可以浏览器的空闲时间执行任务,但是它自身也存在一些问题,比如说并不是所有浏览器都支持它,而且他的触发频率也不是很稳定,所以React最终放弃了requestIdleCallback的使用。

4. 实际解决方案

在React中,官方实现自己的任务调度库,这个库就叫做Scheduler。他也可以实现在浏览器空闲时执行任务,而且还可以设置任务的优先级,高优先级任务限制性,低优先级任务后执行

Schduler 存储在 package/scheduler文件夹中

image.png

Reconciler 协调层

在React 15中,协调器和渲染器交替执行,即找到了差异就直接更新差异。在React16中,这种情况发生了变化,协调器和渲染器不再交替执行。协调器负责只找出差异,所有差异找出之后,统一交给渲染器进行DOM的更新。也就是说协调器的主要任务就是找出差异部分,并且为了差异打上标记

Renderer 渲染层

渲染器根据协调器为Fiber节点打的标记,同步执行对应的DOM操作。

既然比对的过程从递归 变成了 可以中断的循环,那么React是如何解决中断更新时DOM渲染不完全的问题呢?

其实根本就不存在这个问题,因为在整个过程中,调度器和协调的工作是在内存中完成的是可以被打断的渲染器的工作被设定成不可以被打断,所以不存在DOM不完全的问题。

Fiber数据结构

我们来看下,React16中的Fiber数据结构,Fiber其实就是普通的JavaScript对象,是由VirtualDOM对象演变而来的,在Fiber的对象当中有很多属性,我会介绍一些比较重要的对象,一起认识Fiber的数据结构。

虽然Fiber属性比较多,但是我们在阅读源码的时候会将一些属性进行归类。这样我们会更容易理解一些。

大体我们可以把他们分为四类:

  • 和DOM实例相关
    • tag, type, stateNode
  • 和构建Fiber树相关
    • return, child, sibling,alternate
  • 和组件状态相关
    • pendingProps, memoizedProps, memoizedState
  • 和副作用相关
    • UpdateQueue, EffectTag

和DOM实例相关

1. tag

用来区分组件的类型,当前的Fiber对象表现的是函数组件,还是类组件,还是普通的React元素,还是一些其他的组件类型呢?tag属性是用来区分它们的。

tag属性值我们可以去查看WorkTag,其实WorkTag的值为0到22,不同的数值表示不同的组件类型。

比如0表示函数组件,比如1表示类组件,比如3表示当前组件挂载点对应的Fiber对象,默认情况就是ID为root的那个div对应的Fiebr对象。5代表普通的React节点,比如div,span这样的节点。

还有其他的我们暂时忽略他,我们也记不住,也没有必要记住他们。在后面我们看源码遇到了回来查看WorkTag就可以了

WorkTag 文件位置src/react/packages/shared/ReactWorkTags.js

image.png

2. type

type这个在之前的学习中,我们已经很熟悉了,其实就是之前createdElement方法的第一个参数,表示节点的类型。如果当前节点是div,或者span,type属性中存储的值就是字符串类型的”div“或者”span“。如果当前元素是组件,那么type存储的就是组件的构造函数。

3. stateNode

这个属性我们也不陌生,在模拟React源码时我们就使用过,如果当前Fiber对象表示的是普通的DOM节点stateNode存储的就是节点对应的真实DOM对象。如果当前Fiber对象表示的是类组件,stateNode存储的就是组件的实例对象。如果当前Fiber对象表示的函数组件,stateNode存储的就是null,因为函数组件没有实例

和构建Fiber树相关

1. return, child, sibling

Fiber树和DOM树,可以说其实是对应的关系,但不是完全对应的。在DOM树中有子节点,父节点,兄弟节点这样的定义和关系,因为有这些才能构成树这种数据结构Fiber树也一样,也有这些概念,return表示的是父级Fiber节点child表示是子级Fiber节点sibling表示的是下一个兄弟Fiber节点。这样的话我们无论处于当前哪个节点,都可以通过该节点找到子级,父级以及同级的Fiber节点。

alternate

这个属性也是用于构建Fiber树相关的属性,这里先不说,下面还会介绍一个双缓存技术会重点提及alternate,更有助于理解的连贯性,这里暂时提一下,在下面会和双缓存技术重点解释。

和组件状态相关的属性

1. pendingProps

该属性存储的是组件即将更新的props,

2. memoizedProps

存储的是上一次组件更新后的前props,就是旧的props

3. memoizedState

存储的是上一次组件更新后的前state,就是旧的state

和副作用相关的属性

副作用值得就是可以出发DOM操作的属性

1. UpdateQueue

从属性命名可以看得出来,它表示的是任务队列,当前的Fiber对应的组件要执行的任务,比如组件的状态更新,再比如组件的初始化渲染,都属于任务的一种,都会存储在这个任务队列当中。

既然是任务队列,在队列当中就可以存储多个任务。在什么情况下会存在多个任务呢?比如在组件当中多次调用setState方法,进行状态的更新。在setState方法被调用后,更新并不是马上发生的,react会将多个更新任务放在这个队列当中,最后执行批量更新操作。updateQueue属性值其实就是JavaScript对象,updateQueue对象会以链表的方式存储一个一个需要更新的任务。具体是如何实现的我们在后面的源码学习中会了解到。

2. EffectTag

他表示的是当前对应的Fiber节点对应的DOM节点需要进行什么样的操作。他的值我们可以查看sideEffectTag(文件位置: src/react/packages/shared/ReactSideEffectTags.js)的值,它的值都是以二进制的方式进行存储的,但在代码运行的时候我们可以得到十进制的数值。

比如0 是NoEffect表示的是当前DOM不需要进行任何操作,比如1表示的是PerformedWork表示的是当前的DOM节点操作已经完成。当fiber节点对应的DOM操作执行完成以后EffectTag属性值会重置为1(PerfoormWork),再比如2(Placement)表示当前DOM节点要被插入到当前页面当中。再比如4(Update)表示当前节点需要被更新,再比如8(Deletion)表示当前DOM节点需要被删除。

export type SideEffectTag = number;

// Don't change these two values. They're used by React Dev Tools.
export const NoEffect = /*              */ 0b0000000000000; // 0
export const PerformedWork = /*         */ 0b0000000000001; // 1

// You can change the rest (and add more).
export const Placement = /*             */ 0b0000000000010; // 2
export const Update = /*                */ 0b0000000000100; // 4
export const PlacementAndUpdate = /*    */ 0b0000000000110; // 6
export const Deletion = /*              */ 0b0000000001000; // 8
export const ContentReset = /*          */ 0b0000000010000; // 16
export const Callback = /*              */ 0b0000000100000; // 32
export const DidCapture = /*            */ 0b0000001000000; // 64
export const Ref = /*                   */ 0b0000010000000; // 128
export const Snapshot = /*              */ 0b0000100000000; // 256
export const Passive = /*               */ 0b0001000000000; // 512
export const Hydrating = /*             */ 0b0010000000000; // 1024
export const HydratingAndUpdate = /*    */ 0b0010000000100; // 1028

// Passive & Update & Callback & Ref & Snapshot
export const LifecycleEffectMask = /*   */ 0b0001110100100; // 932

// Union of all host effects
export const HostEffectMask = /*        */ 0b0011111111111; // 2047

export const Incomplete = /*            */ 0b0100000000000; // 2048
export const ShouldCapture = /*         */ 0b1000000000000; // 4096

复制代码

3. firstEffect,nextEffect,lastEffect

这些属性存储的是当前的fiber节点的子级fiber节点,是需要执行副作用的fiber节点,在前面我们模拟React源码的时候,我们使用的是effects数组来存储当前fiber节点的所有子节点。我们可以将这三个属性理解为effectsList节点,理解为副作用的列表容器,它是单链表结构,firstEffect是子树种第一个side effect,lastEffect是最后一个,中间的所有effect都是nexteffect进行存储。如图所示:

image.png

4. expirationTime

存储的是任务的过期时间。如果因为任务优先级的关系,任务迟迟没有得到执行,如果超过任务过期时间,react会强制执行该任务。如果是同步任务这个过期时间会被设置成一个很大的数值,在我们后面调试源码的时候,如果你看到这个值是一个比较大数值,我们应该想到他是一个同步任务。

5. mode

表示当前Fiber节点的模式,模式的值可以参考 TypeOfMode(位置: src/react/packages/react-reconciler/src/ReactTypeOfMode.js) 0表示同步渲染模式,1表示严格模式,10表示异步渲染过渡模式,100 异步渲染模式,1000 性能测试模式,在react当中组件也分为很多种,不同组件可以使用不同的模式

export type TypeOfMode = number;

// 0 同步渲染模式
export const NoMode = 0b0000;
// 1 严格模式
export const StrictMode = 0b0001;
// 10 异步渲染过渡模式
export const BlockingMode = 0b0010;
// 100 异步渲染模式
export const ConcurrentMode = 0b0100;
// 1000 性能测试模式
export const ProfileMode = 0b1000;
复制代码

以上就大概讲解了一些fiber主要属性的含义。

双缓存技术

在React中,DOM的更新采用了双缓存技术,双缓存技术致力于更快速的DOM更新。

什么是双缓存技术呢?

举个例子

举个例子,使用canvas绘制动画时,在绘制每一帧前都需要清除上一帧的画面,清除上一帧需要花费时间,如果当前帧画面计算量又比较大,又需要花费比较长的时间,这就导致上一帧清除到下一帧显示中间会有较长的间隙,就会出现白屏。

解决方案

为了解决这个问题,我们可以在内存中绘制当前帧动画,绘制完之后直接用当前帧替换上一帧画面,这样的话在帧画面替换的过程中就会节约非常多的事件,就不会出现白屏问题,这种在内存中构建并直接替换的技术做双缓存

React中实现思路

React使用双缓存技术完成Fiber书的构建与替换,实现DOM对象的快速更新。

在React中最多会同时存在两颗Fiber树,当前在屏幕中显示的内容对应的Fiber树叫做Current Fiber树,当发生更新时,React会在内存中重新构建一颗新的Fiber树,这颗正在构建的Fiber树叫做workinProgress Fiebr树,在双缓存技术中,workinProgress Fiber树就是即将要显示在页面中的Fiber树,当这颗Fiber树构建完成后,React会使用它直接替换current Fiber树达到快速更新DOM的目的,因为workInProgress Fiber树在内存中构建的所以构建他的速度是非常快

一旦workInProgress树在屏幕上出现,他就变成current Fiber树

具体实现的数据结构

current Fiber树 和 workProgress Fiebr树是存在联系的,因为每次构建workProgress Fiber树的时候并不是全部的fiber树属性的构建,实际上很多属性是直接可以复用current Fiber树的。所以在代码层面两者需要建立关联关系,这个关联关系是如何建立的呢?

在current Fiber节点对象中有一个alternate属性指向对应的workProgress Fiebr节点对象,在workInProgress节点中也有一个alternate属性也指向对应的currentFiber节点对象。他们关联关系就是依靠上面所讲的alternate属性进行实现的。

image.png

这张图呢在最顶端是fierRootNode对象(先忽略),在最左侧rootFiber表示的是组件的挂载点对应的fiber对象(即对应的id为root的div对应的fiber对象),react在初始渲染时,会先构建这个div所对应的fiber对象,构建完这个fiber对象以后又将这个对象看成是current fiber树,接下来React会在这个fiber对象添加一个属性alternate,属性值是current Fiber对象的拷贝。将拷贝的current Fiebr对象当做workInprogress Fiber树。当然在workInprogress Fiber树种也添加了alternate属性,属性值指向的是currentfiber树。接下来的工作构建子级fiber对象的工作就在workInprogress fiber树种进行。比如app组件 和 p元素的构建。当所有fiber对象构建完成后以后,使用workInputProgress fiber树替换current Fiber树。 image.png 这样就完成了fiber节点的构建和替换了。替换完成后,workInProgress Fiber树就替换成了current Fiber树。Fiber里面是存储了DOM节点对象的,也就是说DOM对象的构建呢是在内存中完成构建的。当所有的fiber对象完成构建以后,DOM对象也构建完成后了。这样就可以使用内存中的DOM对象替换页面中的DOM对象了

这就是react当中使用的双缓存技术,目的就是实现更快速的DOM更新。

区分fiberRoot(fiber对象的根) 和 rootFiber(就是根DOM对应fiber对象)

fiberRoot 表示Fiber数据结构对象,是Fiber数据结构中的最外层对象

rootFiber表示组件挂载点对应的Fiebr对象,比如React应用中默认的组件挂载点就是id为root的div

fiberRoot包含rootFiber,在fiberRoot对象有一个current属性,存储的就是rootFiber

rootFiber指向fiberRoot,在rootFiber对象中有一个stateNode属性,指向fiberRoot

在React应用中FiberRoot只有一个,而rootFiber可以有多个,因为render方法可以调用多次的

fiberRoot会记录应用的更新信息,比如协调器在完成工作后,会将工作成果存储在fiberRoot中。

image.png

总结

在这篇学习笔记中,我们介绍了React16库的架构和相关技术。这里我们回顾一下今天介绍的知识点: React16的架构可以分为三层:调度层,协调层,渲染层。

调度层,因为React15采用的递归加循环,递归不能停止导致页面会出现卡顿问题,需要解决这个问题react首先采用循环,然后需要在高优任务(页面渲染等)执行完成之后执行低优先fiber构建和比对任务,且低优先任务是可以暂停打断一种实现方案,window全局也有requestIdleCallback可以实现,但是有些浏览器还不能兼容,也不稳定。所以React自己完成了调度层的实现,做到了调度任务的优先级,高优任务可以优先进入协调器的工作

然后在协调层中使用循环,完成构建Fiber数据结构,比对Fiber对象找出差异,记录Fiber对象要进行的DOM操作。这里将VirtualDOM树的节点生成一个个小的Fiber对象,构建一个Fiber树,也就拆分成一个个小任务,在浏览器闲暇的时候执行一个个小任务完成fiber对象的比对,而且使用双缓存技术将DOM在内存中即可完成构建和替换,为实现更快的更新DOM打下基础

最后在renderer层将发生变化的部分渲染到页面上,调度器和协调器工作是在内存中是可以被打断的,渲染器在这个阶段是不可以被打断的。

希望通过这次对React16架构和相关的技术的介绍,大家会对React16有一个系统的认知,日拱一卒,欢迎点赞,评论探讨,关注,加油!!!

文章分类
前端
文章标签