【React 16】模拟实现2

React | 2020-06-08 21:42:42 632次 3次

一、单链表

上一篇介绍了如何合理利用浏览器每一帧时间执行多个任务,本篇介绍任务队单链表实现。单链表是一种链式存取的数据结构,通过 next 指针指向下个数据单元。

微信截图_20200608220609.png

把之前的任务通过链表来实现,为了可以更好的控制执行,可中断某个操作,再继续执行,如下图所示:

微信截图_20200608223924.png

通过代码实现:

class Update {//payload数据或者 说元素
    constructor(payload, nextUpdate) {
        this.payload = payload;
        this.nextUpdate = nextUpdate;//指向下一个节点的指针
    }
}
class UpdateQueue {
    constructor() {
        this.baseState = null;//原状态
        this.firstUpdate = null;//第一个更新
        this.lastUpdate = null;//最后一个更新
    }
    enqueueUpdate(update) {
        if (this.firstUpdate == null) {
            this.firstUpdate = this.lastUpdate = update;
        } else {
            this.lastUpdate.nextUpdate = update;//上一个最后一个节点的nextUpdate指向自己
            this.lastUpdate = update;//让最后一个节指向自己
        }
    }
    //1.获取老状态。然后遍历这个链表,进行更新 得到新状态
    forceUpdate() {
        let currentState = this.baseState || {};//初始状态
        let currentUpdate = this.firstUpdate;
        while (currentUpdate) {
            let nextState = typeof currentUpdate.payload == 'function' ?
                currentUpdate.payload(currentState) : currentUpdate.payload;
            currentState = { ...currentState, ...nextState };//使用当前更新得到新的状态
            currentUpdate = currentUpdate.nextUpdate;// 找下一个节点
        }
        this.firstUpdate = this.lastUpdate = null;//更新完成后要把链表清空
        this.baseState = currentState;
        return currentState;
    }
}
//计数器 {number:0}  setState({number:1})  setState((state)=>({number:state.number+1}))
let queue = new UpdateQueue();
queue.enqueueUpdate(new Update({ }));
queue.enqueueUpdate(new Update({ num: 0 }));
queue.enqueueUpdate(new Update((state) => ({ num: state.num + 1 })));
queue.enqueueUpdate(new Update((state) => ({ num: state.num + 1 })));
queue.forceUpdate();
console.log(queue.baseState); //{ num: 2 }


二、递归树

在react之前的做法中,每一次更新都会遍历整棵树,找出差异化然后更新。

微信截图_20200609230218.png

let root = {
    key: 'A1',
    children: [
        {
            key: 'B1',
            children: [
                { key: 'C1', children: [] },
                { key: 'C2', children: [] },
            ]
        },
        {
            key: "B2", children: []
        }
    ]
}
function walk(vdom) {
    doWork(vdom);
    vdom.children.forEach((child) => {
        walk(child);
    });
}
function doWork(vdom) {
    console.log(vdom.key);
}
walk(root);

但是这样做有一个缺点就是组件数量庞大的时候,会严重占用浏览器主线程渲染,也就是一帧内必须要做完渲染,不然就会卡顿。当React决定要加载或者更新组件树时,会调用各个组件的生命周期函数,计算和比对 Virtual DOM,最后更新 DOM 树,如果这个之间用户进行了 input 输入操作或者点击动作,则界面不会有反应,交互体验就会变得很差,所以需要一种可中断的数据结构--链表来重新设计 react 的核心逻辑,结合上一篇介绍的浏览器渲染帧原理来进行空闲时间渲染,这样可以进一步改善交互体验(注意这样做并不是为了所谓的提升速度)。


三、Fiber介绍

把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会。

React Fiber 把更新过程碎片化,执行过程如下图所示,每执行完一段更新过程,就看看浏览器当前帧是否还有空闲时间,如果有则继续渲染,没有的话 react 就中断执行,将控制权交给浏览器(除非有优先级高的任务和已超时的任务必须执行),等待下一帧浏览器有空闲时间再去恢复渲染,从上次中断的地方。

微信截图_20200609235620.png

其中,维护每一个分片的数据结构,就是 Fiberrender 阶段会构建 fiber 树。和第二部分树对应的相同节点用 fiber 表示结构如下图所示:

微信截图_20200610001135.png

可以看到,现在每个节点的连接通过 child 指针指向子节点,sibling 指向自己的兄弟节点,每个子节点有个 return 指针指向自己的父节点,这个 return 就是单纯的一个指针命名,这样整颗 fiber 树就通过链表的形式展现。


四、Fiber渲染

Fiber 的渲染分为两个阶段,Reconciliation(调和/render阶段) 和 Commit(提交阶段)。

协调阶段: 可以认为是 Diff 阶段, 这个阶段可以被中断, 这个阶段会找出所有节点变更,例如节点新增、删除、属性变更等等, 这些变更React 称之为副作用(Effect)。

提交阶段: 将上一个阶段计算出来的需要处理的副作用(Effects)一次性执行。这个阶段必须同步执行,不能被打断。

按照之前的图中数据结构,我们先手动创建一颗 fiber 树(正常应该在render时候代码生成):

let A1 = { type: 'div', key: 'A1' };
let B1 = { type: 'div', key: 'B1', return: A1 }
let B2 = { type: 'div', key: 'B2', return: A1 }
let C1 = { type: 'div', key: 'C1', return: B1 }
let C2 = { type: 'div', key: 'C2', return: B1 }
A1.child = B1;
B1.sibling = B2;
B1.child = C1;
C1.sibling = C2;

 fiber 树按照深度优先遍历的规则进行渲染,如果到达某个节点后此节点无子节点则证明这个节点渲染完成,也是第一个渲染完成的节点,以此类推,如图所示(红色代表开始遍历,蓝色代表遍历完成):

微信截图_20200610010906.png

下面通过代码实现,并结合 requestIdleCallback 来进行任务分片执行:

let nextUnitOfWork = null;//下一个执行单元
function workLoop(deadline) {
    while ((deadline.timeRemaining() > 1 || deadline.didTimeout) && nextUnitOfWork) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
    if (!nextUnitOfWork) {
        console.log('render阶段结束了');
    } else {//请求下次浏览器空闲的时候帮我调
        requestIdleCallback(workLoop, { timeout: 1000 });
    }
}
function performUnitOfWork(fiber) {//A1 B1 C1 C2
    beginWork(fiber);//处理此fiber
    if (fiber.child) {//如果有儿子,返回大儿子
        return fiber.child;
    }//如果没有儿子,说明此fiber已经完成了
    while (fiber) {
        completeUnitOfWork(fiber);
        if (fiber.sibling) {
            return fiber.sibling;//如果说有弟弟返回弟弟
        }
        fiber = fiber.return;
    }
}
function completeUnitOfWork(fiber) {
    console.log('结束', fiber.key);//C1 C2 B1 B2 A1
}

function beginWork(fiber) {
    console.log('开始', fiber.key);//A1 B1 C1 C2 B2
}
//上面手动创建的fiber
nextUnitOfWork = A1;

requestIdleCallback(workLoop, { timeout: 1000 });

本篇介绍了一些前提数据结构准备,下一篇继续实现真实数据的 render 和 commit。

3人赞

分享到: