react之scheduler

react相关库源码浅析, react ts3 项目

推荐

强烈推荐一篇非常好的文章:如何使用scheduler以及react并发模式做性能优化。需要特别注意的是文中的一段话:

To remove these limitations, the Google Chrome team is working together with React, Polymer, Ember, Google Maps, and the Web Standards Community to create a Scheduling API in the browser. What an exciting time!

vue或许只能针对chrome等进行调整优化,但是chrome却可以为react的一些需求进行优化。不得不说在这个方面react还是挺前沿的。

前言

内容有点多,会慢慢更新完善,写完论文再回来完善。react版本是16.6,或许以后会对比一下新版的实现区别。更多的源码分析请关注上面第一个项目,第二个项目是ts的一个项目,也有比较细节的文档。文中错误可能会很多很繁琐,不过希望开头的设计思想和例子可以帮助你理解后面的源码分析。

设计思想

1、为各个事件回调函数设置相应的优先级,然后根据自定义或者默认优先级确定到期时间,以此为基础,构建一个双向循环列表当做任务队列,每个节点存储了任务的回调函数以及到期时间,优先级,前一个节点后一个节点。

2、形成的任务链表的执行准则是:当前帧有空闲时间,则执行任务。即便没有空闲时间但是当前任务链表有任务到期了或者有立即执行任务,那么必须执行的时候就以丢失几帧的代价,执行任务链表中到期的任务。执行完的任务都会被从链表中删除。每次在执行任务链表中到期的任务的那段时间里,顺便把优先级最高需要立即执行的任务都执行。

总览图:flush*为任务执行模块,主要用于执行任务的回调函数,对任务链表进行操作。idleTick与animationTick为直接调度模块,根据当前帧的空闲时间与任务链表最小到期时间来控制是否在当前帧执行任务链表还是在下一帧继续处理。ensureHostCallbackIsScheduled与requestHostCallback组成任务执行模块与调度模块的枢纽,协调两者的工作,并且ensureHostCallbackIsScheduled为外部API提供入口。

例子

考虑一种比较简单的逻辑,任务链表的执行主要是通过idleTick函数调用flushWork实现的,因此分析idleTick函数处理的三种情况:

情况1:当前帧截止时间大于当前时间,说明当前帧还有时间执行任务链表节点中的回调函数,因此执行flushWork。

情况2:如果当前帧截止时间小于或者等于当前时间,说明当前帧过期了,没有剩余时间执行任务回调函数,但是如果任务链表的最小到期时间已经过期了或者有立即执行的任务,那么说明这个任务链表中的任务非得执行不可,那就直接阻塞渲染,将接下的几个渲染帧的时间用来执行当前过期的任务链表。

情况3:如果当前帧截止时间小于或者等于当前时间,说明当前帧过期了,没有剩余时间执行任务回调函数,并且任务链表的最小到期时间还没到,因此这个任务链表还不急着执行,可以放到下一帧(animation frame fire的时候调用animationTick触发Message事件调用idleTick)去处理,依然分三种情况进行处理。

给出一张图如下或许你会有一个更加清晰的理解:

优先级以及对应的过期时间

五个优先级

var ImmediatePriority = 1;
var UserBlockingPriority = 2;
var NormalPriority = 3;
var LowPriority = 4;
var IdlePriority = 5;
复制代码

五个优先级对应的过期时间

var maxSigned31BitInt = 1073741823;
//	过期时间
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
var USER_BLOCKING_PRIORITY = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
var IDLE_PRIORITY = maxSigned31BitInt;
复制代码

一些变量

// 回调函数被存储双向循环链表中
var firstCallbackNode = null;
//当前事件开始时间
var currentEventStartTime = -1;
//当前事件到期时间
var currentExpirationTime = -1;
复制代码

预备知识

浏览器渲染帧与显示屏的刷新频率

帧:通俗来说就是一张一张展示的画面(学过电视原理的应该不会陌生,本人本科学电子做硬件的。), 由于现在广泛使用的屏幕都有固定的刷新率(比如最新的一般在 60Hz), 在两次硬件刷新之间浏览器进行两次重绘是没有意义的只会消耗性能。因此浏览器的渲染出一帧画面的间隔应该就是硬件的每一帧图像的时间间隔,即刷新频率的倒数。

那么在浏览器呈现两幅图像的空闲(idle)时间里,也就是16.7ms的时间里需要执行如下操作:

  • 脚本执行(JavaScript):脚本造成了需要重绘的改动,比如增删 DOM、请求动画等
  • 样式计算(CSS Object Model):级联地生成每个节点的生效样式。
  • 布局(Layout):计算布局,执行渲染算法
  • 重绘(Paint):各层分别进行绘制(比如 3D 动画)
  • 合成(Composite):合成各层的渲染结果

在这16.7ms中,包括了js脚本执行,需要js线程,而渲染需要的是gui渲染线程,而这两个线程是互斥的。由于GUI渲染线程与JavaScript执行线程是互斥的关系,当浏览器在执行JavaScript程序的时候,GUI渲染线程会被保存在一个队列中,直到JS程序执行完成,才会接着执行。因此如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。

如下图所示为渲染帧,代码在github中reactNote仓库中,这里展示一下:

var start = null;
var element = document.getElementById("move")
element.style.position = 'absolute';

function step(timestamp) {
    console.log("timestamp",timestamp)
    if (!start) start = timestamp;
    var progress = timestamp - start;
    element.style.left = Math.min(progress / 10, 200) + 'px';
    if (progress < 400) {

        window.requestAnimationFrame(step);
        window.postMessage({},"*");
    }
}

var idleTick = function(){
    console.log("idleTick")
}

window.addEventListener('message', idleTick, false);

window.requestAnimationFrame(step);
复制代码

注意:

1、从图中可以看到布局重绘合成之后并不代表是帧的结束。本文的当前帧截止时间frameDeadLine是animation frame fired开始时间 + activeFrameTime,activeFrameTime这个值就是FPS,也就是浏览器当前的刷新频率,表示流畅程度,这个值随着系统的运行而变化。

2、从图中还可以看到postmessage在合成之后执行。

window.requestAnimationFrame

当你准备更新动画时你应该调用此方法。这将使浏览器在下一次重绘之前调用你传入给该方法的动画函数(即你的回调函数)。回调函数执行次数通常是每秒60次,但在大多数遵循W3C建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。为了提高性能和电池寿命,因此在大多数浏览器里,当requestAnimationFrame() 运行在后台标签页或者隐藏的