React - Fiber原理

凯特林
• 阅读 1624

浏览器渲染

屏幕刷新率(FPS)

  • 浏览器的正常绘制频率是60次/秒,小于这个值时,用户会感觉到卡顿
  • 绘制一次的称为一帧,平均每帧16.6ms

  • 每个帧的开头包括样式计算、布局和绘制
  • js的执行是单线程,js引擎和页面渲染引擎都占用主线程,GUI渲染和Javascript执行两者是互斥的
  • 如果某个js任务执行时间过长,浏览器会推迟渲染,每帧的绘制时间超过16.6ms,造成页面卡顿
  • requestAnimationFrame回调函数会在绘制之前执行
  • 在绘制之后,如果还有剩余时间,会执行 requestIdleCallback

React - Fiber原理

requestIdleCallback

  • requestIdleCallback使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应
  • 正常帧任务完成后没超过16.6ms,说明时间有富余,此时就会执行 requestIdleCallback 里注册的任务
  • requestIdleCallback在部分低版本浏览器中不支持,React内部是通过MessageChannel来实现的

React - Fiber原理

<script>
        function sleep(d) {
            for (var t = Date.now(); Date.now() - t <= d;);
        }
        const works = [
            () => {
                console.log("第1个任务开始");
                sleep(20);//sleep(20);
                console.log("第1个任务结束");
            },
            () => {
                console.log("第2个任务开始");
                sleep(20);//sleep(20);
                console.log("第2个任务结束");
            },
            () => {
                console.log("第3个任务开始");
                sleep(20);//sleep(20);
                console.log("第3个任务结束");
            },
        ];

        requestIdleCallback(workLoop, { timeout: 1000 });
        function workLoop(deadline) {
            console.log('本帧剩余时间', parseInt(deadline.timeRemaining()));
            while ((deadline.timeRemaining() > 1 || deadline.didTimeout) && works.length > 0) {
                performUnitOfWork();
            }

            if (works.length > 0) {
                console.log(`只剩下${parseInt(deadline.timeRemaining())}ms,时间片到了等待下次空闲时间的调度`);
                requestIdleCallback(workLoop);
            }
        }
        function performUnitOfWork() {
            works.shift()();
        }
    </script> 

Fiber 解决了什么问题

React的渲染分为协调和提交两个阶段,协调就是遍历dom树,进行domdiff,收集差异的阶段。提交就是修改真实dom,进行页面绘制的阶段。

Fiber之前的协调

  • React 会递归遍历节点,比对VirtualDOM树,找出需要变动的节点,然后同步更新它们。这个过程 React 称为Reconcilation(协调)
  • 在Reconcilation期间,React 会一直占用着浏览器资源,一则会导致用户触发的事件得不到响应, 二则会导致掉帧,用户可能会感觉到卡顿

Fiber的协调

  • fiber使用链表数据结构,通过浏览器requestIdleCallbackapi,让协调过程实现了可中断执行,分片完成协调任务
  • 在浏览器空闲时去执行协调过程,遍历节点,收集变动的节点,避免了界面卡顿

什么是 React fiber

Fiber是一种调度策略

  • 我们可以通过某些调度策略合理分配CPU资源,从而提高用户的响应速度
  • 通过Fiber架构,让自己的协调过程变成可被中断。 适时地让出CPU执行权,让浏览器及时地响应用户的交互

Fiber是一个执行单元

  • Fiber是一个执行单元,每次执行完一个执行单元, React 就会检查现在还剩多少时间,如果没有时间就将控制权让出去

React - Fiber原理

Fiber是一种数据结构

  • React Fiber采用的链表的数据结构
  • 内部会将虚拟Dom转化成Fiber节点,通过链表结构将整个dom树表示出来
  • Fiber树采用孩子兄弟链表表示法,父节点的child指向第一个子节点,子节点的sibling指向下一个兄弟节点,子节点的return指向父节点
let fiberNode = {
    tag, // fiber类型  Host/Root
    type, // 虚拟dom的类型 div/span, 如果是类组件,这里是类名
    props, // 虚拟dom的属性
    stateNode, // 真实dom,如果是类组件,这里是类的实例
    // 构建链表的三个指针
    child, // 指向大儿子
    sibling, // 指向二弟
    return, // 指向父节点
    // 构建Effect List的指针
    firstEffect, // 第一个有副作用的子节点
    nextEffect,
    lastEffect,
    effectTag, // 副作用类型 插入、更新、删除

} 

React - Fiber原理

React fiber的调度顺序

react执行阶段

  • 每次渲染有两个阶段:Reconciliation(协调阶段)和Commit(提交阶段)
  • 协调阶段: 可以认为是 Diff 阶段, 这个阶段可以被中断, 这个阶段会找出所有节点变更,例如节点新增、删除、属性变更等等, 这些变更React 称之为副作用(Effect)
  • 提交阶段: 将上一个阶段收集的需要处理的副作用(Effects)一次性执行,将修改应用到真实Dom节点。这个阶段必须同步执行,不能被打断

构建Fiber树

  • 通过babel打包编译,将JSX元素转化为React.createElement()方法
  • render阶段,创建虚拟Dom节点,通过虚拟Dom创建fiber节点,构建Fiber链表

Fiber树的遍历和完成顺序

  • 从顶点开始遍历
  • 如果有第一个儿子,先遍历第一个儿子
  • 如果没有第一个儿子,标志着此节点遍历完成
  • 如果有弟弟遍历弟弟
  • 如果有没有下一个弟弟,返回父节点标识完成父节点遍历,如果有叔叔遍历叔叔
  • 没有父节点遍历结束
  • 遍历顺序:A1 B1 C1 C2 B2
  • 完成顺序:C1 C2 B1 B2 A1

React - Fiber原理

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
const rootFiber = A1;

//下一个工作单元
let nextUnitOfWork = null;
//render工作循环
function workLoop() {
    while (nextUnitOfWork) {
        //执行一个任务并返回下一个任务
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
    //render阶段结束
}
function performUnitOfWork(fiber) {
    beginWork(fiber);
    if (fiber.child) {//如果子节点就返回第一个子节点
        return fiber.child;
    }
    while (fiber) {//如果没有子节点说明当前节点已经完成了渲染工作
        completeUnitOfWork(fiber);//可以结束此fiber的渲染了 
        if (fiber.sibling) {//如果它有弟弟就返回弟弟
            return fiber.sibling;
        }
        fiber = fiber.return;//如果没有弟弟让爸爸完成,然后找叔叔
    }
}
function beginWork(fiber) {
    console.log('beginWork', fiber.key);
    //fiber.stateNode = document.createElement(fiber.type);
}
function completeUnitOfWork(fiber) {
    console.log('completeUnitOfWork', fiber.key);
}
nextUnitOfWork = rootFiber;

// 请求浏览器分配空闲时间片,执行任务
requestIdleCallback(workLoop, { timeout: 1000 }); 

收集Effect List

  • 遍历Fiber树将有副作用的fiber节点收集起来,形成一个单向链表
  • 遍历完成后通过 commitWork方法,将收集的副作用进行提交,修改真实dom
  • Effect List的顺序和fiber节点遍历的完成顺序一致

React - Fiber原理

let container = document.getElementById('root');
let C1 = { type: 'div', key: 'C1', props: { id: 'C1', children: [] } };
let C2 = { type: 'div', key: 'C2', props: { id: 'C2', children: [] } };
let B1 = { type: 'div', key: 'B1', props: { id: 'B1', children: [C1, C2] } };
let B2 = { type: 'div', key: 'B2', props: { id: 'B2', children: [] } };
let A1 = { type: 'div', key: 'A1', props: { id: 'A1', children: [B1, B2] } };

let nextUnitOfWork = null;
let workInProgressRoot = null;

// 1. 浏览器空闲执行
function workLoop() {
    let shouldYield = false;//是否要让出时间片或者说控制权
    while (nextUnitOfWork && !shouldYield) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);//执行完一个任务后
        shouldYield = deadline.timeRemaining() < 1;//没有时间的话就要让出控制权
    }
    if (!nextUnitOfWork && workInProgressRoot) {
        console.log('render阶段结束');
        commitRoot();
    }

    //不管有没有任务,都请求再次调度 每一帧都要执行一次workLoop,检查看有没有要执行的任务
    requestIdleCallback(workLoop, { timeout: 500 });

}

// 5. 提交变更
function commitRoot() {
    let fiber = workInProgressRoot.firstEffect;
    while (fiber) {
        console.log(fiber.key); //C1 C2 B1 B2 A1
        commitWork(fiber);
        fiber = fiber.nextEffect;
    }
    workInProgressRoot = null;
}

// 6. 修改真实dom
function commitWork(currentFiber) {
    currentFiber.return.stateNode.appendChild(currentFiber.stateNode);
}

// 2. 执行fiber工作
function performUnitOfWork(fiber) {
    beginWork(fiber);
    if (fiber.child) {
        return fiber.child;
    }
    while (fiber) {
        // 完成fiber工作
        completeUnitOfWork(fiber);
        if (fiber.sibling) {
            return fiber.sibling;
        }
        fiber = fiber.return;
    }
}

// 3. 开始工作
function beginWork(currentFiber) {
    if (!currentFiber.stateNode) {
        currentFiber.stateNode = document.createElement(currentFiber.type);//创建真实DOM
        for (let key in currentFiber.props) {//循环属性赋赋值给真实DOM
            if (key !== 'children' && key !== 'key')
                currentFiber.stateNode.setAttribute(key, currentFiber.props[key]);
        }
    }
    let previousFiber;
    // 创建子fiber
    currentFiber.props.children.forEach((child, index) => {
        let childFiber = {
            tag: 'HOST',
            type: child.type,
            key: child.key,
            props: child.props,
            return: currentFiber,
            effectTag: 'PLACEMENT',
            nextEffect: null
        }
        if (index === 0) {
            currentFiber.child = childFiber;
        } else {
            previousFiber.sibling = childFiber;
        }
        previousFiber = childFiber;
    });
}

//4. 完成fiber工作,收集effect List
function completeUnitOfWork(currentFiber) {
    const returnFiber = currentFiber.return;
    if (returnFiber) {
        if (!returnFiber.firstEffect) {
            returnFiber.firstEffect = currentFiber.firstEffect;
        }
        if (currentFiber.lastEffect) {
            if (returnFiber.lastEffect) {
                returnFiber.lastEffect.nextEffect = currentFiber.firstEffect;
            }
            returnFiber.lastEffect = currentFiber.lastEffect;
        }

        if (currentFiber.effectTag) {
            if (returnFiber.lastEffect) {
                returnFiber.lastEffect.nextEffect = currentFiber;
            } else {
                returnFiber.firstEffect = currentFiber;
            }
            returnFiber.lastEffect = currentFiber;
        }
    }
}
console.log(container);

workInProgressRoot = {
    key: 'ROOT',
    stateNode: container,
    props: { children: [A1] }
};
nextUnitOfWork = workInProgressRoot;//从RootFiber开始,到RootFiber结束

// 请求浏览器分配空闲时间片,执行任务
requestIdleCallback(workLoop, { timeout: 1000 }); 
点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Karen110 Karen110
2年前
一篇文章带你了解JavaScript日期
日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
2年前
thinkphp3.2.3模板渲染支持三元表达式
thinkphp3.2.3模板渲染支持三元表达式{$status?'正常':'错误'}{$info'status'?$info'msg':$info'error'}注意:三元运算符中暂时不支持点语法。如下:           <divclass"modalhidefade"id'myModa
Stella981 Stella981
2年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Stella981 Stella981
2年前
Python之time模块的时间戳、时间字符串格式化与转换
Python处理时间和时间戳的内置模块就有time,和datetime两个,本文先说time模块。关于时间戳的几个概念时间戳,根据1970年1月1日00:00:00开始按秒计算的偏移量。时间元组(struct_time),包含9个元素。 time.struct_time(tm_y
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这