学习与理解 React Fiber

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:
import { Component } from './component';

// Fiber 标签
export enum ITag {
    HOST_COMPONENT = 'host',
    CLASS_COMPONENT = 'class',
    HOST_ROOT = 'root',
}

// Effect 标签
export enum Effect {
    PLACEMENT = 1,
    DELETION = 2,
    UPDATE = 3,
}

export interface IdleDeadline {
    didTimeout: boolean;
    timeRemaining(): number;
}

export type IdleRequestCallback = (deadline: IdleDeadline) => any;

export type ComponentType = string | (() => object);

export interface IState {
    [key: string]: any;
}

export interface IVNode {
    type: ComponentType;
    props: {
        children?: IVNode[],
        [key: string]: any,
    };
}

export interface IProps {
    children?: IVNode[];
    style?: object;
    [key: string]: any;
}

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[];
}

export interface IUpdate {
    from: ITag;
    dom?: HTMLElement;
    instance?: Component;
    newProps?: IProps;
    partialState?: IState | null;
}
  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 为下一步需要执行的工作对象。

Postgresql 分组查询

数据

最近有一个项目,有一个表数据结构如下

name description technique_id references_count
name_1 description 1 5
name_2 description 2 4
name_3 description 3 2
name_1 description 1 5
name_2 description 2 4
name_3 description 3 2
name_1 description 1 5
name_2 description 2 4
name_3 description 3 2

需求

现在需要通过一条sql语句, 通过几个 technique_id 查找, 每个technique_id 查找3条, 并且按照references_count降序

实现

SELECT * from (
SELECT *, RANK() OVER (PARTITION BY technique_id ORDER BY reference_count DESC) AS RP FROM gauges
)
G WHERE G.rp <= 3

参考

http://thehobt.blogspot.jp/2009/02/rownumber-rank-and-denserank.html

React 16 Fiber源码速览 (转)

本文的写作有一部分没有完成,打算针对React 16.3再对本文进行修改,请大家留意

React 16在近期发布了。除了将备受争议的BSD+Patents协议改为MIT协议之外,React 16还带来了许多新特性,比如:

  • 允许在render函数中返回节点数组和字符串。
render() {
  // 再也不用在外面套一个父节点了
  return [
    // 别忘了加上key
    <li key="A">First item</li>,
    <li key="B">Second item</li>,
    <li key="C">Third item</li>,
  ];
}
  • 提供更好的错误处理。
  • 支持自定义DOM属性。

但最关键的一点还是:

64c45edcgy1fjzoreufb4j20xm0kcjwa

没错,React 16是一次重写,在保持API不变的情况下,将核心架构改为了代号为Fiber的异步渲染架构。新架构带来了的变化有:

  • 体积减小

64c45edcgy1fjzoretq6dj210q0bu776.jpg

一次预谋已久的重写

Fiber这个架构并不是突然冒出来的。Facebook的工程师在设计React之初就设想未来的UI渲染会是异步的。从setState()的设计和React内部的事务机制可以看出这点。

在去年,React的开发者Andrew Clark在社区中放出了Fiber架构的一个文档。描述了Fiber架构的基本信息。同时表示Facebook的工程师正在实现这个新架构。今年3月的React Conf 2017上,Lin Clark做了A Cartoon Intro to Fiber这个分享,介绍了Fiber架构的工作原理。今年9月,Fiber架构随着React 16正式发布。Fiber架构的代码放在原来的React仓库之中,并且可以通过运行时的判断来切换新老架构,方便测试和部署。因此Fiber的开发是一个渐进的过程。这个网站实时展示了Fiber通过的测试用例,随着所有用例的通过,Fiber也正式发布了。有趣的是,在React 16发布之前,Fiber架构的React就已经运行在Facebook的产品中了。FB的工程师表示看到新架构在线上产品运行起来,是很激动人心的。具体的情况可以看这篇博客:React 16: A look inside an API-compatible rewrite of our frontend UI library

Fiber概念简介

本文的题目是React 16 Fiber源码速览,所以关注的主要是Fiber相关的代码。在分析源码之前,首先介绍一些基本概念。

推荐看上文中提到的A Cartoon Intro to Fiber。这个分享比较系统和形象解释了Fiber架构的工程流程,并且使用了React源码中的术语。有助于理解Fiber的概念和源码。下文中的配图也来自这个分享。

reconciler VS renderer

64c45edcgy1fk0jck2eo4j20gk09a76l.jpg

Reconciler就是我们所说的Virtul DOM,用于计算新老View的差异。React 16之前的reconciler叫Stack reconciler。Fiber是React的新reconciler。Renderer则是和平台相关的代码,负责将View的变化渲染到不同的平台上,DOM、Canvas、Native、VR、WebGL等等平台都有自己的renderer。我们可以看出reconciler是React的核心代码,是各个平台共用的。因此这次React的reconciler更新到Fiber架构是一次重量级的核心架构的更换。

由reconciler和renderer两个概念引出的是phase的概念。Phase指的是React组件渲染时的阶段。第一阶段是reconciliation,这一阶段做的是Fiber的update,然后产出的是effect list(可以想象成将老的View更新到新的状态所需要做的DOM操作的列表)。这一个阶段是没有副作用的,因此这个过程可以被打断,然后恢复执行。第二阶段是commit阶段。Reconciliation产生的effect list只有在commit之后才会生效,也就是真正应用到DOM中。这一阶段往往不会执行太长时间,因此是同步的,这样也避免了组件内视图层结构和DOM不一致。

Fiber是什么

React源码中的注释说:

A Fiber is work on a Component that needs to be done or was done. There can be more than one per component.

简单的说,一个Fiber就是一个POJO对象,代表了组件上需要做的工作。一个React Element可以对应一个或多个Fiber节点。

在render函数中创建的React Element树在第一次渲染的时候会创建一颗结构一模一样的Fiber节点树。不同的React Element类型对应不同的Fiber节点类型。一个React Element的工作就由它对应的Fiber节点来负责。我们如果在console中打印React 16的组件实例,会发现有一个_reactInternalFiber属性指向它对应的Fiber实例。

虽然React的代码中其实没有明确的Virtul DOM概念,但Fiber和我们概念中的Virtul DOM树是等价的。

Fiber带来了一个给React的渲染带来了重要的变化。React内部有事务的概念。之前React渲染相关的事务是连续的,一旦开始就会run to completion。现在React的事务则是由一系列Fiber的更新组成的,因此React可以在多个帧中断断续续的更新Fiber,最后commit变化。

那为什么说一个React Element可以对应不止一个Fiber呢?因为Fiber在update的时候,会从原来的Fiber(我们称为current)clone出一个新的Fiber(我们称为alternate)。两个Fiber diff出的变化(side effect)记录在alternate上。所以一个组件在更新时最多会有两个Fiber与其对应,在更新结束后alternate会取代之前的current的成为新的current节点。

Fiber节点的数据结构

下面介绍Fiber类型的重要属性:

{
    tag: TypeOfWork, // fiber的类型,下一节会介绍
    alternate: Fiber|null, // 在fiber更新时克隆出的镜像fiber,对fiber的修改会标记在这个fiber上

    return: Fiber|null, // 指向fiber树中的父节点

    child: Fiber|null, // 指向第一个子节点
    sibling: Fiber|null, // 指向兄弟节点

    effectTag: TypeOfSideEffect, // side effect类型,下文会介绍
    nextEffect: Fiber | null, // 单链表结构,方便遍历fiber树上有副作用的节点
    pendingWorkPriority: PriorityLevel, // 标记子树上待更新任务的优先级

}

在实际的渲染过程中,Fiber节点构成了一颗树。这棵树在数据结构上是通过单链表的形式构成的,Fiber节点上的chlidsibling属性分别指向了这个节点的第一个子节点和相邻的兄弟节点。这样就可以遍历整个Fiber树了。

Fiber树的图示如下:

64c45edcgy1fkc8x8n8x2j20fa0co428.jpg

TypeOfWork

这是源码中的typeOfWork,代表React中不同类型的fiber节点。

{
  IndeterminateComponent: 0, // Before we know whether it is functional or class
  FunctionalComponent: 1,
  ClassComponent: 2,
  HostRoot: 3, // Root of a host tree. Could be nested inside another node.
  HostPortal: 4, // A subtree. Could be an entry point to a different renderer.
  HostComponent: 5,
  HostText: 6,
  CoroutineComponent: 7,
  CoroutineHandlerPhase: 8,
  YieldComponent: 9,
  Fragment: 10,
}s

对几个常用的类型作一下解释:

ClassComponent

就是应用层面的React组件。ClassComponent是一个继承自React.Component的类的实例。

HostRoot

ReactDOM.render()时的根节点。

HostComponent

React中最常见的抽象节点,是ClassComponent的组成部分。具体的实现取决于React运行的平台。在浏览器环境下就代表DOM节点,可以理解为所谓的虚拟DOM节点。HostComponent中的Host就代码这种组件的具体操作逻辑是由Host环境注入的。

TypeOfSideEffect

说一下这是以二进制位表示的。可以多个叠加。

{
  NoEffect: 0,          
  PerformedWork: 1,   
  Placement: 2, // 插入         
  Update: 4, // 更新           
  PlacementAndUpdate: 6, 
  Deletion: 8, // 删除   
  ContentReset: 16,  
  Callback: 32,      
  Err: 64,         
  Ref: 128,          
};

Priority

Priority指的是Fiber中一个work的优先级。这是React源码中的对Priority类型的定义:

{
  NoWork: 0, // No work is pending.
  SynchronousPriority: 1, // For controlled text inputs. Synchronous side-effects.
  TaskPriority: 2, // Completes at the end of the current tick.
  HighPriority: 3, // Interaction that needs to complete pretty soon to feel responsive.
  LowPriority: 4, // Data fetching, or result from updating stores.
  OffscreenPriority: 5, // Won't be visible but do the work in case it becomes visible.
}

我们可以把Priority分为同步和异步两个类别,同步优先级的任务会在当前帧完成,包括SynchronousPriority和TaskPriority。异步优先级的任务则可能在接下来的几个帧中被完成,包括HighPriority、LowPriority以及OffscreenPriority。

React 16 Fiber源码目录结构

React库的入口、组件的基类ReactComponentReactElement.createElement函数等等所有平台公用的代码位于src/isomorphic下。

我们关注的Fiber代码位于src/renderers/shared/fiber下。我们先来看看src/renderers下面有什么:

64c45edcgy1fk1kj1kekxj20s10ezq6q.jpg

可以看到src/renderers下的代码就是上文介绍的renderer,分dom、native、art等等平台。那我们再看看src/renderers/shared目录下有什么:

64c45edcgy1fk1kj1kekxj20s10ezq6q (1).jpg

src/renderers/shared其实就是reconciler相关的代码了。可以看到里面有fiber和stack新老两大reconciler(在笔者发文时,Stack reconciler已经完成了它的使命,相关的代码已经被移除了)。

最后让我们来看看src/renderers/shared/fiber下的代码:

64c45edcgy1fk1m7cifluj20sh0pk10v.jpg

这些就是React fiber的核心代码了。Fiber节点的定义在ReactFiber.js中,Fiber的reconciler构造函数在ReactFiberReconciler.js中,Fiber节点的工作流程由ReactFiberBeginWork.jsReactFiberCommitWork.jsReactFiberCompleteWork.js组成。Fiber的子节点reconcile逻辑在ReactChildFiber.js中,ReactFiberScheduler.js则是调度相关的逻辑。接下来就让我们通过具体的场景,来分析React 16的源码吧!

阅读React源码须知

下面简单介绍一下在React源码中,起辅助作用的代码。以免大家在看源码时被这些代码所迷惑。

flow type

React使用了flow作为静态类型检查工具。所以React源码中都是带有类型声明的。这对熟悉Java或者C++这些静态类型语言的同学应该不陌生。类型声明对于快速理解源码也是有很大帮助的

if (__DEV__)

React源码中常常有if (__DEV__)这样的代码,比如:

if (__DEV__) {
    warning(
      shouldUpdate !== undefined,
      '%s.shouldComponentUpdate(): Returned undefined instead of a ' +
        'boolean value. Make sure to return true or false.',
      getComponentName(workInProgress) || 'Unknown',
    );
  }

这些代码是为了更好的开发者体验而编写的。React中的友好的报错,render性能测试等等代码都是写在if (__DEV__)中的。在production build的时候,这些代码不会被打包。因此我们可以毫无顾虑的提供专为开发者服务的代码。React的最佳实践之一就是在开发时使用development build,在生产环境使用production build。

大家在刚开始接触源码时可以跳过if (__DEV__)中的代码,专注于理解核心的部分。

源码阅读小技巧

如果读者想在阅读文本之后打算自己深入探索React源码,我可以给出一些阅读源码的小技巧。如果对于React中某个方法的调用过程感兴趣,可以在本地用create-react-app新建一下小demo项目,然后直接在node_modules中的react-dom.development.js和react.development.js两个文件里的对应方法打断点。这样在中断的时候就可以看到整个调用栈了,Chrome种可以通过点击调用栈切换到其中任何一帧的状态。如果发现调用过程中有自己感兴趣的函数,可以clone React的整个仓库,用编辑器对想要查看的函数进行全局搜索,找到那个函数的源码进行阅读。此外还有一个小tip,如果对某个特性的实现感兴趣,可以去搜索React的pull request和issue列表,说不定可以找到当初实现这个特性时候提的PR,PR中一般会写实现时的一些考虑。另外React源码的注释也是非常详尽的,有些已经等于简单的文档了,所以仔细的阅读注释也是理解源码的捷径之一。

本文源码的时效性

React 16.0发布后,新架构的很多特性还没有完全开放,因此React这段时间还在一个积极的开发过程中,源码变动会比较大。本文是分析的源码是React v16.0的源码。大家在阅读时Github上的React源码时要注意,目前的master分支的React源码和本文中的源码会有一些差异。比如在本文发布时,React的目录结构就进行了调整,源码从src中转移到了packages目录下,按react、react-reconciler、react-dom等等NPM模块的方式划分。还有一些fiber的实现也在进行一些小的重构。比如在performWork相关的代码中加入performWorkOnRoot和renderRoot这几个函数,通过准确的命名让函数的作用更清晰。又比如Priority的概念直接被expirationTime取代了,workLoop中直接根据expirationTime来判断任务的执行时机。所以推荐大家阅读master分支下的最新代码,因为React 16在代码质量上的确还处于一个未完成的状态,随着进一步的开发,源码的可读性会更高。

确定源码分析的入口

React 16组件源码分析:用户触发的setState开启的一次渲染

我们知道,React的渲染是由setState触发的,所以就让我们从setState入手,来分析React 16的组件渲染流程。

setState

setState方法是React基类上的一个方法。因此位于src/isomorphic下的modern/class/ReactBaseClasses.js

64c45edcgy1fk1m7bwt6vj20fd05pdgr.jpg

我们看到setState调用了this.updater.enqueueSetState。updater是renderer在渲染的时候注入的对象,这个对象由reconciler提供。具体的逻辑可以看ReactDOM.render相关的代码,这里就不展开了。

enqueueSetState

既然updater是reconciler提供的,那我们就可以在fiber的代码中找到它。updater就位于src/renderers/shared/fiberReactFiberCompleteWork.js中。

64c45edcgy1fk1m7by675j20ch06tjsg.jpg

这里只截取了一部分的updater代码,可以看到updater提供了enqueueSetState方法,这个方法首先从全局拿到React组件实例对应的fiber,然后拿到了fiber的优先级。最后调用了addUpdate向队列中推入需要更新的fiber,并调用scheduleUpdate触发调度器调度一次新的更新。

熟悉React源码的朋友应该知道,setState的流程到这里为止,和React 15的流程基本是一样的。从下面开始,我们就可以看到Fiber架构的不同之处了。

addUpdate

我们首先来看addUpdate函数,这个函数向Fiber的更新队列里加入一次更新:

function addUpdate(
  fiber: Fiber,
  partialState: PartialState<any, any> | null,
  callback: mixed,
  priorityLevel: PriorityLevel,
): void {
  const update = {
    priorityLevel,
    partialState,
    callback,
    isReplace: false,
    isForced: false,
    isTopLevelUnmount: false,
    next: null,
  };
  insertUpdate(fiber, update);
}

addUpdate函数组装了一个update,然后将fiber和update传入了insertUpdate函数中。我们先来看一下这里用到的两个类型,Update和UpdateQueue:

type UpdateQueue = {
  first: Update | null,
  last: Update | null,
  hasForceUpdate: boolean,
  callbackList: null | Array<Callback>,

  // Dev only
  isProcessing?: boolean,
};
type Update = {
  priorityLevel: PriorityLevel,
  partialState: PartialState<any, any>,
  callback: Callback | null,
  isReplace: boolean,
  isForced: boolean,
  isTopLevelUnmount: boolean,
  next: Update | null,
};

我们可以看到,UpdateQueue是一个单向链表,有first和last指针指向链表的头部和尾部。其中的每一个Update都有一个next属性指向下一个Update。这样的数据结构在React 16中是很常见的。

之前说到,在更新时,一个React element会有一个current fiber和一个alternate fiber。我们又把alternate fiber叫working in progress fiber。这两个fiber都有一个Update Queue。这两个Queue里面的item的引用是相同的,也就是所谓的persistent structure。区别在于,working in progress fiber会在更新完一个队列项之后将其从队列中移除。所以working in progress update queue永远是current queue的一个子集。在更新完成之后,working in progress fiber取代current fiber成为新的current fiber。如果更新中断(有更高优先级的更新插入),current fiber的update queue就可以作为备份,使得之前中断的更新可以重新开始。

再看insertUpdate,这个函数处理了将一个update插入到current queue和work-in-progress queue两个队列中的逻辑:

64c45edcgy1fkbtqzlproj21eq210nik.jpg

scheduleUpdate

看完了addUpdate相关的逻辑,我们再来看scheduleUpdate

64c45edcgy1fkbz4oej2tj21do3c91kx.jpg

performWork

performWork的作用就是“刷新”待更新队列,执行待更新的事务:

64c45edcgy1fkc077e5vgj20g2244gun.jpg

performWork的代码很长,其中很大一部分是错误处理代码,这些代码和React16中的新特性有关,官方博客的介绍如下:

Previously, runtime errors during rendering could put React in a broken state, producing cryptic error messages and requiring a page refresh to recover. To address this problem, React 16 uses a more resilient error-handling strategy. By default, if an error is thrown inside a component’s render or lifecycle methods, the whole component tree is unmounted from the root. This prevents the display of corrupted data. However, it’s probably not the ideal user experience.
Instead of unmounting the whole app every time there’s an error, you can use error boundaries. Error boundaries are special components that capture errors inside their subtree and display a fallback UI in its place. Think of error boundaries like try-catch statements, but for React components.

我们需要关注的函数,一个是workLoop,这个函数是React更新pendingWork队列的主循环。一个是scheduleDeferredCallback,这个函数会在未来安排一次更新,来处理workLoop中没有做完的事务。

workLoop

图片注释还需要打磨

我们来看workLoop的代码:

64c45edcgy1fkc0p3sx5fj21e93z97wh.jpg

除了图中所注释的,workLoop中有一个值得注意的细节。我们看到,loop中首先判断nextUnitOfWork的优先级是不是高于或等于TaskPriority。如果不是,则进入另一个分支,这个分支和前一个在对nextUnitOfWork的处理上有着微妙的区别。之前在介绍Priority的时候我们说到过,TaskPriority以及更高的优先级属于同步优先级,这些更新会在nextTick之前完成。所以loop中的两个分支其实就是对同步和异步的任务做了不同的处理。两个分支的区别主要是第二个分支使用了deadline.timeRemaining()来判断是否还有时间继续处理任务。

在之前的分析中,我们没有关注deadline这个参数,workLoop中的这个参数是从performWork中传入的,而performWork中的deadline参数是由scheduleUpdateImpl传入的。scheduleUpdateImpl给同步优先级的任务的deadline参数传入的是null。这是符合常理的,因为同步优先级的任务会一定会在一次workLoop中执行完毕。scheduleUpdateImpl中的异步优先级的任务在scheduleDeferredCallback中处理,我们看这个函数的类型:

scheduleDeferredCallback(
    callback: (deadline: Deadline) => void,
  ): number | void,

deadline出现了!所以异步任务的deadline是在被scheduleDeferredCallback调用时传入的。

scheduleDeferredCallback

让我们来看看scheduleDeferredCallback这个函数。全局搜索一番,我们发现这个函数是在renderer初始化时被注入的。

React 16抽象出了一个叫ReactFiberReconciler的工厂函数。这个函数接收一个HostConfig类型的参数,返回一个Reconciler。每个renderer初始化时需要传入当前平台相关的配置,也就是一个HostConfig实例,才能拿到一个自定义的Reconciler。

这里说一点题外话,React抽象出这个工厂函数意味着React标准化了自定义Renderer的接口。Renderer通过ReactFiberReconciler这个API就可以将自定义Renderer接入FiberReconciler。Making-a-custom-React-renderer就利用了这个函数来打造自定义Renderer。

HostConfig的类型签名是这样的:

export type HostConfig<T, P, I, TI, PI, C, CX, PL> = {
  getRootHostContext(rootContainerInstance: C): CX,
  getChildHostContext(parentHostContext: CX, type: T, instance: C): CX,
  getPublicInstance(instance: I | TI): PI,

  createInstance(
    type: T,
    props: P,
    rootContainerInstance: C,
    hostContext: CX,
    internalInstanceHandle: OpaqueHandle,
  ): I,
  appendInitialChild(parentInstance: I, child: I | TI): void,
  finalizeInitialChildren(
    parentInstance: I,
    type: T,
    props: P,
    rootContainerInstance: C,
  ): boolean,

  prepareUpdate(
    instance: I,
    type: T,
    oldProps: P,
    newProps: P,
    rootContainerInstance: C,
    hostContext: CX,
  ): null | PL,
  commitUpdate(
    instance: I,
    updatePayload: PL,
    type: T,
    oldProps: P,
    newProps: P,
    internalInstanceHandle: OpaqueHandle,
  ): void,
  commitMount(
    instance: I,
    type: T,
    newProps: P,
    internalInstanceHandle: OpaqueHandle,
  ): void,

  shouldSetTextContent(type: T, props: P): boolean,
  resetTextContent(instance: I): void,
  shouldDeprioritizeSubtree(type: T, props: P): boolean,

  createTextInstance(
    text: string,
    rootContainerInstance: C,
    hostContext: CX,
    internalInstanceHandle: OpaqueHandle,
  ): TI,
  commitTextUpdate(textInstance: TI, oldText: string, newText: string): void,

  appendChild(parentInstance: I, child: I | TI): void,
  appendChildToContainer(container: C, child: I | TI): void,
  insertBefore(parentInstance: I, child: I | TI, beforeChild: I | TI): void,
  insertInContainerBefore(
    container: C,
    child: I | TI,
    beforeChild: I | TI,
  ): void,
  removeChild(parentInstance: I, child: I | TI): void,
  removeChildFromContainer(container: C, child: I | TI): void,

  scheduleDeferredCallback(
    callback: (deadline: Deadline) => void,
  ): number | void,

  prepareForCommit(): void,
  resetAfterCommit(): void,

  // Optional hydration
  canHydrateInstance?: (instance: I | TI, type: T, props: P) => boolean,
  canHydrateTextInstance?: (instance: I | TI, text: string) => boolean,
  getNextHydratableSibling?: (instance: I | TI) => null | I | TI,
  getFirstHydratableChild?: (parentInstance: I | C) => null | I | TI,
  hydrateInstance?: (
    instance: I,
    type: T,
    props: P,
    rootContainerInstance: C,
    hostContext: CX,
    internalInstanceHandle: OpaqueHandle,
  ) => null | PL,
  hydrateTextInstance?: (
    textInstance: TI,
    text: string,
    internalInstanceHandle: OpaqueHandle,
  ) => boolean,
  didNotHydrateInstance?: (parentInstance: I | C, instance: I | TI) => void,
  didNotFindHydratableInstance?: (
    parentInstance: I | C,
    type: T,
    props: P,
  ) => void,
  didNotFindHydratableTextInstance?: (
    parentInstance: I | C,
    text: string,
  ) => void,

  useSyncScheduling?: boolean,
};

这里主要包括一些平台相关的代码,比如节点的操作(insertBeforeappendChild等等),还有一些配置项,比如useSyncScheduling。我们看到scheduleDeferredCallback就在其中。我们来看看renderer初始化的代码:

在React DOM的入口中:

scheduleDeferredCallback: ReactDOMFrameScheduling.rIC,

在React Native的入口中:

scheduleDeferredCallback: global.requestIdleCallback,

我们可以看到scheduleDeferredCallback的实现和平台相关。在Native环境下,它是React Native的js runtime提供的global.requestIdleCallback,在浏览器环境下,它是ReactDOMFrameScheduling.rIC

Cooperative Scheduling && requestIdleCallback

window.requestIdleCallback的函数签名和scheduleDeferredCallback是一模一样的。requestIdleCallback的callback接收一个IdleDeadline类型的参数。这个IdleDeadline和React中的deadline都有一个timeRemaining方法。

requestIdleCallback的W3C规范叫Cooperative Scheduling of Background Tasks。React官方在介绍fiber时也提到了Cooperative Scheduling这种技术。从源码来看,React主要利用了浏览器提供的requestIdleCallback API来实现这一特性。

相比于利用setTimout这样的API实现task scheduling,requestIdleCallback带来的Cooperative Scheduling让开发者让浏览器在空闲时间调用callback,并且在callback中可以获取到当前帧剩余的时间。利用这个信息我们可以合理的安排当前帧需要做的工作,如果工作太多而时间不够,就再调用requestIdleCallback来做剩余的工作。

requestIdleCallback的回调具体执行的时间点是在一帧开始,JavaScript执行完,浏览器执行渲染流程之后,到这帧结束之前。图示如下:

64c45edcgy1fkc7kiv42qj20kh03vq3a.jpg

deadline中的timeRemaining的最大值是50ms,以免浏览器长期空闲时,callback的任务一直执行,使得UI不能及时响应用户输入。

ReactDOMFrameScheduling.rIC

ReactDOMFrameScheduling.rIC的逻辑是,如果浏览器实现了requestIdleCallback,就返回原生API。如果没有实现,就返回一个polyfill。这个polyfill的实现非常有趣,可以学到很多有意思的黑科技。

我们来看看ReactDOMFrameScheduling.rIC的实现:

虽然Chrome和Firefox都已经实现了requestIdleCallback,但某些浏览器还是需要polyfill,所以我们重点关注一下requestIdleCallback的polyfill的实现。

预估一个比较低的frame rate。requestAnimationFrame获取一帧开始,时间戳,触发一个message事件,postMessage在layout paint和composite之后被调用。deadline通过frame rate – rafTime可以得到

| frame start time                                      deadline |
[requestAnimationFrame] [layout] [paint] [composite] [postMessage]

通过requestAnimationFrame直接的时间差获取过去两帧的准确frame rate,动态调整当前帧的frame rate。

默认优先级

既然一次更新是同步还是异步是由优先级决定的,那我们在用户代码中通过setState来schedule的一次update的优先级是多少呢?

我们回顾一下enqueueSetState

64c45edcgy1fk1m7by675j20ch06tjsg (1).jpg

addUpdatescheduleUpdatepriorityLevel是通过getPriorityContext(fiber, false)获取的。

我们来看看getPriorityContext的实现:

64c45edcgy1fkd4vrvbycj20ox0ian05.jpg

所以我们得出了一个重要的结论。在React 16中,异步渲染默认是关闭的。用户代码的优先级是同步的。

performUnitOfWork

讲完了deadline对象的由来,我们回到workLoop,看看React是的reconcilation是如何进行的。我们可以看到首先被调用的是performUnitOfWork,这个函数做的就是所谓的reconcilation阶段的工作了。然后React将调用commitAllWork进入commit阶段,将reconcilation结果真正应用到DOM中。

64c45edcgy1fkc84mx1vij20fa0gq7af.jpg

React 16保持了之前版本的事务风格,一个“work”会被分解为begin和complete两个阶段来完成。我们先关注beginWork

64c45edcgy1fk6jdy25scj20wq1kvqdw.jpg

beginWork函数根据fiber节点不同的tag,调用对应的update方法。可以说是一个入口函数。真正的逻辑要看update开头的这一些了函数。

updateClassComponent && updateHostComponent

上一节中讲到,beginWork中不同tag的元素有不同的update系列方法,我们重点关注的是对ClassComponent和HostComponent两种component的更新方法。ClassComponent对应的是React组件实例,HostComponent对应的是一个视图层节点,在浏览器环境中就等于DOM节点。

我们先关注updateClassComponent函数:

64c45edcgy1fkq4dx9ljkj20ox0jyn2j.jpg

updateHostComponent这里就不再详细分析了。因此HostComponent没有生命周期钩子需要处理,这个函数主要做的就是调用reconcileChildren对子节点进行diff。

reconcileChildren

reconcileChildren实现的就是江湖上广为流传的Virtul DOM diff。这年头人人都看过一两个Virtul DOM diff的实现,那React 16的diff是如何实现的

64c45edcgy1fkurhf1dgwj20p60sy44v.jpg

reconcileChildren这个函数里调用了三个功能相似的函数:mountChildFibersInPlacereconcileChildFibersreconcileChildFibersInPlace。在源码中我们发现,这三个函数其实是同一个函数,通过传入不同的参数“重载”而来的。

exports.reconcileChildFibers = ChildReconciler(true, true);

exports.reconcileChildFibersInPlace = ChildReconciler(false, true);

exports.mountChildFibersInPlace = ChildReconciler(false, false);

ChildReconciler是一个工厂函数,它接收shouldClone, shouldTrackSideEffects两个参数。reconcileChildFibers函数的目的是产出effect list,所以shouldClone, shouldTrackSideEffects两个参数都是true。mountChildFibersInPlace是组件初始化时用的,所以不用clone fiber来diff,也不用产出effect list。reconcileChildFibersInPlace是在之前reconcile被中断的fiber树上继续工作,因此shouldClone参数为false。

ChildReconciler内部有很多helper函数,最终返回的函数叫reconcileChildFibers,这个函数实现了对子fiber节点的reconciliation。下面我们关注reconcileChildFibers函数的实现。

reconcileChildFibers

图的注释:

  • 总的,这个函数根据newChild的类型调用不同的方法。newChild可能是一个元素,也可能是一个数组(React16新特性)
  • 如果是reconcile单个元素,以reconcileSingleElement为例比较key和type,如果相同,复用fiber,删除多余的元素(currentFirstChild的sibling),如果不同,调用createFiberFromElement,返回新创建的。
  • 如果是string,reconcileSingleTextNode
  • 如果是array,reconcileChildrenArray
  • 如果是空,deleteRemainingChildren删除老的子元素

React的reconcile算法采用的是层次遍历,这种算法是建立在一个节点的插入、删除、移动等操作都是在节点树的同一层级中进行这个假设下的。所以reconcile算法的核心就是如何diff两个子节点数组。

reconcileChildrenArray

React16的diff算法采用和来自社区的两端同时比较法同样结构的算法。

关于diff算法演化历史可以看司徒正美的这篇博客

因为fiber树是单链表结构,没有子节点数组这样的数据结构。也就没有可以供两端同时比较的尾部游标。所以React的这个算法是一个简化的两端比较法,只从头部开始比较。

下面我们来看一下代码:

图片

从头部遍历。第一次遍历新数组,对上了,新老index都++,比较新老数组哪些元素是一样的,(通过updateSlot,比较key),如果是同样的就update。第一次遍历玩了,如果新数组遍历完了,那就可以把老数组中剩余的fiber删除了。

如果老数组完了新数组还没完,那就把新数组剩下的都插入。

如果这些情况都不是,就把所有老数组元素按key放map里,然后遍历新数组,插入老数组的元素,这是移动的情况。

最后再删除没有被上述情况涉及的元素(也就是老数组中有新数组中无的元素,上面的删除只是fast path,特殊情况)

completeUnitOfWork

注:这里effect list链表插入的想法只是猜测,需要进一步确认。

completeUnitOfWork是complete阶段的入口。complete阶段的作用就是在一个节点diff完成之后,对它进行一些收尾工作,主要是更新props和调用生命周期方法等等。completeUnitOfWork主要的逻辑是调用completeWork完成收尾,然后将当前子树的effect list插入到HostRoot的effect list中。具体的让我们来看代码:

64c45edcgy1fks860npkqj20p41foalg.jpg

completeWork

complete阶段主要工作都是在completeWork中完成的。这个函数很长,需要仔细梳理。

64c45edcgy1fkurhf2r9jj20ou3c9x1u.jpg

可见completeWork主要是完成reconciliation阶段的扫尾工作,重点是对HostComponent的props进行diff,并标记更新。

到这里,我们就讲完了reconciliation阶段。这个阶段主要负责产出effect list。所以可以说reconcile的过程相当于是一个纯函数,输入是fiber节点,输出一个effect list。side-effects是在commit阶段被应用到UI中的,这样就将side-effects从reconciliation中隔离开了。因为纯函数的可预测性,让我们可以随时中断reconciliation阶段的执行,而不用担心side-effects给让组件状态和实际UI产生不一致。

commit这个阶段有点像Git的commit概念。在缓冲区中的代码改动只有在commit之后才会被添加到Git的Object store中。

下面我们就来关注commit阶段的实现。看看effect list是如何被“提交”到UI中的。

commitAllWork

reconciliation阶段结束之后,我们需要将effect list更新到UI中。这就是commit节点的工作。commit阶段的入口是commitAllWork函数,我们来看看它的

64c45edcgy1fkorwi0836j20oz2f71b8.jpg

这里需要注意的是,React 16中的生命周期方法是在reconciliation和commit两个阶段中被调用的,commit阶段的commitAllLifeCycles函数中的生命周期方法包括componentDidMountcomponentDidUpdatecomponentWillUnmount三个。

64c45edcgy1fknn7fnubmj20eb08wjt7.jpg

reconciliation+commit流程总结

经过上述对reconciliation和commit两个阶段的源码分析,是不是觉得有些混乱?我总结了一张reconciliation+commit过程中的函数调用图,希望可以帮助你理清这两个阶段的函数调用流程。从图中我们可以看出,workLoop中调用了performUnitOfWorkcommitAllWork,分别作为reconciliation和commit两个阶段的入口。performUnitOfWork中又分为begin和complete两个阶段来

64c45edcgy1fkota00kyhj20o402ydg3.jpg

展望&&结语

潜伏的大招——异步渲染

在上文中,我们知道,React 16中默认没有开启异步渲染。用户的setState都是和React 15一样,在一个tick内完成的。fiber可以解决的问题,比如将优先级低的任务分散在多个帧中完成,在每一帧中留足够的时间给响应用户输入和渲染这样优先级高的任务。在默认不开启异步渲染的情况下,是不能做到的。因此我们期待未来版本的React可以开启这个杀手特性。

我们在阅读源码的过程中,看到了一些没有被文档记录的组件类型,比如CoroutineComponent和YieldComponent。这也许意味着未来React会把渲染的时机掌控权交给用户。我们可以定义一个CoroutineComponent,在reconcile完成后交出控制权给用户。由用户主动调用commit来让组件继续渲染。因为React将组件的渲染分为reconcile和commit两个阶段,reconcile又是没有副作用的,由多个院子操作组成。因此这样的设想是完全可行的。以上只是笔者的推测,丢一个A Clark的链接。

React 16的设计给前端框架带来的思考

这次React更新核心架构,让我们看到Facebook的工程师再次用技术推进了用户体验的极限。淘宝FED的口号是用技术为体验提供无限可能,笔者觉得这句话用来形容React也是很合适的。在React上,我们看到了一些借鉴自操作系统中的设计。Fiber可以被比作是一个轻量级线程。有自己的数据,也有优先级的分别。React的作用就是调度fiber,使得优先级高的任务优先执行,同时也保证低优先级的任务会在未来一段时间执行完毕。在diff算法的设计上,React借鉴了社区的经验,这是对社区的一种认可。