老大 react 说:schedule,我们今年的小目标是一个亿
hello,这里是潇晨,今天来讲个故事
讲个故事:
从前,有家 z 公司,z 公司的 ceo 叫react
,它收下有个小弟或者叫小 leader,schedule
schedule
每天负责消化老大react
画的大饼,然后将拆解成一个个小小的task
,给下面的小弟去完成,并且负责划分优先级,调度小弟们的任务排序。
那schedule
怎样给这些任务划分优先级呢,它想了个最简单的办法,用deadline
或者过期时间,给这些task
划分优先级,过期时间越短说明这个任务越紧急,赶紧分配苦力(下面的小弟)去完成,过期时间越长,说明这个task
越不紧急,可以以后慢慢干,还有一类task
已经过了它的deadline
,这个过期的任务优先级最高,没办法,延期之后也是要完成的,可怜了程序员小哥哥了。
于是小 leader,scheduler
把老板的饼掰碎了,然后给这些小task
按照deadline
排了个优先级,于是程序员小哥哥开始接任务了
程序员小哥哥 A 接受了task1
和task2
,于是他给自己排了个任务清单,按照优先级先做task1
,然后做task2
,于是小哥进入密集开发中(render
阶段),正在做task1
但是天又不测风云,老板根据业务的需要,给scheduler
下达了一个非常紧急的需求,苦了程序员小哥了,scheduler
说,唉,没办法呀,加加班,把这个非常紧急的需求现插个队吧,程序员小哥单线程的,每次只能做一个task
,于是插个队,加加班做最紧急的需求task0
吧。
接下来就是密集的加班中。。。(这一阶段称为render
阶段)
终于在不屑的努力下,最终程序员小哥还是加班加点的把所有任务完成了,交给测试验证(commit
阶段),
以上情况是有紧急任务时候的打断,还有一种情况是老板给的大饼很难消化,但是这个task2
还没到达deadline
,程序员小哥在做这个任务的时候遇到了困难,所以就先放一放吧,反正是个艰巨的任务,在空闲的时候在做吧,先完成优先级高的task0
和task1
,有时间在做task2
进入正题:
当我们在类似下面的搜索框组件进行搜索时会发现,组件分为搜索部分和搜索结果展示列表,我们期望输入框能立刻响应,搜素列表可以有等待的时间,如果搜索列表数据量很大,在进行渲染的时候,我们又输入了一些文字,因为用户输入事件的优先级是很高的,所以就要停止结果列表的渲染,这就引出了不同任务之间的优先级和调度
Scheduler
我们知道如果应用占用较长的js
执行时间,比如超过了设备一帧的时间,那么设备的绘制就会出现不流畅的现象。
Scheduler
主要的功能是时间切片和调度优先级,react
在对比节点差异的时候会占用一定的 js 执行时间,Scheduler
内部借助MessageChannel
实现了在浏览器绘制之前指定一个时间片,如果react
在指定时间内没执行完差异的对比,Scheduler
就会强制交出执行权给浏览器
时间切片
在浏览器的一帧中 js 的执行时间如下
requestIdleCallback
是在浏览器重绘重排之后,如果还有空闲就可以执行未完成的任务,所以为了不影响重绘重排,可以在浏览器在requestIdleCallback
中执行耗性能的计算,但是由于requestIdleCallback
存在兼容和触发时机不稳定的问题,scheduler
中采用MessageChannel
来实现requestIdleCallback
,如果当前环境不支持MessageChannel
就采用setTimeout
。
在performUnitOfWork
(render
阶段的起点)之后会执行 render 阶段和commit
阶段,如果在浏览器的一帧中,cup
的计算还没完成,就会让出 js 执行权给浏览器,这个判断在workLoopConcurrent
函数中,shouldYield
就是用来判断剩余的时间有没有用尽。在源码中每个时间片时 5ms,这个值会根据设备的fps
调整。
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {//如果fiber链还没遍历完,没有被暂停或者打断
performUnitOfWork(workInProgress);//执行render阶段
}
}
复制代码
function forceFrameRate(fps) {//计算时间片
if (fps < 0 || fps > 125) {
console['error'](
'forceFrameRate takes a positive int between 0 and 125, ' +
'forcing frame rates higher than 125 fps is not supported',
);
return;
}
if (fps > 0) {
yieldInterval = Math.floor(1000 / fps);
} else {
yieldInterval = 5;//时间片默认5ms
}
}
复制代码
任务的暂停
在shouldYield
函数中有一段,所以可以知道,如果当前时间大于任务开始的时间+yieldInterval
,就打断了任务的进行。
//deadline = currentTime + yieldInterval,deadline是在performWorkUntilDeadline函数中计算出来的
if (currentTime >= deadline) {
//...
return true
}
复制代码
调度优先级
在Scheduler
中有两个函数可以创建具有优先级的任务
function unstable_runWithPriority(priorityLevel, eventHandler) {
switch (priorityLevel) {//5种优先级
case ImmediatePriority:
case UserBlockingPriority:
case NormalPriority:
case LowPriority:
case IdlePriority:
break;
default:
priorityLevel = NormalPriority;
}
var previousPriorityLevel = currentPriorityLevel;//保存当前的优先级
currentPriorityLevel = priorityLevel;//priorityLevel赋值给currentPriorityLevel
try {
return eventHandler();//回调函数
} finally {
currentPriorityLevel = previousPriorityLevel;//还原之前的优先级
}
}
复制代码
scheduleCallback
:以一个优先级注册callback
,在适当的时机执行,因为涉及过期时间的计算,所以scheduleCallback
比runWithPriority
的粒度更细。
在scheduleCallback
中优先级意味着过期时间,优先级越高 priorityLevel 就越小,过期时间离当前时间就越近,var expirationTime = startTime + timeout;
例如IMMEDIATE_PRIORITY_TIMEOUT=-1
,那var expirationTime = startTime + (-1);
就小于当前时间了,所以要立即执行。
scheduleCallback
调度的过程用到了小顶堆,所以我们可以在O(1)
的复杂度找到优先级最高的task
,不了解可以查阅资料,或者查阅我的leetcode算法精讲系列,在源码中小顶堆存放着任务,每次peek
都能取到离过期时间最近的task
。
scheduleCallback
中,未过期任务task
存放在timerQueue
中,过期任务存放在taskQueue
中。
新建newTask
任务之后,判断newTask
是否过期,没过期就加入timerQueue
中,如果此时taskQueue
中还没有过期任务,timerQueue
中离过期时间最近的 task 正好是newTask
,则设置个定时器,到了过期时间就加入taskQueue
中。
当timerQueue
中有任务,就取出最早过期的任务执行。
function unstable_scheduleCallback(priorityLevel, callback, options) {
var currentTime = getCurrentTime();
var startTime;//开始时间
if (typeof options === 'object' && options !== null) {
var delay = options.delay;
if (typeof delay === 'number' && delay > 0) {
startTime = currentTime + delay;
} else {
startTime = currentTime;
}
} else {
startTime = currentTime;
}
var timeout;
switch (priorityLevel) {
case ImmediatePriority://优先级越高timeout越小
timeout = IMMEDIATE_PRIORITY_TIMEOUT;//-1
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT;//250
break;
case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT;
break;
case LowPriority:
timeout = LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT;
break;
}
var expirationTime = startTime + timeout;//优先级越高 过期时间越小
var newTask = {//新建task
id: taskIdCounter++,
callback//回调函数
priorityLevel,
startTime,//开始时间
expirationTime,//过期时间
sortIndex: -1,
};
if (enableProfiling) {
newTask.isQueued = false;
}
if (startTime > currentTime) {//没有过期
newTask.sortIndex = startTime;
push(timerQueue, newTask);//加入timerQueue
//taskQueue中还没有过期任务,timerQueue中离过期时间最近的task正好是newTask
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
if (isHostTimeoutScheduled) {
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
//定时器,到了过期时间就加入taskQueue中
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);//加入taskQueue
if (enableProfiling) {
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);//执行过期的任务
}
}
return newTask;
}
复制代码
视频讲解(高效学习):点击学习
往期 react 源码解析文章:
1.开篇介绍和面试题
2.react的设计理念
3.react源码架构
4.源码目录结构和调试
5.jsx&核心api
6.legacy和concurrent模式入口函数
7.Fiber架构
8.render阶段
9.diff算法
10.commit阶段
11.生命周期
12.状态更新流程
13.hooks源码
14.手写hooks
15.scheduler&Lane
16.concurrent模式
17.context
18事件系统
19.手写迷你版react
20.总结&第一章的面试题解答
评论