react fiber架构及源码解析

267 阅读6分钟

用过react框架的开发者都知道react 16 后应用到fiber 架构,今天来跟大家唠叨一下fiber及react源码,虽然react17也已经发布,但官方表示了并没有其他多大的变化,所以还是基于16版本。

fiber

首先一点是官方为什么要用fiber及什么时fiber,这个就要从16之前的版本来说了,在没有应用之前,官方的一句话解释是“React Fiber是对核心算法的一次重新实现”,说白了引用了fiber是未来提高性能

同步更新过程的局限

在现有React中,更新过程是同步的,这可能会导致性能问题。

当React决定要加载或者更新组件树时,会做很多事,比如调用各个组件的生命周期函数,计算和比对Virtual DOM,最后更新DOM树,这整个过程是同步进行的,也就是说只要一个加载或者更新过程开始,那React就以不破楼兰终不还的气概,一鼓作气运行到底,中途绝不停歇。

表面上看,这样的设计也是挺合理的,因为更新过程不会有任何I/O操作嘛,完全是CPU计算,所以无需异步操作,的确只要一路狂奔就行了,但是,当组件树比较庞大的时候,问题就来了。 假如更新一个组件需要1毫秒,如果有200个组件要更新,那就需要200毫秒,在这200毫秒的更新过程中,浏览器那个唯一的主线程都在专心运行更新操作,无暇去做任何其他的事情。想象一下,在这200毫秒内,用户往一个input元素中输入点什么,敲击键盘也不会获得响应,因为渲染输入按键结果也是浏览器主线程的工作,但是浏览器主线程被React占着呢,抽不出空,最后的结果就是用户敲了按键看不到反应,等React更新过程结束之后,咔咔咔那些按键一下子出现在input元素里了。 这就是所谓的界面卡顿,很不好的用户体验。

简单一点来说就是,react更新过程,如果更新几百个组件,这种遍历是递归调用,执行栈会越来越深。而且 不能中断,因为中断后再想恢复 就非常难了,遍历很深的话,因为JavaScript单线程的特点,每个同步任务不能耗时太长,不然就会让程序不会对其他输入作出相应,React的更新过程就是犯了这个禁忌,而React Fiber就是要改变现状。

而fiber实现原理是:把整个任务分解成很多小任务,每次执行一个任务后看一下有没有剩余时间,如果有,继续下一个任务,如果没有时间,则放弃执行,交给浏览器进行调度

所以fiber 是一个可执行单元,是一种数据结构

requestIdleCallback

一般屏幕的刷新频率24Hz,一帧16.6 如果每次执行一个fiber因为此任务的执行时间已经超过了16.6毫秒, 那么需要把控制 权交给浏览器。 其中window.requestIdleCallback(callback, { timeout: 1000 });react内部调用了window里面的这个函数,

  • callback:回调,即空闲时需要执行的任务,该回调函数接收一个IdleDeadline对象作为入参。其中IdleDeadline对象包含:

  • didTimeout,布尔值,表示任务是否超时,结合 timeRemaining 使用。

  • timeRemaining(),表示当前帧剩余的时间,也可理解为留给任务的时间还有多少。 我们写一个案例来模拟一下,感受一下requestIdleCallback的用法:

//这是一个全局属性
       //我作为用户,告诉浏览器,我现在执行callback函数,但是它的优先级比较低。告诉 浏览器,可以空闲的时候执行callback
       //但是如何这个到了超时时间了,就必须马上执行
       //window.requestIdleCallback(callback, { timeout: 1000 });

       function sleep(delay) { //d=50//
           //在JS里如何实现睡眠的功能 t=当前时间
           for (var start = Date.now(); Date.now() - start <= delay;) { 
               console.log(222);
           }
       }
       sleep(2)
       console.log(444);
       let allStart = Date.now();
       //fiber是把整个任务分成很多个小任务,每次执行一个任务
       //执行完成后会看看有没剩余时间,如果有继续下一个任务,如果没有放弃执行,交给浏览器进行调度
       const works = [
           () => {
               console.log('第1个任务开始');
               while (true) { }
               sleep(3);//一帧16.6 因为此任务的执行时间已经超过了16.6毫秒,所需要把控制 权交给浏览器
               console.log('第1个任务结束 ');
           },
           () => {
               console.log('第2个任务开始');
               sleep(3);
               console.log('第2个任务结束 ');
           },
           () => {
               console.log('第3个任务开始');
               sleep(3);
               console.log('第3个任务结束 ');
               console.log(Date.now() - allStart);
           }
       ]
           //告诉 浏览器在1000毫秒后,即使你没有空闲时间也得帮我执行,因为我等不及你
       // requestIdleCallback(workLoop, { timeout: 1000 });
       //deadline是一个对象 有两个属性
       // timeRemaining()可以返回此帧还剩下多少时间供用户使用
       // didTimeout 此callback任务是否超时
       function workLoop(deadline) {
           console.log(`本帧的剩余时间为${parseInt(deadline.timeRemaining())}`);
           //如果此帧的剩余时间超过0,或者此已经超时了
           while ((deadline.timeRemaining() > 1 || deadline.didTimeout) && works.length > 0) {
               performUnitOfWork();
           }//如果说没有剩余时间了,就需要放弃执行任务控制权,执行权交还给浏览器
           if (works.length > 0) {//说明还有未完成的任务
               window.requestIdleCallback(workLoop, { timeout: 1000 });
           }
       }
       function performUnitOfWork() {
           //shift取出数组中的第1个元素
           works.shift()();
       }

现在我把更新遍历加到requestIdleCallback中:

/**
 * 1.从顶点开始遍历
 * 2.如果有大儿子,先遍历大儿子
 *
 */
function sleep(delay) { //d=50//
    //在JS里如何实现睡眠的功能 t=当前时间
    for (var start = Date.now(); Date.now() - start <= delay;) { }
}
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;

let nextUnitOfWork = null;//下一个执行单元
let startTime = Date.now();
function workLoop(deadline) {
    //while (nextUnitOfWork) {//如果有待执行的执行单元,就执行,然后会返回下一个执行单元
    while ((deadline.timeRemaining() > 1 || deadline.didTimeout) && nextUnitOfWork) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
    if (!nextUnitOfWork) {
        console.log('render阶段结束了');
        console.log(Date.now() - startTime);
    } 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);//A1 B1 C1 C2
}

function beginWork(fiber) {
    sleep(20);
    console.log('开始', fiber.key);//A1 B1 C1 C2  B2
}
nextUnitOfWork = A1;
requestIdleCallback(workLoop, { timeout: 1000 });

element.js

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;
module.exports = A1;

我们再看看更新队列:

/**
 * 在Fiber中 很多地方都用到链表
 */
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({ name: 'zhufeng' }));
queue.enqueueUpdate(new Update({ number: 0 }));
queue.enqueueUpdate(new Update((state) => ({ number: state.number + 1 })));
queue.enqueueUpdate(new Update((state) => ({ number: state.number + 1 })));
queue.forceUpdate();
console.log(queue.baseState);

queue.enqueueUpdate像不像你在外部更新状态的写法,没错,react内部事用链表的方式实现的。

github源码 gitee.com/yokeney/rea…