「04」实现Reconciler架构

497 阅读5分钟

明确reconciler的作用

在前端框架出现之前,我们熟知的jQuery过程驱动的,也就是开发者去调用宿主环境的API,然后更新到真实的UI

在前端框架出现之后,我们熟知的react等框架是状态驱动的,也就是开发者只需要描述UI,然后框架的运行时核心模块去调用宿主环境的API,再更新到真实的UI

  • react的运行时核心模块是reconciler,而vue的运行时核心模块是renderer
  • react的UI描述方法是JSX,而vue的UI描述方法是模板语法

对于react来说,消费的是jsx,并且没有编译优化,而其他框架有编译优化,也就是说react是一个纯运行时的前端框架,我们的核心模块可以开放通用的API供不同的宿主环境使用,这也就是跨端的方式,比如React NativeReact ARReact

明确reconciler消费JSX的过程

在前面我们实现了jsx方法来转换jsx代码,最后返回一个ReactElement数据结构,但是ReactElement无法作为reconciler操作的数据结构,主要是存在以下问题:

  • ReactElement是没有办法表达节点之间的关系的,只能表达它内部有什么数据
  • ReactElement字段有限,不好拓展,数据只能用于表达当前数据状态,并不能表达当前节点,接下来会发生什么变化

所以我们需要实现一种新的数据结构,他的特点是:

  • 介于ReactElement与真实UI节点之间
  • 能够表达各节点之间的关系
  • 方便拓展(不仅能作为数据存储单元,也能作为工作单元)

这就是FiberNode(虚拟DOM在React中的实现)
VNode即虚拟DOM在Vue中的实现

Fiber

我们需要明确Fiber的三层含义

  • 作为架构来说,React16之前的Reconciler采用的是递归方式执行,数据保存在递归调用栈中,所以被称为stack Reconciler,React16之后的Reconciler基于Fiber节点实现,被称为Fiber Reconciler
  • 作为静态的数据结构,每个Fiber节点对应一个组件,保存了该组件的类型、对应的DOM节点等信息
  • 作为动态的工作单元,每个Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)

首先来实现FiberNode的数据结构

  • 新建packages/react-reconciler/src/fiber.ts
export class FiberNode{
    type:any;
    tag:WorkTag;
    pendingProps:Props;
    key:Key;
    stateNode:any;
    ref:Ref
    
    return:FiberNode|null
    sibling:FiberNode|null;
    child:FiberNode|null;
    index:number
    
    meoizedProps:Props|null;
    alternate:FiberNode|null;
    flags:Flags;
    
    constructor(tag:WorkTag,pendingProps:Props,key:Key){
        // 实例
        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.ref=null
        
        // 作为工作单元
        this.pendingProps=pendingProps
        this.memoizedProps=null
        
        this.alternate=null
        
        // 副作用
        this.flags=Noflags
      }
   }
  • 作为一个实例:属性有tag即Fiber对应组件的类型Function/Class/HostkeystateNodetype,比如一个HostComponentdiv,则stateNode保存的就是div DOM,即对应的真实DOM节点
  • 作为表现各节点之间的关系:属性有return指向父节点,sibling指向兄弟节点,child指向子节点,index例如<ul>li*3</ul>下面有三个li同级,则第一个li为0,第二个li为1
  • 作为工作单元pendingProps是这个节点刚开始工作的propsmemoizedProps是工作确定完了之后的props

我们知道ReactElement是一个存储数据的单元,而FiberNode也可以存储数据,所以我们可以对其中的数据进行比较,根据比较的结果,就能产生很多标记,如果FibrtNodenull的话,比较的结果就会生成子FiberNode,同时对应Placement的标记插入。

我们目前还需要FiberNode类型标识

新建packages/react-reconciler/src/workTags.ts

// FiberNode节点类型标识
export type WorkTag =
	| typeof FunctionComponent
	| typeof HostRoot
	| typeof HostComponent
	| typeof HostText;

export const FunctionComponent = 0;
export const HostRoot = 3;
export const HostComponent = 5;
export const HostText = 6;

FiberNode动作标记

新建packages/react-reconciler/src/fiberFlags.ts

export type Flags = number;

export const NoFlags = 0b00000000000000000000000000;
export const Placement = 0b00000000000000000000000010;
export const Update = 0b00000000000000000000000100;
export const ChildDeletion = 0b00000000000000000000010000;

render流程

render阶段,会有两个阶段分别是阶段和阶段

递阶段方法为beginWork:与ReactElement进行比较并返回FiberNode

// 递归中的递阶段

export const beginWork = () => {
	// 比较,返回子fiberNode
};

归阶段方法为completeWork

// 归阶段
export const completeWork = () => {
	// 递归中的归
};

实现大体的工作流程

新建packages/react-reconciler/src/workLoop.ts

首先需要一个全局的指针指向正在工作的单元workInProgress

// 需要全局的指针指向当前正在工作的单元
let workInprogress: FiberNode | null = null;

实现一个renderRoot函数,调用prepareRefreshStack函数初始化,将指针指向第一个FiberNode,之后执行递归流程,

function prepareFreshStack(fiber: FiberNode) {}

function renderRoot(root: FiberNode) {
	// 初始化 让workInprogress指向第一个fiberNode
	prepareFreshStack(root);
	// 执行递归流程
	do {
		try {
			workLoop();
			break;
		} catch (e) {
			console.warn('workLoop发生错误', e);
			workInprogress = null;
		}
	} while (true);
}

只要指针不为null我们就继续执行performUnitOfWork

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

如果有子节点就遍历子节点,如果没有就调用归阶段流程

function performUnitOfWork(fiber: FiberNode) {
	// 可能是fiber的子fiber也可能是null
	const next = beginWork(fiber);

	fiber.memoizedProps = fiber.pendingProps;
	// 如果是子fiber继续向下遍历
	if (next === null) {
		completeUnitOfWork(fiber);
	} 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;
		workInprogress = node;
	} while (node !== null);
}

所有代码:github.com/ohlyf/oh-re…