3. 实现Reconciler架构

170 阅读4分钟

1. Reconciler的概念

reconciler是React核心逻辑所在的模块,中文名叫协调器(对标Vue的Render)。协调(reconcile)就是diff算法的意思。React是一个纯运行时的框架,没有模版语法,没有编译优化。

image.png

通过jsx的转换,我们能够得到的数据结构是ReactElement,但是它并不能作为reconciler核心模块操作的数据结构,存在的问题如下:

  • 无法表达节点之间的关系
  • 字段有限,不好拓展
export interface ReactElementType {
  $$typeof: symbol | number;
  type: ElementType;
  key: Key;
  props: Props;
  ref: Ref;
  __mark: string;
}

因此,需要一种新的数据结构FeberNode(对标Vue中的VNode),具备以下特点:

  • 介于ReactElement与真实UI节点之间
  • 能够表达节点之间的关系
  • 方便拓展(不仅可以记录节点本身的属性,也能够表达节点接下来会发生的变化)

2. 实现FiberNode

packages目录下新建react-reconciler文件夹,进入文件夹下执行pnpm init,修package.json的内容如下

{
  "name": "react-conciler",
  "version": "1.0.0",
  "description": "React协调器",
  "module": "index.ts",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

由于下述fiber.ts当中会引用shared包下的内容,需要在react-reconcilerpackage.json中添加shared的依赖,添加完成后执行pnpm i,此时react-reconciler包下会出现node_modules/shared

{
  "name": "react-conciler",
  "version": "1.0.0",
  "description": "React协调器",
  "module": "index.ts",
  "dependencies": {
    "shared": "workspace: *"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

react-reconciler下新建src/fiber.ts,其中会存放fiberNode的数据结构

import { Props, Key, Ref } from 'shared/ReactTypes';
import { WorkTag } from './workTags';
import { Flags, NoFlags } from './fiberFlags';

export class FiberNode {
  ref: Ref;
  tag: WorkTag; // FiberNode的tag
  key: Key; //
  stateNode: any; // HostComponent实例 <div> div Dom
  type: any; // FunctionComponent本身 () => {}
  pendingProps: Props; // 工作单元开始工作前的props
  memorizedProps: Props; // 工作单元工作完成后确定下来的props
  alternate: FiberNode | null; // 用于在current和workInProgress之间进行切换,如果当前fiberNode是current,这个字段就会指向workInProgress的fiberNode,反之亦然
  return: FiberNode | null; // 指向父fiberNode
  sibling: FiberNode | null; // 指向同级fiberNode
  child: FiberNode | null; // 指向子fiberNode
  index: number; // 当前FiberNode在同级FiberNode中的序号
  flags: Flags; // fiberNode更新是不同执行方式的标记(移动、删除、新增...)

  constructor(tag: WorkTag, pendingProps: Props, key: Key) {
    this.ref = null;
    this.tag = tag;
    this.key = key;
    this.stateNode = null;
    this.type = null;

    // 构成树状结构
    this.return = null;
    this.sibling = null;
    this.child = null;
    this.index = 0;

    // 作为工作单元
    this.pendingProps = pendingProps;
    this.memorizedProps = null;
    
    this.alternate = null;

    // 副作用
    this.flags = NoFlags;
  }
}

fiber.ts同级目录下创建workTags.ts存放fiberNode的类型

// 标识当前的FiberNode是什么类型的
export type WorkTag =
  | typeof FunctionComponent
  | typeof HostRoot
  | typeof HostComponent
  | typeof HostText;
// 函数式组件
export const FunctionComponent = 0;
// 挂载的根节点
export const HostRoot = 3;
// div
export const HostComponent = 5;
// div下面的文本
export const HostText = 6;

fiber.ts同级目录下创建fiberFlags存放fiberNode执行不同操作的标记

3. Reconciler的工作方式

对于同一个节点,比较其ReactElementfiberNode,生成子fiberNode。并根据比较的结果生成不同的标记(插入、删除、移动...),对应不同宿主环境API的执行。

image.png

举例:

  1. 挂载<div></div>
React Element --> jsx("div") 
对应fiberNode --> null 
生成子fiberNode 
对应标记 --> Placement
  1. <div></div>更新为<p></p>
React Element --> jsx("p")
对应fiberNode --> FiberNode {type: 'div'}
生成子fiberNode
对应标记 --> Deletion Placement

当所有ReactElement比较完后,会生成一颗fiberNode树,一共会存在两颗fiberNode树

  • current: 与视图中真实UI对应的fiberNode树
  • workInProgress: 触发更新后,正在reconciler中计算的fiberNode树

3. 递归遍历

react是以DFS(深度优先遍历)的顺序遍历ReactElement,这意味着:

  • 如果有子节点,优先处理子节点
  • 如果没有子节点,遍历兄弟节点

这是一个递归的过程,存在递、归两个阶段

  • 递:对应beginWork
  • 归:对应completeWork

react-reconciler下新建beginWork.tscompleteWork.ts两个文件,分别对应递归过程中递和归的处理逻辑,文件内容如下

// beginWork.ts 对应递归中的递阶段
export const beginWork = () => {};

// completeWork.ts 对应递归中的归
export const completeWork = () => {};

react-reconciler下新建workLoop.ts文件,用于调用beginWorkcompleteWork,文件内容如下

import { beginWork } from './beginWork';
import { completeWork } from './completeWork';
import { FiberNode } from './fiber';

// 指向全局唯一正在工作的FiberNode
let workInProgress: FiberNode | null = null;

// 用于执行初始化的操作
function prepareFreshStack(fiber: FiberNode) {
  workInProgress === fiber;
}

function renderRoot(root: FiberNode) {
  // 初始化,让workInProgress指向第一个FiberNode
  prepareFreshStack(root);

  // 执行递归的过程
  do {
    try {
      workLoop();
      break;
    } catch (e) {
      console.warn('workLoop发生错误', e);
      workInProgress = null;
    }
  } while (true);
}

function workLoop() {
  while (workInProgress! == null) {
    performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(fiber: FiberNode) {
  const next = beginWork(fiber);

  // fiber执行完,就可以将pendingProps赋值给memorizedProps
  fiber.memorizedProps = fiber.pendingProps;

  // 代表当前fiber没有子节点了,该横向遍历兄弟节点了
  if (next === null) {
    completeUnitOfWork(fiber);
  }
  // 如果next有值,说明没有到最底层,重新赋值workInProgress,继续向下递归
  else {
    workInProgress = next;
  }
}

function completeUnitOfWork(fiber: FiberNode) {
  let node: FiberNode | null = fiber;

  do {
    completeWork(node);
    const sibling = node.sibling;

    if (sibling !== null) {
      workInProgress = sibling;
      return;
    }

    // 如果不存在兄弟节点,应该递归到上一层级
    node = node?.return;
  } while (node !== null);
}

代码整体的执行逻辑如下

image.png

对应的jsx树的执行逻辑如下

image.png