New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

学习与理解 React Fiber #44

Open
creeperyang opened this Issue Mar 21, 2018 · 2 comments

Comments

Projects
None yet
3 participants
@creeperyang
Owner

creeperyang commented Mar 21, 2018

React 在 v16 引入了众多新特性,其中最核心的更新属于引入了新的核心架构 Fiber (Fiber reconciler,代替之前的 Stack reconciler),本文主要是对 fiber 的学习过程的记录。

一、为什么需要 Fiber ?

长话短说就是:性能

In its current implementation React walks the tree recursively and calls render functions of the whole updated tree during a single tick.

React 15 及之前版本,协调算法(Stack Reconciler)会一次同步处理整个组件树。它会递归遍历每个组件(虚拟DOM树),去比较新旧两颗树,得到需要更新的部分。这个过程基于递归调用,一旦开始,很难去打断。也就是说,一旦工作量大,就会堵塞整个主线程(The main thread is the same as the UI thread.)。

而事实上,我们的更新工作可能并不需要一次性全部完成,比如 offscreen 的 UI 更新并不紧急,比如 动画 需要优先完成——我们可以根据优先级调整工作,把diff过程按时间分片!

If something is offscreen, we can delay any logic related to it. If data is arriving faster than the frame rate, we can coalesce and batch updates. We can prioritize work coming from user interactions (such as an animation caused by a button click) over less important background work (such as rendering new content just loaded from the network) to avoid dropping frames.

所以 React 引入了 Fiber。

二、Fiber 是什么?

Fiber 的基本目标是可以利用 scheduling(scheduling 即决定工作什么时候执行),即可以:

  • 暂停工作,并在之后可以返回再次开始;
  • 可以为不同类型的工作设置优先级;
  • 复用之前已经完成的工作;
  • 中止已经不再需要的工作。

要达成以上目标,首先我们必须能把工作分成小单元(break work down into units)。从这一点来说,A fiber represents a unit of work。

进一步讲,React 的一个核心概念是 UI 是数据的投影 ,组件的本质可以看作输入数据,输出UI的描述信息(虚拟DOM树),即:

ui = f(data)

也就是说,渲染一个 React app,其实是在调用一个函数,函数本身会调用其它函数,形成调用栈。前面我们已经讲到,递归调用导致的调用栈我们本身无法控制,
只能一次执行完成。而 Fiber 就是为了解决这个痛点,可以去按需要打断调用栈,手动控制 stack frame——就这点来说,Fiber 可以理解为 reimplementation of the stack,即 virtual stack frame

React Fiber is a virtual stack frame, with React Fiber being a reimplementation of a stack frame specialized for React components. Each fiber can be thought of as a virtual stack frame where information from the frame is preserved in memory on the heap, and because info is saved on the heap, you can control and play with the data structures and process the relevant information as needed.

三、Fiber 的简易实现

这一节本来是要直接去探索 React 怎么实现 Fiber 的。但 Rodrigo Pombo 有篇非常棒的自定义 Fiber 实现博文,这里先讲一讲这个实现,有助于我们理解 Fiber 到底是什么,是怎么实现手动控制 stack frame 的。

我阅读了 Rodrigo Pombo 的实现,并用 typescript 重写了一遍(有助于我自己理解),并加上了详细的注释(理解有谬误的大家可以帮忙提出):

import { Component, createInstance } from './component';
import { createDomElement, updateDomProperties } from './dom-utils';
import { Effect, IdleDeadline, IdleRequestCallback, IFiber, ITag, IUpdate, IVNode } from './interface';
declare var requestIdleCallback: (fn: IdleRequestCallback) => number;
// 毫秒,检测 deadline.timeRemaining() 是否有足够空余时间。
const ENOUGH_TIME = 1;
// 追踪/缓存 pending updates,空闲时执行这些更新
const updateQueue: IUpdate[] = [];
let nextUnitOfWork: IFiber | null | undefined = null;
let pendingCommit: IFiber | null = null;
/**
* 把 virtual DOM tree(可以是数组)渲染到对应的容器 DOM
* @param elements VNode elements to render
* @param containerDom container dom element
*/
export function render(elements: any, containerDom: HTMLElement) {
// 把 update 压入 updateQueue
updateQueue.push({
from: ITag.HOST_ROOT,
dom: containerDom,
newProps: { children: elements },
});
requestIdleCallback(performWork);
}
/**
* 安排更新,通常是由 setState 调用。
* @param instance 组件实例
* @param partialState state,通常是对象
*/
export function scheduleUpdate(instance: Component, partialState: any) {
// 把 update 压入 updateQueue
updateQueue.push({
// scheduleUpdate 只被 setState 调用,所以来源一定是 CLASS_COMPONENT
from: ITag.CLASS_COMPONENT,
// 相应组件实例
instance,
// setState 传来的参数
partialState,
});
// 下次空闲时开始更新
requestIdleCallback(performWork);
}
/**
* 执行渲染/更新工作
* @param {IdleDeadline} deadline requestIdleCallback 传来,用于检测空闲时间
*/
function performWork(deadline: IdleDeadline) {
workLoop(deadline);
if (nextUnitOfWork || updateQueue.length > 0) {
requestIdleCallback(performWork);
}
}
/**
* 核心功能,把更新工作分片处理(可打断);处理结束后进入提交阶段(不可打断)。
*
* 1. 通过 deadline 去查看剩余可执行时间,时间不够时暂停处理;
* 2. 把 wip fiber tree 的创建工作分片处理(可分片/暂停,因为没有操作DOM);
* 3. 一旦 wip fiber tree 创建完毕,同步执行 DOM 更新。
* @param {IdleDeadline} deadline requestIdleCallback() 的参数
*/
function workLoop(deadline: IdleDeadline) {
// 如果 nextUnitOfWork 为空,则重新开始分片工作。
if (!nextUnitOfWork) {
resetNextUnitOfWork();
}
// 如果 nextUnitOfWork 非空,且剩余空闲时间足够,继续 reconcile
// 实质上是在构造新的 work-in-progress fiber tree
while (nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (pendingCommit) {
commitAllWork(pendingCommit);
}
}
/**
* 重新开始分片工作 (next unit of work),设置reconciler的起点。
*/
function resetNextUnitOfWork() {
// 从 updateQueue 从取出最早的 update,如果没有,说明无更新要做,结束。
const update = updateQueue.shift();
if (!update) {
return;
}
// 如果有 partialState (说明一定是 setState,一定是 Class Componnet)
// 则把 partialState 存到对应的(old)fiber 上。
if (update.partialState) {
((update.instance as Component).__fiber as IFiber).partialState = update.partialState;
}
// 获取 root fiber
// 1. 如果是 Tag.HOST_ROOT,说明是 React.render() ,直接拿 update.dom._rootContainerFiber;
// 2. 否则是 Class Componnet,从 update.instance.__fiber 一路往上拿到 root fiber。
const root =
update.from === ITag.HOST_ROOT
? (update.dom as any)._rootContainerFiber
: getRoot((update.instance as Component).__fiber as IFiber);
// nextUnitOfWork (reconciler的起点)被设置为一个 fiber,
// 这个 fiber 是一个全新的 work-in-progress fiber tree 的 root
nextUnitOfWork = {
tag: ITag.HOST_ROOT, // 标记为 root
// 1. 如果之前没有 old tree(即是 React.render),则设为传来的参数 DOM;
// 2. 否则复用之前的 root.stateNode。
stateNode: update.dom || root.stateNode,
// 1. 如果之前没有 old tree(即是 React.render),则props 是 newProps;
// 2. 否则共享原来的 props。
// 如果使用 newProps,我们知道,children 是什么将无法保证。
props: update.newProps || root.props,
// 对应的 old fiber tree(React.render 时为 null)
alternate: root,
};
}
/**
* 对当前 fiber 取 root (通过 fiber 的 parent 属性)
* @param {IFiber} fiber fiber 对象
*/
function getRoot(fiber: IFiber): IFiber {
let node = fiber;
while (node.parent) {
node = node.parent;
}
return node;
}
/**
* 迭代创建 work-in-progress fiber
* @param wipFiber work-in-progress fiber
*/
function performUnitOfWork(wipFiber: IFiber) {
// 为 wipFiber 创建 children fibers
beginWork(wipFiber);
// 如果有 children fibers,返回第一个 child 作为 nextUnitOfWork
if (wipFiber.child) {
return wipFiber.child;
}
// 没有 child,我们调用 completeWork 直到我们找到一个 sibling 作为 nextUnitOfWork。
// 如果没有 sibling 的话,向上找 parent。
let uow: IFiber | null | undefined = wipFiber;
while (uow) {
completeWork(uow);
// 如果有 sibling,设置 sibling 作为 nextUnitOfWork,重新开始。
if (uow.sibling) {
// Sibling needs to beginWork
return uow.sibling;
}
// 否则,向上找到 parent (children已处理完毕)开始 completeWork。
uow = uow.parent;
}
}
/**
* 为 fiber 创建 children fibers
*
* 1. 创建 stateNode 如果 wipFiber 没有的话;
* 2. 对 wipFiber 的 children 执行 reconcileChildrenArray。
* @param {IFiber} wipFiber 当前 work-in-progress fiber
*/
function beginWork(wipFiber: IFiber) {
if (wipFiber.tag === ITag.CLASS_COMPONENT) {
updateClassComponent(wipFiber);
} else {
updateHostComponent(wipFiber);
}
}
/**
* 处理 host component 和 root component(即都 host/原生 组件)。
* @param wipFiber 当前 work-in-progress fiber
*/
function updateHostComponent(wipFiber: IFiber) {
// 如果没有 stateNode (比如 React.render),创建 stateNode。
// ⚠️不会为 child 创建 DOM,也不会把 DOM 添加到 document。
if (!wipFiber.stateNode) {
wipFiber.stateNode = createDomElement(wipFiber) as Element;
}
// 从 wipFiber 的 props.children 获取 children 来创建 children fibers。
const newChildElements = wipFiber.props.children;
reconcileChildrenArray(wipFiber, newChildElements);
}
/**
* 处理 class component(即用户自定义的组件)。
* @param wipFiber 当前 work-in-progress fiber
*/
function updateClassComponent(wipFiber: IFiber) {
let instance = wipFiber.stateNode as Component;
// 如果 instance 不存在,调用 constructor 来创建实例。
if (instance == null) {
instance = wipFiber.stateNode = createInstance(wipFiber);
}
// 否则,如果 props 没变,且不存在更新了 state,则不需要做更新。
// 复制上次的 children 即可。
else if (wipFiber.props === instance.props && !wipFiber.partialState) {
cloneChildFibers(wipFiber);
return;
}
// 更新 props,state,用于调用 render,获取虚拟 vnode tree。
instance.props = wipFiber.props;
instance.state = Object.assign({}, instance.state, wipFiber.partialState);
wipFiber.partialState = null;
// 同样,我们得到了 child elements 来创建 children fibers;
// ⚠️由于 reconcileChildrenArray 支持数组,所以现在 render 可以返回数组了!
const newChildElements = instance.render();
reconcileChildrenArray(wipFiber, newChildElements);
}
function arrify(val: any) {
return val == null ? [] : Array.isArray(val) ? val : [val];
}
/**
* 核心函数,逐步创建 work-in-progress tree,决定提交阶段 (commit phase)需要
* 做的 DOM 操作(怎么更新 DOM)。
* 这里主要是比较 alternate 的 children filbers 和 newChildElements (virtual nodes)。
* @param wipFiber work-in-progress fiber
* @param newChildElements 要处理的 virtual dom tree(s),用于创建 children fibers。
*/
function reconcileChildrenArray(wipFiber: IFiber, newChildElements: any) {
// newChildElements 无法保证是数组,可能是单个 element,也可能是 null。
const elements = arrify(newChildElements) as IVNode[];
let index = 0;
let oldFiber = wipFiber.alternate ? wipFiber.alternate.child : null;
let newFiber: IFiber | null = null;
while (index < elements.length || oldFiber != null) {
// 记录 prevFiber (开始时是 null),用于后面更新 sibling 属性
const prevFiber = newFiber;
const element = index < elements.length && elements[index];
const sameType = oldFiber && element && element.type === oldFiber.type;
// 如果是相同类型(肯定已满足:element 和 oldFiber 都存在)
// 说明只需要执行更新就好
if (sameType) {
newFiber = {
// 和 oldFiber 共享相同的 type/tag/stateNode
type: (oldFiber as IFiber).type,
tag: (oldFiber as IFiber).tag,
stateNode: (oldFiber as IFiber).stateNode,
// 设置 parent 和 alternate
parent: wipFiber,
alternate: oldFiber,
// 设置 props 和 partialState
props: (element as IVNode).props,
partialState: (oldFiber as IFiber).partialState,
// 设置为 UPDATE
effectTag: Effect.UPDATE,
};
}
// 如果类型不同(可能是添加/删除/替换)
else {
// 如果 element 存在,则需要添加/替换为 element 代表的新 DOM
if (element) {
newFiber = {
// 设置 type 和 tag,stateNode 为空,稍后处理
type: element.type,
tag: typeof element.type === 'string'
? ITag.HOST_COMPONENT
: ITag.CLASS_COMPONENT,
props: element.props,
parent: wipFiber,
// 设置为 PLACEMENT
effectTag: Effect.PLACEMENT,
};
}
// 如果有 oldFiber,则要删除 oldFiber 对应的 DOM,这里通过 parent fiber 记录删除操作
// ⚠️ 本质是因为 oldFiber 不在 wip fiber tree 内了,在 completeWork 时无法被
// 遍历到,只能先放到 parent fiber 的 effects 中。
if (oldFiber) {
oldFiber.effectTag = Effect.DELETION;
wipFiber.effects = wipFiber.effects || [];
wipFiber.effects.push(oldFiber);
}
}
// 更新 oldFiber 为 oldFiber 的 sibling,继续处理过程
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
// 如果 index 为 0,说明是处理的第一个 child fiber,则
// 需要设置父 fiber 的 child 属性
if (index === 0) {
wipFiber.child = newFiber;
}
// 否则如果 elelment 存在,更新 prevFiber 的 sibling 属性
// 通过这两步操作,建立 wip fiber tree。
else if (prevFiber && element) {
prevFiber.sibling = newFiber;
}
// 继续处理下一个 element
index++;
// 可以看到,reconciliation 过程中没有使用 key,所以不知道来 child 是否有被移动位置过。
}
}
/**
* 直接复制对应 old fiber 的 children fibers 到 work-in-progress fiber
* 由于我们确信没有更新,所以只需要复制就好。
* @param parentFiber work-in-progress fiber
*/
function cloneChildFibers(parentFiber: IFiber) {
const oldFiber = parentFiber.alternate as IFiber;
if (!oldFiber.child) {
return;
}
let oldChild: IFiber | null | undefined = oldFiber.child;
let prevChild: IFiber | null = null;
// 通过 sibling 属性递归复制所有children fibers
while (oldChild) {
// 确保不共享 fiber,直接复制 old fiber的每个属性。
const newChild = {
type: oldChild.type,
tag: oldChild.tag,
stateNode: oldChild.stateNode,
props: oldChild.props,
partialState: oldChild.partialState,
alternate: oldChild,
parent: parentFiber,
};
if (prevChild) {
prevChild.sibling = newChild;
} else {
parentFiber.child = newChild;
}
prevChild = newChild;
oldChild = oldChild.sibling;
}
}
/**
* 设置 CLASS_COMPONENT fiber 的 __fiber,为 parent fiber 建立 effects。
* @param fiber 叶子fiber(没有children)或者子fiber已执行过 completework 的fiber
*/
function completeWork(fiber: IFiber) {
// 此时 fiber 是叶子fiber(没有children)或者子fiber已执行过 completework 的fiber。
// 如果 fiber 对应的组件是 CLASS_COMPONENT,设置 __fiber,用于之后
// resetNextUnitOfWork 时找到 root fiber。
if (fiber.tag === ITag.CLASS_COMPONENT) {
(fiber.stateNode as Component).__fiber = fiber;
}
// 如果 fiber 有 parent,则把 fiber 自身的 effects (以及子 fiber 的 effects)
// 合并到 parent 的 effects。
// 这其实是在 root fiber 的 effects 中收集了所有 fiber (该 fiber 有 effectTag)。
if (fiber.parent) {
const childEffects = fiber.effects || [];
const thisEffect = fiber.effectTag != null ? [fiber] : [];
const parentEffects = fiber.parent.effects || [];
fiber.parent.effects = parentEffects.concat(childEffects, thisEffect);
}
// 没有 parent,说明已经处理到 root fiber,处理结束,开始 commit 阶段。
// 把 pendingCommit 设为 root fiber。
else {
pendingCommit = fiber;
}
}
/**
* 遍历root fiber的 effects (通过 completeWork 已收集所有变更),执行更新。
* @param fiber root fiber
*/
function commitAllWork(fiber: IFiber) {
(fiber.effects as IFiber[]).forEach((f) => {
commitWork(f);
});
// 在container DOM 上设置 _rootContainerFiber,
// 用于之后resetNextUnitOfWork 时找到 root fiber。
(fiber.stateNode as any)._rootContainerFiber = fiber;
// 重置 nextUnitOfWork 和 pendingCommit,等待下一次更新触发(setState/render)。
nextUnitOfWork = null;
pendingCommit = null;
}
/**
* 检查 fiber 的 effectTag 并做对应的更新。
* @param fiber 需要处理的 fiber
*/
function commitWork(fiber: IFiber) {
// HOST_ROOT 无需处理
if (fiber.tag === ITag.HOST_ROOT) {
return;
}
let domParentFiber: IFiber = fiber.parent as IFiber;
// 对于 CLASS_COMPONENT 套 CLASS_COMPONENT 的情况,向上找到非 CLASS_COMPONENT
// 的 fiber,从而取到对应的真正的 DOM
while (domParentFiber.tag === ITag.CLASS_COMPONENT) {
domParentFiber = domParentFiber.parent as IFiber;
}
const domParent = domParentFiber.stateNode as Element;
// 如果是 PLACEMENT 且 fiber 对应 HOST_COMPONENT,添加 stateNode 到 domParent
if (fiber.effectTag === Effect.PLACEMENT && fiber.tag === ITag.HOST_COMPONENT) {
domParent.appendChild(fiber.stateNode as Element);
}
// 如果是 UPDATE,更新属性即可。
else if (fiber.effectTag === Effect.UPDATE) {
updateDomProperties(fiber.stateNode as HTMLElement, (fiber.alternate as IFiber).props, fiber.props);
}
// 如果是 DELETION,删除即可。
else if (fiber.effectTag === Effect.DELETION) {
commitDeletion(fiber, domParent);
}
}
/**
* 删除 fiber 对应的 DOM。
* @param fiber 要执行删除的目标 fiber
* @param domParent fiber 所包含的 DOM 的 parent DOM
*/
function commitDeletion(fiber: IFiber, domParent: Element) {
let node = fiber;
while (true) {
// 如果 node 是 CLASS_COMPONENT,则取其 child
if (node.tag === ITag.CLASS_COMPONENT) {
node = node.child as IFiber;
continue;
}
// BEGIN: 删除 node 对应的 DOM元素(stateNode)
domParent.removeChild(node.stateNode as Element);
/// 在 BEGIN 和 END 之间:
// node 不是 fiber 且 node 没有 sibling,则向上取 parent。
// 为什么有这种操作?可以看到前面在 node 是 CLASS_COMPONENT 时,我们向下取 child 了。
// 当我们删除了 child 之后,我们需要向上返回,并删除 node 的 sibling。
// 这种向上返回的过程结束于 2 种情况:
// 1. node 有 sibling,则我们要 break 下来删除这个 sibling(后面从这个 sibling 向上返回);
// 2. node 已经是 fiber,此时整个删除过程已经结束。
while (node !== fiber && !node.sibling) {
node = node.parent as IFiber;
}
// 如果 node 是 fiber,结束删除过程。
// ⚠️(删除 fiber 的 sibling显然是错误的,我们要删除的是 fiber 对应的 DOM)
if (node === fiber) {
return;
}
// END: 取 node 的 sibling,并继续删除。
node = node.sibling as IFiber;
}
}

原作者的博客还是很易读易懂的,这里不再赘述。下面主要列出一些帮助理解的重点:

  1. 在具体实现中,一个 fiber 可以理解为一个纯 JavaScript 对象,对应一个component:

export interface IFiber {
tag: ITag;
type?: ComponentType;
// parent/child/sibling 用于构建 fiber tree,对应相应的组件树。
parent?: IFiber | null;
child?: IFiber | null;
sibling?: IFiber | null;
// 大多数时候,我们有2棵fiber树:
// 1. 一棵对应已经渲染到DOM的,我们称之为 current tree / old tree;
// 2. 一棵是我们正在创建的,对应新的更新(setState() 或者 React.render()),叫 work-in-progress tree。
// ⚠️ work-in-progress tree 不和 old tree 共享任何 fiber;一旦 work-in-progress tree 创建
// 完成并完成需要的 DOM 更新,work-in-progress tree 即变成 old tree 。
// alternate 用于 work-in-progress fiber 链接/指向(link)到它们对应的 old tree 上的 fiber。
// fiber 和它的 alternate 共享 tag, type 和 stateNode。
alternate?: IFiber | null;
// 指向组件实例的引用,可以是 DOM element 或者 Class Component 的实例
stateNode?: Element | Component;
props: IProps;
partialState?: IState | null;
effectTag?: Effect;
effects?: IFiber[];
}

  1. React 中 reconciliation 和 render 是两个独立的过程,其中 reconciliation 过程是纯粹的 virtual dom diff,不涉及任何 DOM 操作——这是我们为什么能够把 reconciliation 分割为多个工作单元 (unit of work) 的原因。而 didact 中是怎么分割/设置工作单元呢?

    didact 中,reconciliation 可以理解为是创建 work-in-progress fiber tree 的过程。从 root fiber 开始,每处理一个 fiber 都是一个工作单元。每个 fiber 的处理过程基本是:

    • 如果没有 stateNode,则创建(离线的DOM node或者是创建 class component的实例);
    • 通过 props.children 或者 instance.render() 的返回值去创建 fiber 的 children fibers(effectTag 和 effects 存储了后面commit phase需要的 DOM 操作)。
  2. 通过 requestIdleCallback API 来 schedule 工作;同时以 nextUnitOfWork 为下一步需要执行的工作对象。

四、从 React 源码来看 Fiber

@OwlAford

This comment has been minimized.

OwlAford commented Mar 27, 2018

非常好,期待继续

@CollinPeng

This comment has been minimized.

CollinPeng commented May 3, 2018

这个挺好的

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment