Skip to content
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实现原理 #100

Open
xwchris opened this issue Nov 6, 2019 · 0 comments
Open

React Fiber实现原理 #100

xwchris opened this issue Nov 6, 2019 · 0 comments

Comments

@xwchris
Copy link
Owner

xwchris commented Nov 6, 2019

本文主要对React Fiber的原理进行简单介绍,解决什么是Fiber的疑问。阅读本节需要你对React有一定的了解

React Fiber和React Hook简单实现 - Redul

知识准备

什么是React

React是用于构建用户界面的 JavaScript 库。同时React是一个pull类型的库,开发者只需要关注业务,不需要过多关注优化与调度等,这是与push类型的库不同的地方。

浏览器渲染

浏览器渲染主体流程如下,需要了解的是js与css都会阻塞浏览器的渲染

浏览器渲染图片

为什么需要Fiber

  1. 每一个状态的改变不是都需要马上反应在UI界面上
  2. 不同的状态改变应该有不同的优先级,如动画,输入等应该有更高的优先级
  3. 对于大量状态的改变复杂操作应该进行更好的调度,避免UI页面卡顿

Fiber vs Stack

stack demo page

fiber demo page

Fiber 原理

Fiber 结构

我们知道代码执行是在栈中执行,栈中的代码会一直执行直到栈为空。
所以为了实现上述的目标我们需要一个能够打断,保存恢复状态和自由调度的栈。这就是Fiber
在之前的React16之前的版本中我们都知道virtual dom(存储了待渲染节点的信息),这个就很适合做为自定义栈的栈帧,而且我们需要在其中添加更多的信息,这就个就被称为fiber节点(虚拟栈帧)

fiber结构

fiber中除了要渲染的节点信息,还包括了节点间的关系的信息,以及其他一些额外的信息

interface FiberNode<P = any> {
        tag: FiberNodeTag
        // element attrs
        // HOST_ROOT_NODE has node type
        type?: ElementType
        props?: PropsWithChildren<P>
        children: ElementChildren

        // fiber relations
        alternate?: FiberNode | null
        parent?: FiberNode | null
        child?: FiberNode | null
        sibling?: FiberNode | null

        // effect
        effectTag?: EffectTag | null
        effects: FiberNode[]

        // other
        statNode: HTMLElementOrText | RootHTMLElementWithFiberNode | null
        hooks?: Hook | null
        isPartialStateChanged?: boolean
        updateQueue?: HookEffect[]
        isMount?: boolean
    }

fiber树的整体结构是一个双向循环链表,这种结构能够更加快速的找到相对应的节点。

在Reconcile过程中为了能够知道之前节点的信息,需要将新的fiber节点与老fiber节点进行关联。

Fiber中会同时存在两种fiber tree,每次Reconcile的过程就是新fiber tree构建的过程,当commit之后新的fiber tree就变成了current fiber tree,如此循环往复。

fiber简单实现

Fiber Effect

在Reconcile的过程中,需要给节点设置状态,与旧节点相比需要达到的状态。每个fiber节点构建完成后(设置自己的effectTag状态),如果有effect则将自己以及其子孙元素放入父节点的effects中,这样层层构建,最终新的fiber tree的effects中存储的就是所有要处理的fiber node。然后进入到commit阶段,将所有的fiber node进行到dom的转换,进行UI页面的刷新。

// fiber effect
export enum EffectTag {
    NOTHING,
    UPDATE,
    REPLACE,
    ADD,
    REMOVE
}

Fiber 调度

Fiber既然是一个虚拟栈,那么就需要进行调度。为了实现更佳的UI体验,就需要在合适的时间执行我们的代码
这里介绍一个浏览器API
idle period

所以我们可以利用该函数在浏览器空闲的时候来执行我们的代码,这样可以达到不阻塞页面渲染的目的

window.requestIdleCallback(callback[, options])

该函数会在浏览器空闲的时候调用,并传递一个IdleDeadline对象给callback,我们要用到IdleDeadline.timeRemaining函数,该函数会返回一个值,告诉我们浏览器的idle时间还有多久,如果已经结束则值是0

export function render(element: ElementInput, containerDom: HTMLElement) {
    // clear all before render
    dispatcher.clearDomContent(containerDom)
    const rootFiberNode = createRootFiberNode(element, containerDom)
    taskQueue.push(rootFiberNode)

    requestIdleCallback(performWork)
    return containerDom
}

export function scheduleUpdate(fiberNode: FiberNode) {
    taskQueue.push(fiberNode)

    // when no work in progress, start immediately
    if (!nextUnitWork) {
        requestIdleCallback(performWork)
    }
}

function performWork(deadline: RequestIdleCallbackDeadline) {
    nextUnitWork = resolveNextUnitWork()
    if (!nextUnitWork) {
        commitAllWork()
        return
    }

    if (deadline.timeRemaining() > ENOUGH_TIME) {
        nextUnitWork = performUnitWork(nextUnitWork)
    }

    requestIdleCallback(performWork)
}

真正的React中使用的并不是RequestIdleCallback API,因为它有两个问题

  1. 兼容性不好
  2. 一秒钟仅能调用20次,对于UI任务来说基本没什么用

所以React中实际上是自己实现了一个requestIdleCallback,实现中要用的一个API

window.requestAnimationFrame(callback)

用该函数作为定时器,其会在下一次页面重绘前进行调用,精度较高。但它也有一个缺点,就是在后台的时候不会执行,这个时候可以使用setTimeout做降级处理

rAFID = requestAnimationFrame(function(timestamp) {
        // cancel the setTimeout
        localClearTimeout(rAFTimeoutID);
        callback(timestamp);
});
rAFTimeoutID = setTimeout(function() {
        // 定时 100 毫秒是算是一个最佳实践
        localCancelAnimationFrame(rAFID);
        callback(getCurrentTime());
}, 100);

有了定时器之后,我们根据当前时间performance.now()和每一帧的时间(假如是60fps则每一帧平均时间为16.6ms)算出下一帧的时间,执行的时候跟当前时间比对就可以知道是否还有空余时间

Fiber 优先级

为了更好的用户体验,需要让优先级更高的任务优先执行,如动画,输入等。Fiber中分为五种优先级,每种优先级对应一个过期时间。

// 5种优先级

// TODO: Use symbols?
var ImmediatePriority = 1;
var UserBlockingPriority = 2;
var NormalPriority = 3;
var LowPriority = 4;
var IdlePriority = 5;

// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) - 1
// 0b111111111111111111111111111111
var maxSigned31BitInt = 1073741823;

// 5种优先级对应的5种过期时间

// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY = maxSigned31BitInt;

每次循环,如果有过期的任务,那么无论如何要把过期的任务执行完毕,然后如果有剩余时间则按照到过期时间小的优先执行,以此类推。

参考资料&好文推荐

  1. react-fiber-architecture
  2. The how and why on React’s usage of linked list in Fiber to walk the component’s tree
  3. Didact Fiber: Incremental reconciliation
  4. learn-react-essence/调度原理
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant