Skip to content

yuanxiang1990/emini

Repository files navigation

一个仿react的迷你库

主要实现react fiber架构。

TODO LIST

  1. setSatate支持(done)。
  2. class类型组件支持(done)。
  3. 组件生命周期支持(done)。
  4. fiber任务调度机制(初步实现)。
  5. react事件支持(初步实现)。
  6. 当当前组件无更新时需要跳过当前组件。(TODO 中)
  7. differ算法优化(解决每次differ相同key对象会重复创建的问题)(done)
  8. reconcile打算优化(目前存在bug,打断后会直接进入commit)(TODO 中)
  9. function component组件和Hook支持(TODO 低)
  10. 目前仅实现了rootQueue,针对单fiber的updateQueue没有做优先级处理,调度任务的粒度应该是针对每一次setState!!!(done)
  11. differ性能(去除链表转数组操作)(done)

为什么需要fiber架构

react的大体流程主要分为render阶段和commit阶段。render阶段主要differ出虚拟dom的变更,即收集各节点的effect。commit阶段主要将各节点的effcet应用到真实dom上。在react16之前,render阶段采用递归的方式实现,当dom节点较为复杂时,render的执行时间会非常长。由于js代码执行和浏览器dom操作都是执行在主线程上,render阶段可能会持续占用主线程,造成用户的很多操作的得不到马上响应,十分影响用户体验。所以react16提出了fiber架构来解决这个问题。

fiber核心问题分析

对哪个阶段进行拆分

fiber架构主要对render阶段进行拆分,commit阶段不需要拆分,一方面commit阶段不会需要非常久的执行时间,再加上对commit阶段进行拆分会导致dom操作执行一不分被终止,对用户体验的影响非常大。

如何拆分

首先需要改变以前的递归的differ方式,如果继续采用递归这种形式的话,是没法进行拆分的。react主要是讲之前的递归改成了循环的形式,因此,react引入了一种新的数据结构fiber。fiber的数据结构如下:

export class FiberNode {
    constructor(tag, type, pendingProps, key) {
        // Instance
        this.tag = tag;//节点类型
        this.key = key;
        this.stateNode = null;//对应真实dom或者class类型组件实例
        this.type = type;//对应的是以前reaat element的type属性

        // Fiber
        this.return = null;//父节点
        this.child = null;//子节点
        this.sibling = null;//兄弟节点
        this.index = 0;

        this.props = pendingProps;
        this.memoizedState = null;

        // Effects
        // this.effectTag = NoEffect;
        this.effects = [];//子节点变更
        this.effectTag = null;//当前节点变更
        this.expirationTime = NoWork;//节点到期时间,根据这个属性判断任务优先级

        this.alternate = null;//对应变更下的fiber节点

        this.updateQueue = [];//更新队列

    }
}

注意:改fiber结构做了很多简化,删除了一些不需要的属性。 这个alternate属性需要特别说一下,任务开始时会创建一颗workInProgress Tree。该对象是从alternate属性上创建的,workInProgress对象的alternate属性又指向当前的tree,当前的tree的alertnate对象又指向workInProgress。(貌似非常绕)后续的操作都是针对workInProgress操作,完成之后workInProgress就变成了新的fiber tree。下次执行时不会继创建workInProgress对象,而是把原对象的alternate属性赋值给workInProgress作为新的对象,此时的workInProgress的alternate就变成了原fibertree,然后把当前的child再赋值给新的workInprogre对象。后续的differ都是对当前对象的child和alternate对象的chidl做对比,如果相同则把alternate对象的child拷贝到新创建的fiber child的alertnate属性下。

上面说了一大推估计基本都看晕了。总结一下就是当前tree和alternate是相互引用关系,新的任务到来时,当前对象需要挂载到alternate下,则只需把新的workInProgress指向当前对象的alternate即可。而新的child都需要从最新的element下创建。这种相互持有引用的技术,react称之为双缓冲技术。

代码如下:

export function createWorkInProgress(current) {
    let workInProgress = current.alternate;
    if (workInProgress === null) {
        workInProgress = new FiberNode(current.tag);
        workInProgress.alternate = current;
        workInProgress.stateNode = current.stateNode;
        workInProgress.props = current.props || {};
        workInProgress.expirationTime = current.expirationTime;
        current.alternate = workInProgress;
    } else {
        workInProgress.effects = [];
        workInProgress.child = current.child;
        workInProgress.props = current.props;
    }
    workInProgress.expirationTime = current.expirationTime;
    workInProgress.updateQueue = current.updateQueue;
    return workInProgress;
}

下面我们来看以下html结构

<div id="A">
    <div id="B">
        <div id="D">aaa</div>
        <div id="E">bbb</div>
    </div>
    <div id="C">
        <div id="F">ccc</div>
        <div id="G">ddd</div>
    </div>
<div>

这些真实dom会转化成虚拟dom。react16之前的节点是按层级遍历对比的,先对比A节点,再对比B、C节点,在分别对比F、G和D、E将节点。react16之后会先遍历左子树,知道没有子节点后再回溯遍历对比父节点的兄弟节点的子树,最后一次遍历玩整棵树。在fiber架构中,遍历顺序会是A->B->C->D->E->aaa文本节点->bbb文本节点->F->G->ccc文本节点->ddd文本节点。 该结构是一个链表结构,可随时中断,之后再恢复。

如何给任务执行分片

任务执行分片主要是采用浏览器的requestIdleCallback这个api。requestIdleCallback可以在浏览器一帧的空闲时间执行。requestIdleCallback具体可参考http://www.zhangyunling.com/702.html/

如何划分任务优先级

在react中每个任务都会对应一次root,每个root会对应一个到期时间。在react中时间是反过来的,先出初始化一个非常大的时间然后依次减少。时间越大,任务优先级越高。(注意:在原生react中当当前时间减去expirationTime差值为0时会自动获得高优先级执行该任务,防止低优先级任务一直被插队)计算到期时间的方法如下

function computeExpirationForFiber(currentTime) {
    let expirationTime;
    if (isWorking) {
        if (isCommitting) {//commit阶段不能被打断
            expirationTime = Sync;//同步任务优先级最高
        } else {
            expirationTime = nextRenderExpirationTime
        }
    } else {
        if (isBatchingInteractiveUpdates) {//优先级较高的任务如用户交互等
            expirationTime = computeInteractiveExpiration(currentTime);//计算出的值较大,优先级高
        } else {//普通异步任务
            expirationTime = computeAsyncExpiration(currentTime);//计算的值较小,优先级低
        }
    }
    return expirationTime
}

fiber实现细节

React.render和setState执行入口api实现

React.render主要在初始化的时候执行,setState则是触发虚拟dom的更新。 react.render核心代码如下

const ReactDom = {
    render(element, container, callback) {
        const root = createHostRootFiber();
        root.stateNode = container;
        root.alternate = null;
        updateContainer(element, root);
    }
};

该方法主要是创建root节点后执行updateContainer方法。

export function updateContainer(children, containerFiberRoot) {
    let root = containerFiberRoot;
    let currentTime = requestCurrentTime();
    let expirationTime = computeExpirationForFiber(currentTime);
    root.expirationTime = expirationTime;
    root.updateQueue.push({
        element: children
    })
    return updateContainerAtExpirationTime(root, expirationTime)
}

function updateContainerAtExpirationTime(currentFiber, expirationTime) {
    currentFiber.expirationTime = expirationTime;
    scheduleWork(currentFiber, expirationTime)
}

updateContainer方法主要是计算expirationTime并将react element放入更新队列中,最后执行scheduleWork开始进入render阶段。

setState方法主要是调用内部的enqueueSetState方法。

const classComponentUpdater = {
    enqueueSetState: function (inst, payload) {
        const fiber = inst._reactInternalFiber;
        const currentTime = requestCurrentTime();
        const expirationTime = computeExpirationForFiber(currentTime);
        if (expirationTime > nextRenderExpirationTime) {//更高优先级任务到来时终止当前任务
            nextUnitOfWork = null;
            nextRenderExpirationTime = NoWork;
        }
        fiber.updateQueue.push({
            payload
        })
        const root = getRootFiber(fiber);
        root.expirationTime = expirationTime;
        scheduleWork(root, expirationTime);
    }
}

该方法和updateContainer方法类似,同样是拿到执行的root节点后计算expirationTime最后执行scheduleWork方法。注意,当前任务的优先级大于正在执行任务的优先级时,会中断当前任务,执行高优先级任务。

任务调度方法scheduleWork和requestWork

function scheduleWork(root, expirationTime) {
    addRootToSchedule(root, expirationTime);

    if (isBatchingUpdates) {
        return;
    }

    requestWork(root, expirationTime);
}

function requestWork(root, expirationTime) {
    if (isRendering) {
        //当前正在渲染时先不执行,最后一次再一起执行
        return
    }
    if (expirationTime === Sync) {
        performSyncWork(root);
    } else {
        performAsyncWork(root, expirationTime);
    }
}

scheduleWork方法首先会把root节点放入队列,其次需注意isBatchingUpdates和isRendering这两个参数。isBatchingUpdates参数主要是在事件回调方法执行之前会设置为true,主要是处理事件回调里面包含多个setState方法的情况,会把多个setState合成更新。isRendering参数代表当前正在渲染时先不执行,最后一次再一起执行。

performSyncWork和performAsyncWork方法

export function performSyncWork() {
    performWork(null)
}

function performAsyncWork(root, expirationTime) {
    recomputeCurrentRendererTime();
    requestIdleCallback((deadline) => {
        return performWork(deadline)
    })
}

两者都会执行performWork方法,异步方法会调用requestIdleCallback执行并传入deadline参数

root任务调度大循环

function performWork(deadline, root) {
    findHighestPriorityRoot();
    while (nextFlushedRoot !== null && nextFlushedExpirationTime !== NoWork) {
        performWorkOnRoot(deadline, nextFlushedRoot);
        findHighestPriorityRoot();
    }
}

每次循环都会调用findHighestPriorityRoot方法会找出优先级最高的root出来执行。

performWorkOnRoot方法


function performWorkOnRoot(deadline, root) {
    isWorking = true;
    isRendering = true;
    if (nextUnitOfWork == null) {
        workInProgress = createWorkInProgress(root);
        nextUnitOfWork = workInProgress;
        nextRenderExpirationTime = workInProgress.expirationTime;
    }
    try {
        workLoop(deadline);
    }
    catch (e) {
        nextUnitOfWork = throwException(nextUnitOfWork, e);
        console.error(e);
    }
    recomputeCurrentRendererTime();
    let expirationTime = workInProgress.expirationTime;
    //继续处理回调
    if (nextUnitOfWork) {
        if (currentRendererTime > expirationTime && deadline.didTimeout) {//帧超时退出
            requestIdleCallback((deadline) => {
                performWorkOnRoot(deadline, nextUnitOfWork);
            })
        }
        else {//错误异常退出等。。。
            performWorkOnRoot(deadline, nextUnitOfWork);
        }
    }
    /**
     * 任务已处理完,直接进入commit阶段
     */
    else {
        isCommitting = true;
        commitAllWork(pendingCommit);
        isCommitting = false;
        isRendering = false;
        isWorking = false;
        pendingCommit = null;
        nextRenderExpirationTime = NoWork;
        root.expirationTime = NoWork;
        rootQueue.splice(rootQueue.indexOf(root), 1);
        //workInProgress = null;
    }
}

该方法首先会调用createWorkInProgress方法构建一颗workInProgress树,workLoop方法则是循环处理每一个fiber节点。workLoop方法执行之后,如果fiber节点已经处理完,则会进入commit阶段。最后workInProgress树会变成当前树。

fiber节点处理循环

function workLoop(deadline) {
    if (deadline) {
        while (nextUnitOfWork && !deadline.didTimeout) {
            nextUnitOfWork = performUnitWork(nextUnitOfWork);
        }
    }
    else {
        while (nextUnitOfWork) {
            nextUnitOfWork = performUnitWork(nextUnitOfWork);
        }
    }
}


function performUnitWork(nextUnitOfWork) {
    const currentFiber = nextUnitOfWork;
    const nextChild = beginWork(currentFiber);
    finalizeInitialFiber(currentFiber, getRootFiber(currentFiber))
    if (nextChild) return nextChild;
    let topFiber = currentFiber;
    while (topFiber) {
        completeWork(topFiber);
        if (topFiber.sibling) {
            return topFiber.sibling
        }
        else {
            topFiber = topFiber.return;
        }
    }
    return null;

}
function beginWork(workInProgress) {
    workInProgress.effects.length = 0;
    switch (workInProgress.tag) {
        case tag.ClassComponent: {//处理class类型组件
            let update = {};
            workInProgress.updateQueue.forEach(item => {
                update = Object.assign(update, item.payload);
            })
            console.log(update, 'up')
            if (!isEmptyObject(update)) {
                workInProgress.stateNode._partialState = update;
                workInProgress.effectTag = Effect.UPDATE;
            }
            workInProgress.stateNode.updater = classComponentUpdater;
            return updateClassComponent(workInProgress);
        }
        case tag.HostRoot: {
            const update = workInProgress.updateQueue.shift();
            if (update) {
                workInProgress.props.children = update.element;
            }
            return updateHostComponent(workInProgress);
        }
        default: {
            return updateHostComponent(workInProgress);
        }
    }
}

workLoop会分同步和异步两种方式调用performUnitWork方法。performUnitWork方法则是单个节点的处理过程。beginWork方法相当于是节点分分类,根据节点的类型调用不同的方法differ节点。处理完之后completeWork依次收集节点变更到root节点。到此render阶段的任务执行完成。

commit阶段

/**
 * 进入commit阶段
 */
export function commitAllWork(topFiber) {
    console.log(topFiber.effects.slice(0))
    commitPreLifeCycle(topFiber);
    topFiber.effects.forEach(fiber => {
        let domParent = fiber.return;
        while (domParent.tag === tag.ClassComponent) {//class类型组件
            domParent = domParent.return;
        }
        if (fiber.effectTag === Effect.PLACEMENT) {
            commitPlacement(fiber, domParent);
        }
        if (fiber.effectTag === Effect.DELETION) {
            commitDeletion(fiber, domParent);
        }
        fiber.effectTag = null;
    })
    commitAfterLifeCycle(topFiber)
    topFiber.effects = [];
}

commit阶段比较简单,主要就是将收集到的变更应用到真实dom当中。

Releases

No releases published

Packages

No packages published