优化背景:
在react16 版本之前,react 的更新渲染存在着一些性能的问题,这个问题产生的原因是因为在更新/渲染时,采用递归的方式(栈调度器),而递归有一个不好的地方就是,他不能被随意的break;也不能在break之后也很难恢复到break之前的状态,而因为浏览器是单线程的,V8 引擎执行js 和 渲染引擎渲染页面是两个互斥的行为,这也就是说当更新任务太庞大的时候,js执行太久而致使页面丢帧,不流畅,甚至卡顿。
优化方式
react16 之后,react团队在react中加入了一个全新的架构:Fiber,并赋予了他光荣而艰巨的使命----解决16版本之前的性能问题;
Fiber 架构实际上是个链表结构,每个节点包含了 return,subling,children。他们分别代表父节点,兄弟节点,子节点。有了链表结构,那就可以进行DFS,而且也可以很轻松的break & continue;
当然,上述都是题外话,今天主要分析react 中的 scheduler。
今天的正题:scheduler
个人拙见,如有问题希望大佬指正。知也无涯,共勉!
今天主要分析的源码的就是,react 中的任务调度器。在16.8版本之后,scheduler已经被react 团队从react-dom中抽离了出来,作为一个独立的包单独发布到npm上。
if (expirationTime === Sync) {
callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
callbackNode = scheduleCallback(priorityLevel, performConcurrentWorkOnRoot.bind(null, root),
{
timeout: expirationTimeToMs(expirationTime) - now()
});
}
这段代码位于react-dom 的源码中,说明了,其中的expirationTime代表了过期时间,当为Sync的时候继续沿用react16版本之前的更新,否则就会采用全新的调度规则。
而其中的performConcurrentWorkOnRoot 就是一个即将执行的任务,它被传入scheduler中,进行调度处理。
unstable_scheduleCallback
这是schedule中抛出的一个核心方法,该方法主要执行了以下几个操作
1: 声明一个信的Task对象属性如下
var newTask = {
id: taskIdCounter++,
callback: callback, // 对应上一个代码片段的 performConcurrentWorkOnRoot方法
priorityLevel: priorityLevel, //优先级
startTime: startTime, //开始时间
expirationTime: expirationTime, //过期时间
sortIndex: -1 //初始值是 -1 后续会根据是否delay 而被赋予不同的值,如果delay = starttime 反之 expirationTime
};
2: 执行代码
if (startTime > currentTime) {
newTask.sortIndex = startTime;
push(timerQueue, newTask);
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
if (isHostTimeoutScheduled) {
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
{
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
以上代码主要做的事情就是,首先判断是否是delay的任务,如果是,就执行requestHostTimeout,如果不是就执行requestHostCallback,当然中间还处理了taskQuene 和timerQuene。这两个队列顾名思义,taskQuene是任务队列,timerQuene就是为了后续让,被delay的任务能在delay ms 之后执行的一个依据 (通过 advanceTimers 方法 advanceTimers讲队列按时常排序)
首先先看正常任务的执行流程:
先将newTask 的isQueued 置为true 然后执行requestHostCallback函数,并将flushWork作为参数传进去,接下来看requestHostCallback代码
requestHostCallback = function (cb) {
if (_callback !== null) {
// Protect against re-entrancy.
setTimeout(requestHostCallback, 0, cb);
} else {
_callback = cb;
setTimeout(_flushCallback, 0);
}
};
可以看到,其实就是一个递归,反复执行requestHostCallback,并传入flushWork,其实可以看到,requestHostCallback其实目的就是了反复修改一个全局变量 _callback,并执行 _flushCallback, 它们有什么意义呢?我们一会再看
接下来在看,如果是delay的任务,则会执行,requestHostTimeout,代码如下
requestHostTimeout = function (cb, ms) {
_timeoutID = setTimeout(cb, ms);
};
它实际上就是将handleTImeout 方法延迟delay ms之后在执行,并声称一个 _timeoutID 方便后续使用 cancelHostTimeout() clear掉,handleTimeout 代码如下
function handleTimeout(currentTime) {
isHostTimeoutScheduled = false;
advanceTimers(currentTime);
if (!isHostCallbackScheduled) {
if (peek(taskQueue) !== null) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
} else {
var firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
}
}
}
所以很显然, 正常任务 和 delay 任务的执行差别就是,正常任务会立刻执行requestHostCallback 而 delay 的则会在delay ms 之后通过setTimeOut来执行requestHostCallback /很像是废话,但就是如此,,,/
上面说到,requestHostCallback 其实就是在维护一个全局变量 _callback 以及调用_flushCallback 方法,_flushCallback 方法定义如下
var _flushCallback = function () { //简化后的去掉了 异常捕捉处理
if (_callback !== null) {
var currentTime = exports.unstable_now();
var hasRemainingTime = true;
_callback(hasRemainingTime, currentTime);
_callback = null;
}
};
由此可见,它其实就是执行了 _callback 并传入了两个参数,hasRemainingTime, currentTime,而这个_callback 变量则是 刚刚传入的实参 flushWork,所以我们来看 flushWork 的定义
function flushWork(hasTimeRemaining, initialTime) {
isHostCallbackScheduled = false;
if (isHostTimeoutScheduled) { //如果在执行 delay的任务,cancel掉
// We scheduled a timeout but it's no longer needed. Cancel it.
isHostTimeoutScheduled = false;
cancelHostTimeout();
}
isPerformingWork = true;
var previousPriorityLevel = currentPriorityLevel;
try {
if (enableProfiling) { //这个变量一直是true ,所以其他代码就忽略掉了
try {
return workLoop(hasTimeRemaining, initialTime); //如果workLoop 正常,执行,否则走catch
} catch (error) { //这里面实际上
if (currentTask !== null) {
var currentTime = exports.unstable_now();
markTaskErrored(currentTask, currentTime);
currentTask.isQueued = false;
}
throw error;
}
}
} finally {
//这里面简单说一下,就是执行workloop 之后将一些变量置为初始值
}
这里面很明显,如果workLoop 不抛出异常,执行完毕之后就会将变量置为初始值,否则,会将当前的currentTask.isQueued 置为false,所以我们还是要继续看 workLoop这个方法的功能是什么;
function workLoop(hasTimeRemaining, initialTime) {
var currentTime = initialTime;
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (currentTask !== null && !(enableSchedulerDebugging )) {
if (currentTask.expirationTime > currentTime && (!hasTimeRemaining || shouldYieldToHost())) {
break;
}
var callback = currentTask.callback;
if (callback !== null) {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
var didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
markTaskRun(currentTask, currentTime);
var continuationCallback = callback(didUserCallbackTimeout);
currentTime = exports.unstable_now();
if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback;
markTaskYield(currentTask, currentTime);
} else {
{
markTaskCompleted(currentTask, currentTime);
currentTask.isQueued = false;
}
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
advanceTimers(currentTime);
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
if (currentTask !== null) {
return true;
} else {
var firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
首先,先从task队列中拿出最先插入的任务,而while 的作用就是,将整个task队列里每一个的 task 的 callback,而其中的
var continuationCallback = callback(didUserCallbackTimeout);
if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback;
}
则说明了,会反复的执行 callback 也就是,文章开始提到的 react-dom里传入的实参任务 performConcurrentWorkOnRoot.bind(null, root);期间并调整timer 队列的顺序。至此,代码的走向又指向了 react-dom 里的 performConcurrentWorkOnRoot 方法。scheduler 的任务也就完成了。
总结一下,它做了什么:
1: 声明一个task任务,其中包含了id,callback,starttime,exprationtime,priorityLevel,sortIndex,isQueued这七个属性,
2: 声明两个队列结构的变量,taskQuene ,timerQuene,其中的taskQuene就是正常的任务,而timerQuene就是需要延缓执行的任务。
3: 遍历taskQuene 中的每个task,并且执行完毕所有的callback,其中callback属性是会递归覆盖的,所以会清空performConcurrentWorkOnRoot 被绑定的 root 里的任务,
而这个 root 实际上就是 最顶层fiberRoot下的 fiber.stateNode;这里就暂不赘述