react源码--Fiber的生成

891 阅读8分钟

众所周知当前前端MVVM框架都会面临这一个问题:如何更好的减少频繁DOM更新带来的性能损耗?其中,最主流的做法是通过构建虚拟DOM树,虚拟DOM记录了真实DOM的相关数据,在复杂的更新过程中,利用系统调度将复杂的更新按帧率分割成多个小更新,通过计算出每个时间片虚拟DOM的数据和该时间片前虚拟DOM的数据进行比对,便可得出最小的更新位置和更新内容,最后再按时间片映射进真实的DOM中,这样就能平衡DOM频繁渲染带来的性能损耗和复杂计算带来的渲染阻塞。而在React中便是使用Fiber对标虚拟DOM。本篇主要介绍React中如何创建Fiber。

本篇研究的react是17.0.2版本,相关的代码--> 点击这里

一个简单的组件

先看下面一段代码

// FunctionComponent.ts
import { FC } from 'react';

interface FunctionComponentProps {
  content1: string;
  content2?: string;
}

export const FunctionComponent: FC<FunctionComponentProps> = (props) => {
  const { content1, content2 } = props;
  return (
    <div className="function-component">
      <span>{content1}</span>
      <span>{content2}</span>
    </div>
  );
}

FunctionComponent.defaultProps = {
  content2: '这是content2',
}


// ClassComponent.ts
import { Component } from 'react';

export class ClassComponent extends Component {
  render() {
    return (
      <>
        <span>a</span>
        <span>b</span>
      </>
    );
  }
}


// index.ts
import * as ReactDOM from 'react-dom';
import { FunctionComponent } from './FunctionComponent';
import { ClassComponent } from './ClassComponent';

const app = (
  <div className="app">
    <FunctionComponent content1="test" />
    <ClassComponent />
  </div>
);

ReactDOM.render(
  app,
  document.getElementById('root')
);


运行index.ts后会发生什么呢?react是如何构建虚拟dom的呢?别急我们继续往下看。

从JSX出发

在React开发中,官方推荐使用JSX创建ReactElement,简单来说JSX就是用类似与Html的语法格式通过babel转化成构建节点的一种语法糖。

JSX转化结果可以点击这里 ,在线查看。

通过转化后,我们发现JSX调用的是React.createElement函数构建ReactElementObj,相关结构如下:

// 注意这里不用ReactElement命名是,因为在源码中ReactElement是一个创建ReactElementObj的函数,这里为了避免歧义所以用ReactElementObj
interface ReactElementObj {
  // 标识ReactElementObj的类型
  // 一般由createElement创建$$typeof为 REACT_ELEMENT_TYPE
  $$typeof: numberSymbol;
  // createElemennt传入的type
  type: FunctionComponent | ClassComponent | string | numberSymbol | any;
  // createElement处理的props
  props: Record<string, any>;

  ...
}

function createElement(type: FunctionComponent | ClassComponent | string | numberSymbol | any, config: Record<string, any> | null, ...children: ReactElementObj[]): ReactElementObj;

其中createElement接收的参数:

  1. type: JSX的标签,普通的html标签t为string类型(小写), 自定义标签为FunctionComponent或ClassComponent,React内置标签(Fragment、StrictMode、Suspense...)为特定number或Symbol(如果浏览器支持)。
  2. config: JSX标签的属性,没有则传null。
  3. children: 从第3个参数起表示该标签的孩子节点。

在createElement中主要处理(相关源码----> 点击这里):

  1. 从config分离出config中的key,ref,__self,__source(特殊属性),和props(常规属性)。
  2. 向props中添加children(单项/数组)。
  3. 对props中的属性为undefined且组件存在defaultProps的属性赋默认值。
  4. 返回ReactElement字面量。

回到刚开始开始的那段代码,index.ts中的app是就是一个ReactElementObj的字面量,我们用console.log把它打印出来如下图

众所周知当前前端MVVM框架都会面临这一个问题:如何更好的减少频繁DOM更新带来的性能损耗?其中,最主流的做法是通过构建虚拟DOM树,虚拟DOM记录了真实DOM的相关数据,在复杂的更新过程中,利用系统调度将复杂的更新按帧率分割成多个小更新,通过计算出每个时间片虚拟DOM的数据和该时间片前虚拟DOM的数据进行比对,便可得出最小的更新位置和更新内容,最后再按时间片映射进真实的DOM中,这样就能平衡DOM频繁渲染带来的性能损耗和复杂计算带来的渲染阻塞。而在React中便是使用Fiber对标虚拟DOM。本篇主要介绍React中如何创建Fiber。

本篇研究的react是17.0.2版本,相关的代码--> 点击这里

一个简单的组件

先看下面一段代码

// FunctionComponent.ts
import { FC } from 'react';

interface FunctionComponentProps {
  content1: string;
  content2?: string;
}

export const FunctionComponent: FC<FunctionComponentProps> = (props) => {
  const { content1, content2 } = props;
  return (
    <div className="function-component">
      <span>{content1}</span>
      <span>{content2}</span>
    </div>
  );
}

FunctionComponent.defaultProps = {
  content2: '这是content2',
}


// ClassComponent.ts
import { Component } from 'react';

export class ClassComponent extends Component {
  render() {
    return (
      <>
        <span>a</span>
        <span>b</span>
      </>
    );
  }
}


// index.ts
import * as ReactDOM from 'react-dom';
import { FunctionComponent } from './FunctionComponent';
import { ClassComponent } from './ClassComponent';

const app = (
  <div className="app">
    <FunctionComponent content1="test" />
    <ClassComponent />
  </div>
);

ReactDOM.render(
  app,
  document.getElementById('root')
);


运行index.ts后会发生什么呢?react是如何构建虚拟dom的呢?别急我们继续往下看。

从JSX出发

在React开发中,官方推荐使用JSX创建ReactElement,简单来说JSX就是用类似与Html的语法格式通过babel转化成构建节点的一种语法糖。

JSX转化结果可以点击这里 ,在线查看。

通过转化后,我们发现JSX调用的是React.createElement函数构建ReactElementObj,相关结构如下:

// 注意这里不用ReactElement命名是,因为在源码中ReactElement是一个创建ReactElementObj的函数,这里为了避免歧义所以用ReactElementObj
interface ReactElementObj {
  // 标识ReactElementObj的类型
  // 一般由createElement创建$$typeof为 REACT_ELEMENT_TYPE
  $$typeof: numberSymbol;
  // createElemennt传入的type
  type: FunctionComponent | ClassComponent | string | numberSymbol | any;
  // createElement处理的props
  props: Record<string, any>;

  ...
}

function createElement(type: FunctionComponent | ClassComponent | string | numberSymbol | any, config: Record<string, any> | null, ...children: ReactElementObj[]): ReactElementObj;

其中createElement接收的参数:

  1. type: JSX的标签,普通的html标签t为string类型(小写), 自定义标签为FunctionComponent或ClassComponent,React内置标签(Fragment、StrictMode、Suspense...)为特定number或Symbol(如果浏览器支持)。
  2. config: JSX标签的属性,没有则传null。
  3. children: 从第3个参数起表示该标签的孩子节点。

在createElement中主要处理(相关源码----> 点击这里):

  1. 从config分离出config中的key,ref,__self,__source(特殊属性),和props(常规属性)。
  2. 向props中添加children(单项/数组)。
  3. 对props中的属性为undefined且组件存在defaultProps的属性赋默认值。
  4. 返回ReactElement字面量。

回到刚开始开始的那段代码,index.ts中的app是就是一个ReactElementObj的字面量,我们用console.log把它打印出来如下图

ReactElementObj

其中我们关注的是type和props,由于app的外层是div html普通的标签所以type值为‘div’,props则取得div 中的属性className和下级节点的ReactElementObj,我们再观察子项ReactElementObj的type都为componet导入的部分。

经过这一步的分析我们得出在ReactDOM.render 中第一入参其实是一个ReactElementObj字面量,而ReactDOM.render 中首要任务便是通过传入的ReactElementObj构建出整个虚拟Dom树---FiberTree

Fiber基本结构

做为基本虚拟DOM,至少包含:真实DOM的数据、渲染方式和与其他DOM的关系。

而Fiber的基本结构如下:

interface FiberNode {

  // 标识Fiber的类型
  tag: WorkTag;

  // dom相关数据
  // 本次渲染的Props;对标ReactElementObj中的props
  pendingProps: Record<string, any>;
  // 本次渲染前的Props
  memoizedProps: Record<string, any>;
  // 渲染前的State
  memoizedState: Record<string, any>;

  // 渲染方方式,(生成子Fiber相关)
  // 对标ReactElementObj的type
  type: FunctionComponent | ClassComponent | string | number | Symbol | any;
  // 当type为ClassComponent时,stateNode指向实例化后的对象。
  // 当为react内部类型时,stateNode则指向对应系统相关类型渲染需要的工具类对象
  stateNode: any;
 
  // 与其他fiber的关系
  // 指向父本Fiber
  return: FiberNode | null;
  // 指向第一个孩子节点
  child: FiberNode | null;
  // 指向下一个兄弟节点
  sibling: FiberNode | null;
  // 记录在兄弟节点(父本所有子节点)中的位置
  index: number;

  ...
}

实现源码---> 点击这里

构建Fiber树

react的整颗fiber树是由rootFiber为根引出来的。根据ReactElementObj类型我们大概可以把Fiber分为3种类型:

  1. 普通html标签:下一级的节点记录在该fiber的pendingProps.children中,通过ReactElementObj转化为Fiber节点,当该节点的pendingProps.children为空或为string(文本节点)时证明该节点为顶级节点(没有子节点)。
  2. react内部提供标签:下一级的节点由react内部自己维护的逻辑生成子节点
  3. 自定义组件: 下一级的节点存在于当前fiber的type中(FunctionComponent:直接调用可以生成ReactElementObj, ClassComponent:实例化type后调用render()方法)生成ReactElementObj,在通过ReactElementObj转化为下级节点。

react把每个fiber当成生成fiber最小单元只要迭代所有fiber则到顶级Fiber时整颗FiberTree便生成了。

源码中主要通过调用workLoopSync实现fiber迭代:

ReactFiberWorkLoop

整体源码

其中生成下一个迭代fiber核心在于beginWork:

beginWork

其中主要分析自定义的组件和普通组件如何生成子fiber其他的类型都非常相似。

FunctionComponet:

FunctionComponent

ClassComponent:

ClassComponent

普通标签

HostComponent

相关源码

由此可见beginWork核心思想就是根据不同的fiber.tag产生不同的获取子ReactElementObj的方法,在交由reconcileChildren转化、生成、调和Fiber,最返回workInProgress.child(当前fiber的第一个子fiber)。

recconcileChildren

其中mountChildFibers的核心为 reconcileChildFibers :

reconcileChildFiber

reconcileChildFiber

相关源码

以上就是FiberTree生成的相关逻辑。

总结

  1. 构建react虚拟DOM树的量子是Fiber,Fiber维护了下级DOM的数据、渲染方式、及Fiber的关系。
  2. react中的JSX都是通过调用React.createElement生成相关的ReactElementObj,再由ReactElementObj转化生成FiberTree。
  3. react通过的深度优先遍历(DFS)的模型迭代出整颗FiberTree

而文章开头给出的代码其实html结构如下图:

fiber-dom.jpeg

转换成FiberTree如下图:

FiberTree.jpg