Published on

深入React - part 1

Authors
  • avatar
    Name
    Deng Hua
    Twitter

目录

前情

《深入React》系列内容将由浅入深介绍React的工作原理,讲解中将使用以下Demo,在进入正文前,可以先通览熟悉下Demo。

组件 与 元素

component

react组件用于描述UI界面,通常是一个javascript函数,返回一个 React Element ,一个个 React Element 构成了element tree(元素树), 通常我们使用JSX语法来编写这些元素,UI由组件产生,本质上可以将组件视为一个"蓝图"或"模板"。

react通过组件创建一个或多个组件实例

component

每次我们在代码中的某个地方使用该组件时都会执行此操作,例如示例代码中的<Tab>组件。

component

在代码中,<Tab>组件出现了三次,因此<tab>的三个实例将被插入到"元素树"中,react会调用<Tab>函数组件三次,每个组件一次。

实际上,每个实例都拥有自己的stateprops,也有自己的生命周期。它可以“存活”一段时间,直到最终“死亡”(卸载)。

component

我们都知道,JSX语法本质上其实是在编写javascript。JSX实际上会编译为多个React.createElement函数来调用,当调用时,将返回一个 React Element

React Element 是一个很大的react保留在内存中的不可变javascript对象。包含了创建DOM元素所需的所有信息,最终 React Element 将被"转换"为实际的DOM元素。

最后由浏览器绘制到屏幕上。


React Element

我们可以在JSX中打印出 React Element

console.log(<DifferentContent />);

当在JSX中看到<component></component>这样的语法时,react在内部就会调用对应的组件函数,返回一个 React Element

我们查看控制面板:

React Element

可以看到一些属性,如type属性的值对应了我们的组件名称ƒ DifferentContent()。因为没有传递props所以为空。

我们试着传递一些props:

console.log(<DifferentContent test={23} />);

React Element

现在props属性也有内容了。

我们看到有一个奇怪的属性: $$typeof,实际上它是react实现的一个安全功能,为了保护我们免受XSS跨站脚本攻击,它是一个symbol类型。


react甚至允许我们这样调用组件函数

function DifferentContent() {
  return (
    <div className='tab-content'>
      <h4>I&apos;m a DIFFERENT tab, so i reset state</h4>
    </div>
  )
}
console.log(<DifferentContent test={23} />);
console.log(DifferentContent());

查看控制台,会发现也会有两个 React Element 被打印出来。

React Element

第二个 React Element它的type属性值为div,不再是函数组件名称。并且props属性也不一样,包含了className

这意味着,react不再将其视为react函数组件,它只是一个原始的 React Element

所以,请不要将react组件作为函数直接调用。


React渲染流程概览

How Rendering Works Overview

通过前两个部分,我们大概知道了react组件是如何渲染到画面的。我们已经知道react组件都会被转化为React.createElement被调用,从而创建 React Element

但是对于 React Element 最终到变为UI界面的过程,还是非常模糊。之后将根据以下几个阶段细分说明。

How Rendering Works Overview

1. RENDER IS TRIGGERD —— 触发渲染

新的渲染大部分都是通过更新状态触发的(如setState),当状态变化时会进入渲染阶段

2. RENDER PHASE —— 渲染阶段

在渲染阶段中,react调用组件函数。react需要明白它应该如何更新DOM,来反映最新的变化,而变化信息就在组件中。

但是它不会立即去更改DOM,react对 Render 的定义与传统的 DOM Render 不一样

在react中,Render 不是更新DOM,或者在屏幕上显示元素。渲染阶段从始至终只发生在react核心内部,目前它不会产生任何浏览器UI上的视觉变化。

当react知道如何更新DOM后,进入提交阶段

3. COMMIT PHASE —— 提交阶段

在这个阶段中,新元素可能会被放置在DOM中,已经存在的元素可能会被更新或者删除。实际上,这个提交阶段更像是我们传统上所说的渲染的概念所做的事。

4. BROWSER PAINT —— 浏览器绘制

最后,浏览器会注意到DOM已经更新,重新绘制,这里就和react无关了。


现在详细介绍各个阶段。

RENDER IS TRIGGERD —— 触发渲染

只有两种方式可以触发 Render

  • 应用程序首次运行,也就是Initial-Render(初始渲染)

  • 一个或多个组件中发生了状态更新,也就是常说的Re-Render(重渲染)

总结: 渲染过程实际上是触发的,这是对于整个应用来说,而不是仅仅是某个组件。但这不意味着整个DOM更新,在react的渲染阶段中,只涉及调用组件函数创建React Element

Re-Render(重渲染)实际上不是在状态更新之后立即触发的,它被设计为在Javascript引擎的空闲时间运行(requestIdleCallback)


RENDER PHASE —— 渲染阶段

这里引入一个新概念: component tree(组件树),React官方称之为渲染树,本质是对函数组件的抽象表达。

component tree

注意文档里的: 渲染树仅由 React 组件 组成

在渲染阶段的开始,react将遍历整个组件树,获取所有触发了Re-Render的组件,并调用这些组件函数。

还记得吗,调用组件函数将返回新的 React Element ,这些新的react元素共同组成所谓的Virtual DOM

Virtual DOM

首次渲染时,react会从根节点开始遍历整个组件树,并挨个调用,最终创建了一个大的React Element Tree,这就是我们所说的虚拟DOM

虚拟DOM是一个由所有 React Element 组成的树,来自组件树中的所有组件(调用)。虚拟DOM的创建过程相比传统DOM更快速,内存开销更小。毕竟它只是一个javascript对象。

Virtual DOM

现在,假设组件D中将有一个状态更新,组件D将触发Re-Render,此时react将重新调用组件d函数。

并将组件D新的 React Element 插入到在新的React Element Tree中,最终新的React Element Tree又会生成新的虚拟DOM。

每当react重新Re-Render时,对应组件的所有子组件也会被Re-Render,无论其中的props有没有变化,如果更新的组件返回一个或多个组件,这些嵌套的组件也将在组件树中重新渲染。

这听起来不合理,因为react不知道组件中的更新如何影响子组件,所以默认情况下,react会重新渲染所有。不过此时不意味整个DOM也更新了,目前还只是一个被重新创建的虚拟DOM。

Fiber 与 Fiber Tree

接下来,这个被创建的虚拟DOM,将与之前的虚拟DOM进行重新比对,这个比对过程在react中,称为(Reconcilication - 协调),其中会用到差异比较算法(Diff)。

在React v16中旧的React架构(Stack Reconciler)被重写后,现在的架构被官方称为 Fiber


  • 什么是 Fiber ?

    Fiber不是计算机术语中的新名词,他的中文翻译叫做纤程,与进程(Process)、线程(Thread)、协程(Coroutine)同为程序执行过程。

    React Fiber可以理解为:React内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态。


fibere tree

这个比对过程的结果,将是一个更新后的Fiber Tree,最终它会被用于正确的渲染DOM树。


  • 什么是 Fiber Tree ?

    在React15及之前,是采用堆栈协调(Stack Reconciler)的架构创建虚拟DOM,通过递归调用组件函数来得到要返回的React Element,这个过程遇到子组件就得再去调用子组件的函数,从而形成调用栈的结构),这个过程是同步的,如果组件树很深,将会占用主线程很多时间,这会产生相应的一些问题例如造成卡顿页面、用户的事件没有响应……等等。

    为了解决这个问题,React 想要将这种无法中断的更新重构成非同步可以中断的更新,而曾经依靠的 Virtual DOM 数据结构明显是无法满足这个条件的,于是全新的React Fiber 架构也就随之诞生了。所以通常在 React 16 之后的 Reconciler 也被称为 Fiber Reconciler, 而 Virtual DOM 这个名词 React 官方也有提到说要怕混搞,在 Fiber 架构出现后会尽量避免使用,可以将它看成是 React Fiber 同样要创建一个虚拟的树状结构,但结构跟以前的虚拟 DOM 树的版本已经不一样了,所以通常会命名为 Fiber 树让开发者搞混。


fibere tree

在应用程序的首次运行时,Raect Fiber将对整个React Element Tree生成Fiber Tree

Fiber Tree是一种特殊的树结构,和React Element 不同,Fiber TreeRe-Render不会每次都被重新创建

Fiber树永远都不会被销毁,它是一种可变的数据结构,一旦在首次运行中创建了它,之后只是在Reconcilication过程中被更新。

更重要的是,我们在屏幕上看到的任何组件的实际stateprops,都存储在Fiber Tree中对应的Fiber Node内,每个Fiber单元还将包含一个工作队列,用于更新state,运行副作用,执行DOM更新等。


  • 什么是 Fiber Node ?

    在协调期间,每个 React Element的数据都会合并到 Fiber 节点树中。每个 React Element都有一个对应的 Fiber Node。也是Fiber Tree的组成单元。

    每个Fiber节点保存了各自组件的类型(函数组件/类组件/原生组件...)、对应的DOM节点等信息。也保存了更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)。

    我们可以看一下Fiber Node的属性定义:

    ReactFiber

      function FiberNode(
        tag: WorkTag,
        pendingProps: mixed,
        key: null | string,
        mode: TypeOfMode,
      ) {
        // 作为静态数据结构的属性
        this.tag = tag;
        this.key = key;
        this.elementType = null;
        this.type = null;
        this.stateNode = null;
    
        // 用于连接其他Fiber节点形成Fiber树
        this.return = null;
        this.child = null;
        this.sibling = null;
        this.index = 0;
    
        this.ref = null;
    
        // 作为动态的工作单元的属性
        this.pendingProps = pendingProps;
        this.memoizedProps = null;
        this.updateQueue = null;
        this.memoizedState = null;
        this.dependencies = null;
    
        this.mode = mode;
    
        this.effectTag = NoEffect;
        this.nextEffect = null;
    
        this.firstEffect = null;
        this.lastEffect = null;
    
        // 调度优先级相关
        this.lanes = NoLanes;
        this.childLanes = NoLanes;
    
        // 指向该fiber在另一次更新时对应的fiber
        this.alternate = null;
      }
    

在这张图中,我们看到Fiber Tree以一种奇怪的方式的方式排列 React Element fibere tree

对于每一个子节点,都有一个"link"链接到父节点(App <-> VideoModal <-> Overlay <-> H3)

而所有的子节点也和同级兄弟节点有链接(Video <-> Modal <-> BtnH3 <-> button)。

这种结构就是链表结构。

每个Fiber节点有个对应的React Element,多个Fiber节点靠如下三个属性连接形成树:

// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;

关于具体的链接方式,这里先略过,可查看参考。

可参考文章:

An Introduction to React Fiber - The Algorithm Behind React

A deep dive into React Fiber

Fiber的结构

React Fiber Reconciler

Fiber 为什么是 React 性能的一个飞跃?/ 链表结构


Fiber的Reconcilication过程

以这段代码为例:

应用中,有一个showModal的state,当前设置为trueshowModal的状态会影响<Modal>的存在与否和<Btn>的显示文字。

<App>
  <Video />
  {showModal && (
    <Modal>
      <Overlay>
        <h3>Rate course!</h3>
        <button>5 🌟</button>
      </Overlay>
    </Modal>
  )}

  <Btn>
    {showModal ? 'Rate' : 'Hide'}
  </Btn>
</App>

然后我们将showModal的值更新为false,这将触发Re-Render,这会创建一个新的Fiber Tree,也叫WorkInProgress Tree。在这个新的Fiber Tree中,<Modal>和它的子组件消失了。因为showModalfalse时,它们不显示。


  • 什么是 WorkInProgress Tree ?

    workInProgress 代表当前正在执行更新的 Fiber 树。在 render 或者 setState 后,会构建一颗 Fiber 树,也就是 workInProgress tree,这棵树在构建每一个节点的时候会收集当前节点的副作用,整棵树构建完成后,会形成一条完整的副作用链。


然后,这个新的Fiber Tree需要与之前的Fiber Tree进行比较,替换。这个"之前的Fiber Tree"也有一个新名词,叫CurrentFiber Tree


  • 什么是 CurrentFiber Tree ?

currentFiber tree 表示上次渲染构建的 Fiber Tree。在每一次更新完成后 workInProgress tree 都会替换之前的 currentFiber tree。

可以理解为之前内存中存在的Fiber Tree


动图就是WorkInProgress Tree的生成过程:

Fiber遍历一步一步的遍历整颗树,并分析当前新的Fiber Tree之间需要更改的地方,基于新的Fibe来更新Fiber Tree(替换)。

其中一步一步的比较元素的过程,称为 —— Diffing

fibere tree

<Btn>组件有一些新的文本需要更新,将会被标记为DOM update<Modal><H3><button>组件它们将被标记为DOM delete

一旦这个比对过程结束,所有的这些变化都会被集中到一个列表内,称为effects list,将在下一阶段(COMMIT PHASE提交阶段)被使用。

fibere tree

新 workInProgress tree 的创建过程中,会同 currentFiber tree的对应节点进行 Diff 比较,收集副作用。同时也会复用和 currentFiber tree对应的节点对象,减少新创建对象带来的开销。

参考:

mount时、update时的构建/替换流程


COMMIT PHASE —— 提交阶段

提交阶段,这个阶段 React 会遍历更新队列,将其中需要的变更「一次性」更新到 DOM 上(插入,删除,更新DOM元素)。

React通过渲染过程中最后产生的effects list将它们一个接一个地应用于实际的DOM元素。

提交阶段将是同步执行的,无法异步执行,无法被打断,这是必须的,保证DOM不会只显示部分的最新结果,确保UI界面始终保持一致。

最后一旦提交阶段结束,当前的WorkInProgress Tree将成为下一个渲染周期的CurrentFiber TreeFiber Tree自创建后不会被销毁,不会从零开始创建,它们只会不断的比对更新自身。

提交阶段已经结束,此时新的DOM已经绘制完毕,浏览器会在空闲时间时重新绘制UI。

commit phase

我们都知道,DOM的重新绘制是由浏览器负责的,确切的说是浏览器的渲染进程负责

而渲染阶段是由React负责,涉及到状态更新,Diffing,Fiber树等操作。那么提交阶段是不是也是由React负责呢?

实际上并不是。负责提交阶段的实际上是一个单独的库 —— ReactDOM

React本身不会接触DOM,它根本不知道渲染阶段的结果以及实际提交和需要绘制的地方。React被设计成独立于平台使用,因此React可以与不同的平台结合使用。如React Native,Remoetion等。

reactdom


参考:

React Fiber很难?六个问题助你理解 React Fiber

React技术揭秘

React Fiber Reconciler

React Fiber Architecture

Inside Fiber: in-depth overview of the new reconciliation algorithm in React

In-depth explanation of state and props update in React

The how and why on React’s usage of linked list in Fiber to walk the component’s tree