React的秘密-原理解析第壹篇:核心概念

作为壹个构建用户界面的库,React的核心始终围绕着更新这壹个重要的目标,将更新和极致的用户体验结合起来是React团队壹直在努力的事情。为什麽React可以将用户体验做到这麽好?我想这是基於以下两点原因:

  • Fiber架构和Scheduler出色的调度模式可以实现异步可中断的更新行为。
  • 优先级机制贯穿更新的整个周期

本文是对React原理解读系列的第1篇文章,後续的文章会定期更新,欢迎持续关注。在正式开始之前,我们先基於以上的这两点展开介绍,以便对壹些概念可以先有个基础认知。

配合的源码调试环境在这里 ,会跟随React主要版本进行更新,欢迎随意下载调试。

Fiber是什麽

Fiber是什麽?它是React的最小工作单元,在React的世界中,壹切都可以是组件。在普通的HTML页面上,人为地将多个DOM元素整合在壹起可以组成壹个组件,HTML标签可以是组件(HostComponent),普通的文本节点也可以是组件(HostText)。每壹个组件就对应着壹个fiber节点,许多个fiber节点互相嵌套、关联,就组成了fiber树,正如下面表示的Fiber树和DOM的关系壹样:

    Fiber树                    DOM树

   div#root                  div#root
      |                         |
    <App/>                     div
      |                       /   \
     div                     p     a
    /   ↖
   /      ↖
  p ----> <Child/>
             |
             a

壹个DOM节点壹定对应着壹个Fiber节点,但壹个Fiber节点却不壹定有对应的DOM节点。

fiber 作为工作单元它的结构如下:

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {

  // Fiber元素的静态属性相关
  this.tag = tag;
  this.key = key; // fiber的key
  this.elementType = null;
  this.type = null; // fiber对应的DOM元素的标签类型,div、p...
  this.stateNode = null; // fiber的实例,类组件场景下,是组件的类,HostComponent场景,是dom元素

  // Fiber 链表相关
  this.return = null; // 指向父级fiber
  this.child = null; // 指向子fiber
  this.sibling = null; // 同级兄弟fiber
  this.index = 0;

  this.ref = null; // ref相关

  // Fiber更新相关
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null; // 存储update的链表
  this.memoizedState = null; // 类组件存储fiber的状态,函数组件存储hooks链表
  this.dependencies = null;

  this.mode = mode;

  // Effects
  // flags原为effectTag,表示当前这个fiber节点变化的类型:增、删、改
  this.flags = NoFlags;
  this.nextEffect = null;

  // effect链相关,也就是那些需要更新的fiber节点
  this.firstEffect = null;
  this.lastEffect = null;

  this.lanes = NoLanes; // 该fiber中的优先级,它可以判断当前节点是否需要更新
  this.childLanes = NoLanes;// 子树中的优先级,它可以判断当前节点的子树是否需要更新

  /*
  * 可以看成是workInProgress(或current)树中的和它壹样的节点,
  * 可以通过这个字段是否为null判断当前这个fiber处在更新还是创建过程
  * */
  this.alternate = null;

}

fiber架构下的React是如何更新的

首先要明白,React要完成壹次更新分为两个阶段: render阶段和commit阶段,两个阶段的工作可分别概括为新fiber树的构建和更新最终效果的应用。

render阶段

render阶段实际上是在内存中构建壹棵新的fiber树(称为workInProgress树),构建过程是依照现有fiber树(current树)从root开始深度优先遍历再回溯到root的过程,这个过程中每个fiber节点都会经历两个阶段:beginWork和completeWork。组件的状态计算、diff的操作以及render函数的执行,发生在beginWork阶段,effect链表的收集、被跳过的优先级的收集,发生在completeWork阶段。构建workInProgress树的过程中会有壹个workInProgress的指针记录下当前构建到哪个fiber节点,这是React更新任务可恢复的重要原因之壹。

如下面的动图,就是render阶段的简要过程:

fiberTask

commit阶段

在render阶段结束後,会进入commit阶段,该阶段不可中断,主要是去依据workInProgress树中有变化的那些节点(render阶段的completeWork过程收集到的effect链表),去完成DOM操作,将更新应用到页面上,除此之外,还会异步调度useEffect以及同步执行useLayoutEffect。

这两个阶段都是独立的React任务,最後会进入Scheduler被调度。render阶段采取的调度优先级是依据本次更新的优先级来决定的,以便高优先级任务的介入可以打断低优先级任务的工作;commit阶段的调度优先级采用的是最高优先级,以保证commit阶段同步执行不可被打断。

Scheduler 的作用

Scheduler用来调度执行上面提到的React任务。

何为调度?依据任务优先级来决定哪个任务先被执行。调度的目标是保证高优先级任务最先被执行。

何为执行?Scheduler执行任务具备壹个特点:即根据时间片去终止任务,并判断任务是否完成,若未完成则继续调用任务函数。它只是去做任务的中断和恢复,而任务是否已经完成则要依赖React告诉它。Scheduler和React相互配合的模式可以让React的任务执行具备异步可中断的特点。

优先级机制

为了区分任务的轻重缓急,React内部有壹个从事件到调度的优先级机制。事件本身自带优先级属性,它导致的更新会基於事件的优先级计算出更新自己的优先级,更新会产生更新任务,更新任务的优先级由更新优先级计算而来,更新任务被调度,所以需要调度优先级去协调调度过程,调度优先级由更新任务优先级计算得出,就这样壹步壹步,React将优先级的概念贯穿整个更新的生命周期。

React优先级相关的更多介绍请移步 React中的优先级

双缓冲机制

双缓冲机制是React管理更新工作的壹种手段,也是提升用户体验的重要机制。

当React开始更新工作之後,会有两个fiber树,壹个current树,是当前显示在页面上内容对应的fiber树。另壹个是workInProgress树,它是依据current树深度优先遍历构建出来的新的fiber树,所有的更新最终都会体现在workInProgress树上。当更新未完成的时候,页面上始终展示current树对应的内容,当更新结束时(commit阶段的最後),页面内容对应的fiber树会由current树切换到workInProgress树,此时workInProgress树即成为新的current树。

function commitRootImpl(root, renderPriorityLevel) {
    ...

    // finishedWork即为workInProgress树的根节点,
    // root.current指向它来完成树的切换
    root.current = finishedWork;

    ...
}

两棵树在进入commit阶段时候的关系如下图,最终commit阶段完成时,两棵树会进行切换。
current树和workInProgress树

在未更新完成时依旧展示旧内容,保持交互,当更新完成立即切换到新内容,这样可以做到新内容和旧内容无缝切换。

总结

本文基本概括了React大致的工作流程以及角色,本系列文章会以更新过程为主线,从render阶段开始,壹直到commit阶段,讲解React工作的原理。除此之外,会对其他的重点内容进行大篇幅分析,如事件机制、Scheduler原理、重点Hooks以及context原理。

本系列文章耗时较长,落笔撰写时,17版本还未发布,所以参照的源码版本为16.13.1、17.0.0-alpha.0以及17共三个版本,我曾经对文章中涉及到的三个版本的代码进行过核对,逻辑基本无差别,可放心阅读。


<<:  分享一个好用的进销存软件

>>:  Joining Two More Locales for the 2021 Lottery Millionaire Raffle!

Day9 Topic in Cpp

今天要来讲怎麽用 cpp 写一个 Topic, Topic 是一种异步的通讯方式,一般来说每一个节点...

[Day02] Tableau 轻松学 - Tableau 介绍

Tableau 优点 Tableau 是一种企业级的商业智能 (BI, Business Intel...

[DAY-03] 有顶尖的同事 才有一流的工作环境

团队如果有一两个人能力仅免强胜任 会拉低团队所有人的表现. IF 你团队有五名优秀的下属 那这两个...

DAY 18 制作 Nav Bar - dropdown

针对 dropdown 的部分,我们要来细节微调他的 style ,让他符合 vogue 上的设计,...

[Day2] Jetpack Compose: UI要怎麽排列?

#布局 接续昨天的例子,我们如果新增一个Greeting("Jetpack Compose...