自从react 16
出来之后,react fiber
相关的文章层出不穷,但大多都是讲解fiber
的数据结构,以及组件树的diff
是如何由递归改成循环遍历的。对于time slicing
的描述通常都说利用了requestIdleCallback
这个api来作调度,但对于任务如何调度却很难找到详细的描述。node
所以,本篇文章就是来干这个事情的,从源码角度来一步步阐述React Scheduler
是怎么实现任务调度的。react
虽说标题是React Scheduler
,但本文的内容跟react
是不相关的,由于任务调度器其实跟react
是没有关系的,它只是描述怎么在合适的时机去执行一些任务,也就是说你即便没有react
基础也能够进行本文的阅读,若是你是框架做者,也能够借鉴这个scheduler
的实现,在本身的框架里来进行任务调度。git
react v16.7.0
版本的源码,请注意时效性。Scheduler.js
接下来先来了解一下阅读本文须要知道的一些基础知识。github
window.performance.now
这个是浏览器内置的时钟,从页面加载开始计时,返回到当前的总时间,单位ms
。意味着你在打开页面第10分钟在控制台调用这个方法,返回的数字大概是 600000(误)。api
window.requestAnimationFrame
这个方法应该很常见了,它让咱们能够在下一帧开始时调用指定的函数。它的执行是是跟随系统的刷新频率的。requestAnimationFrame
方法接收一个参数,即要执行的回调函数。这个回调函数会默认地传入一个参数,即从打开页面到回调函数被触发时的时间长度,单位为毫秒。浏览器
能够理解为系统在调用回调前立马执行了一下performance.now()
传给了回调当参数。这样咱们就能够在执行回调的时候知道当前的执行时间了。bash
requestAnimationFrame(function F(t) {
console.log(t, '===='); //会不断打印执行回调的时间,若是刷新频率为60Hz,则相邻的t间隔时间大约为1000/60 = 16.7ms
requestAnimationFrame(F)
})
复制代码
requestAnimationFrame
有个特色,就是当页面处理未激活的状态下,requestAnimationFrame
会中止执行;当页面后面再转为激活时,requestAnimationFrame
又会接着上次的地方继续执行。数据结构
window.MessageChannel
这个接口容许咱们建立一个新的消息通道,并经过它的两个MessagePort(port1,port2)
属性发送数据。 示例代码以下框架
var channel = new MessageChannel();
var port1 = channel.port1;
var port2 = channel.port2;
port1.onmessage = function(event){
console.log(event.data) // someData
}
port2.postMessage('someData')
复制代码
这里有一点须要注意,onmessage
的回调函数的调用时机是在一帧的paint完成以后。据观察vue
的nextTick
也是用MessageChannel
来作fallback
的(优先用setImmediate
)。
react scheduler
内部正是利用了这一点来在一帧渲染结束后的剩余时间来执行任务的
先默认你们对链表有个基本的认识。没有的话本身去补一下知识。
这里要介绍的是双向循环链表
previous
和next
两个属性来分别指向先后两个节点。previous
指向最后一个节点,造成一个环形的人体蜈蚣
。//person的类型定义
interface Person {
name : string //姓名
age : number //年龄,依赖这个属性排序
next : Person //紧跟在后面的人,默认是null
previous : Person //前面相邻的那我的,默认是null
}
var firstNode = null; //一开始链表里没有节点
//插入的逻辑
function insertByAge(newPerson:Person){
if(firstNode = null){
//若是 firstNode为空,说明newPerson是第一我的,
//把它赋值给firstNode,并把next和previous属性指向自身,自成一个环。
firstNode = newPerson.next = newPerson.previous = newPerson;
} else { //队伍里有人了,新来的人要找准本身的位置
var next = null; //记录newPerson插入到哪一个人前边
var person = firstNode; // person 在下边的循环中会从第一我的开始日后找
do {
if (person.age > newPerson.age) {
//若是person的年龄比新来的人大,说明新来的人找到位置了,他刚好要排在person的前边,结束
next = person;
break;
}
//继续找后面的人
node = node.next;
} while (node !== firstNode); //这里的while是为了防止无限循环,毕竟是环形的结构
if(next === null){ //找了一圈发现 没有person的age比newPerson大,说明newPerson应该放到队伍的最后,也就是说newPerson的后面应该是firstNode。
next = firstNode;
}else if(next === firstNode){ //找第一个的时候就找到next了,说明newPerson要放到firstNode前面,这时候firstNode就要更新为newPerson
firstNode = newPerson
}
//下面是newPerson的插入操做,给next及previous两我的的先后连接都关联到newPerson
var previous = next.previous;
previous.next = next.previous = newPerson;
newPerson.next = next;
newPerson.previous = previous;
}
//插入成功
}
//删除第一个节点
function deleteFirstPerson(){
if(firstNode === null) return; //队伍里没有人,返回
var next = firstNode.next; //第二我的
if(firstNode === next) {
//这时候只有一我的
firstNode = null;
next = null;
} else {
var lastPerson = firstNode.previous; //找到最后一我的
firstNode = lastPerson.next = next; //更新新的第一人
next.previout = lastPerson; //并在新的第一人和最后一人之间创建链接
}
}
复制代码
因为react16
内大量利用了链表来记录数据,尤为react scheduler
内对任务的操做使用了双向循环链表结构。因此理解了上述的代码,对于理解react
对任务的调度就会比较容易了。
注:为了梳理总体的运行流程,下面的示例代码有可能会在源码基础上有少许删减
```
getCurrentTime = function() {
return performance.now();
//若是不支持performance,利用 Date.now()作fallback
}
```
复制代码
react内对任务定义的优先级分为5种,数字越小优先级越高
var ImmediatePriority = 1; //最高优先级
var UserBlockingPriority = 2; //用户阻塞型优先级
var NormalPriority = 3; //普通优先级
var LowPriority = 4; // 低优先级
var IdlePriority = 5; // 空闲优先级
复制代码
这5种优先级依次对应5个过时时间
// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) - 1
var maxSigned31BitInt = 1073741823;
// 立马过时 ==> ImmediatePriority
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// 250ms之后过时
var USER_BLOCKING_PRIORITY = 250;
//
var NORMAL_PRIORITY_TIMEOUT = 5000;
//
var LOW_PRIORITY_TIMEOUT = 10000;
// 永不过时
var IDLE_PRIORITY = maxSigned31BitInt;
复制代码
每一个任务在添加到链表里的时候,都会经过 performance.now() + timeout
来得出这个任务的过时时间,随着时间的推移,当前时间会愈来愈接近这个过时时间,因此过时时间越小的表明优先级越高。若是过时时间已经比当前时间小了,说明这个任务已通过期了还没执行,须要立马去执行(asap
)。
上面的maxSigned31BitInt
,经过注释能够知道这是32
位系统V8
引擎里最大的整数。react
用它来作IdlePriority
的过时时间。
据粗略计算这个时间大概是12.427
天。也就是说极端状况下你的网页tab
若是能一直开着到12天半,任务才有可能过时。
function scheduleCallback()
unstable_scheduleCallback
,意思是当前仍是不稳定的,这里就以scheduleCallback
做名字。下面上代码
function scheduleCallback(callback, options? : {timeout:number} ) {
//to be coutinued
}
复制代码
这个方法有两个入参,第一个是要执行的callback
,暂时能够理解为一个任务。第二个参数是可选的,能够传入一个超时时间来标识这个任务过多久超时。若是不传的话就会根据上述的任务优先级肯定过时时间。
//这是一个全局变量,表明当前任务的优先级,默认为普通
var currentPriorityLevel = NormalPriority
function scheduleCallback(callback, options? : {timeout:number} ) {
var startTime = getCurrentTime()
if (
typeof options === 'object' &&
options !== null &&
typeof options.timeout === 'number'
){
//若是传了options, 就用入参的过时时间
expirationTime = startTime + options.timeout;
} else {
//判断当前的优先级
switch (currentPriorityLevel) {
case ImmediatePriority:
expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
expirationTime = startTime + USER_BLOCKING_PRIORITY;
break;
case IdlePriority:
expirationTime = startTime + IDLE_PRIORITY;
break;
case LowPriority:
expirationTime = startTime + LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
}
}
//上面肯定了当前任务的截止时间,下面建立一个任务节点,
var newNode = {
callback, //任务的具体内容
priorityLevel: currentPriorityLevel, //任务优先级
expirationTime, //任务的过时时间
next: null, //下一个节点
previous: null, //上一个节点
};
//to be coutinued
}
复制代码
上面的代码根据入参或者当前的优先级来肯定当前callback
的过时时间,并生成一个真正的任务节点。接下来就要把这个节点按照expirationTime
排序插入到任务的链表里边去。
// 表明任务链表的第一个节点
var firstCallbackNode = null;
function scheduleCallback(callback, options? : {timeout:number} ) {
...
var newNode = {
callback, //任务的具体内容
priorityLevel: currentPriorityLevel, //任务优先级
expirationTime, //任务的过时时间
next: null, //下一个节点
previous: null, //上一个节点
};
// 下面是按照 expirationTime 把 newNode 加入到任务队列里。参考基础知识里的person排队的例子
if (firstCallbackNode === null) {
firstCallbackNode = newNode.next = newNode.previous = newNode;
ensureHostCallbackIsScheduled(); //这个方法先忽略,后面讲
} else {
var next = null;
var node = firstCallbackNode;
do {
if (node.expirationTime > expirationTime) {
next = node;
break;
}
node = node.next;
} while (node !== firstCallbackNode);
if (next === null) {
next = firstCallbackNode;
} else if (next === firstCallbackNode) {
firstCallbackNode = newNode;
ensureHostCallbackIsScheduled(); //这个方法先忽略,后面讲
}
var previous = next.previous;
previous.next = next.previous = newNode;
newNode.next = next;
newNode.previous = previous;
}
return newNode;
}
复制代码
ensureHostCallbackIsScheduled
就是前面讲的双向循环链表的插入逻辑。ensureHostCallbackIsScheduled
方法执行的两个分支。因此咱们如今应该知道,ensureHostCallbackIsScheduled
是用来在合适的时机去启动任务执行的。接下来就须要实现这么一个功能,如何在合适的时机去执行一个function。
requestIdleCallback pollyfill
如今请暂时忘掉上面那段任务队列相关的事情,来思考如何在浏览器每一帧绘制完的空闲时间来作一些事情。
答案能够是requestIdleCallback
,但因为某些缘由,react团队放弃了这个api,转而利用requestAnimationFrame
和MessageChannel
pollyfill
了一个requestIdleCallback
function requestAnimationFrameWithTimeout()
首先介绍一个超强的函数,代码以下
var requestAnimationFrameWithTimeout = function(callback) {
rAFID = requestAnimationFrame(function(timestamp) {
clearTimeout(rAFTimeoutID);
callback(timestamp);
});
rAFTimeoutID = setTimeout(function() {
cancelAnimationFrame(rAFID);
callback(getCurrentTime());
}, 100);
}
复制代码
这段代码什么意思呢?
当咱们调用requestAnimationFrameWithTimeout
并传入一个callback
的时候,会启动一个requestAnimationFrame
和一个setTimeout
,二者都会去执行callback
。但因为requestAnimationFrame
执行优先级相对较高,它内部会调用clearTimeout
取消下面定时器的操做。因此在页面active
状况下的表现跟requestAnimationFrame
是一致的。
到这里你们应该明白了,一开始的基础知识里说了,requestAnimationFrame
在页面切换到未激活的时候是不工做的,这时requestAnimationFrameWithTimeout
就至关于启动了一个100ms
的定时器,接管任务的执行工做。这个执行频率不高也不低,既能不影响cpu能耗,又能保证任务能有必定效率的执行。
下面咱们暂时先认为requestAnimationFrameWithTimeout
等价于 requestAnimationFrame
(不知不觉篇幅已经这么长了,今天先写到这里吧,下次有机会再更)