requestdleCallback简单实战,以及react中的调度核心原理
1、核心功能介绍
讲到Fiber不得不先提到一个Api那就是requestdleCallback,这个Api是干嘛的那,就是说写在这个函数里的将在浏览器空闲时间被调用,当高优先级任务执行的时候,当前任务就可以被停止,执行高优先级任务。我想从这个api的例子里去带大家去先理解一下空闲时间以及让出主线程,再深入理解react里的调度核心原理。
空闲时间:页面是一帧一帧渲染出来的,1s有60帧代表这个页面的流畅的,每一帧大概是16ms,而如果我们每一帧的执行是小于16ms,就意味着有空闲时间。
Api介绍:
2代表的是函数返回值的id,可以把它传入 Window.cancelIdleCallback() 方法来结束回调。
dieTimeout是一个Boolean类型当它的值为 true 的时候说明 callback 正在被执行。
其中的原型对象上有一个timeRemaining表示的是当前这个周期(帧)的空闲时间剩余多少的时间。
2、简单实战
先看第一段代码和表现,计算会一直占用你的主线程,导致你用户的交互没法去吧背景更改颜色。
紧接着看第二段代码,给他加上requestdleCallback就可以在用户点击的时候把当前计算任务暂停,然后去执行把背景颜色变为绿色的这个操作。
3、源码实现
这里只是描述了一下空闲时间以及让出主线程这个概念,但实际上react中并不是用requestdleCallback去实现调度,具体可以看我写的这篇文章,讲了一下react到底为什么不用requestdleCallback去实现调度。
3.1、源码实现
黄色的是向外抛出的事件,源码是通过MessageChannel来实现消息的发送和接收。
let scheduledHostCallback = null;
let taskTimeoutID = -1;
let yieldInterval = 5;
let deadline = 0;
// TODO: Make this configurable
// TODO: Adjust this based on priority?
const maxYieldInterval = 300;
let needsPaint = false;
if (
enableIsInputPending &&
navigator !== undefined &&
navigator.scheduling !== undefined &&
navigator.scheduling.isInputPending !== undefined
) {
const scheduling = navigator.scheduling;
// 17.02的源码这里比较简单,其实这都不用看直接看false,因为enableIsInputPending为false,就而且17.2里面isInputPending是不可以用的,我们直接看18把,然后我就发现18里面好像依然没解决这个问题
shouldYieldToHost = function() {
const currentTime = getCurrentTime();
if (currentTime >= deadline) {
// 简单的说就是长时间阻塞主线程的东西就给它打断了,绘制(交互)和用户输入之类的东西就给他打断了,都不是我们可以在最大延时里再看看
if (needsPaint || scheduling.isInputPending()) {
// There is either a pending paint or a pending input.
return true;
}
// 300ms最大延时打断
return currentTime >= maxYieldInterval;
} else {
// 不需要打断
return false;
}
};
requestPaint = function() {
needsPaint = true;
};
} else {
// 直接看这里这个deadline是在performWorkUntilDeadline执行任务的时候去设置的
shouldYieldToHost = function() {
return getCurrentTime() >= deadline;
};
// Since we yield every frame regardless, `requestPaint` has no effect.
requestPaint = function() {};
}
// 强制设置检测时间,源码没用到调试的时候可以设置,电脑越好,fps越高分片时间越短
forceFrameRate = function(fps) {
if (fps < 0 || fps > 125) {
// Using console['error'] to evade Babel and ESLint
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 {
// reset the framerate
yieldInterval = 5;
}
};
const performWorkUntilDeadline = () => {
// 有执行任务
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
// 计算一帧的打断检测时间
deadline = currentTime + yieldInterval;
const hasTimeRemaining = true;
try {
// 执行c回调
const hasMoreWork = scheduledHostCallback(
hasTimeRemaining,
currentTime,
);
// 执行完该回调后, 判断后续是否还有其他任务
if (!hasMoreWork) {
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// 还有其他任务, 推进进入下一个宏任务队列中
port.postMessage(null);
}
} catch (error) {
// If a scheduler task throws, exit the current browser task so the
// error can be observed.
port.postMessage(null);
throw error;
}
} else {
isMessageLoopRunning = false;
}
// Yielding to the browser will give it a chance to paint, so we can
// reset this.
// 重置状态
needsPaint = false;
};
const channel = new MessageChannel();
// port2 发送
const port = channel.port2;
// port1 接收
channel.port1.onmessage = performWorkUntilDeadline;
// 在每一帧中执行任务
requestHostCallback = function(callback) {
// 回调注册
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
// 进入宏任务队列
port.postMessage(null);
}
};
// 取消回调
cancelHostCallback = function() {
scheduledHostCallback = null;
};
// 设置超时回调
requestHostTimeout = function(callback, ms) {
taskTimeoutID = setTimeout(() => {
callback(getCurrentTime());
}, ms);
};
// 取消超时
cancelHostTimeout = function() {
clearTimeout(taskTimeoutID);
taskTimeoutID = -1;
};
这段react的核心调度可以分成2部分:
调度:通过MessageChannel建立通道,然后2个端口一个发消息一个接消息,我们抛出发消息的事件,当接到消息的时候我们就可以设置空闲时间deadline,yieldInterval是默认5ms的,同时也执行回调函数。
切片:当我们接受到消息的时候设置了deadline,shouldYieldToHost通过deadline返回一个boolean来决定是否去打断构建,就是实现时间分片的主要函数。
3.2、源码setTimeout的降级实现
当我们在没有dom环境下,我们的react选择的是setTimeout.
if (
// 无dom环境下,感觉可能是因为node环境下MessageChannel用来做线程管理了,就使用一个降级策略用setTimeout去代替MessageChannel
typeof window === 'undefined' ||
// Check if MessageChannel is supported, too.
typeof MessageChannel !== 'function'
) {
// If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore,
// fallback to a naive implementation.
let _callback = null;
let _timeoutID = null;
const _flushCallback = function() {
if (_callback !== null) {
try {
const currentTime = getCurrentTime();
const hasRemainingTime = true;
_callback(hasRemainingTime, currentTime);
_callback = null;
} catch (e) {
setTimeout(_flushCallback, 0);
throw e;
}
}
};
requestHostCallback = function(cb) {
if (_callback !== null) {
// Protect against re-entrancy.
setTimeout(requestHostCallback, 0, cb);
} else {
_callback = cb;
setTimeout(_flushCallback, 0);
}
};
cancelHostCallback = function() {
_callback = null;
};
requestHostTimeout = function(cb, ms) {
_timeoutID = setTimeout(cb, ms);
};
cancelHostTimeout = function() {
clearTimeout(_timeoutID);
};
shouldYieldToHost = function() {
return false;
};
requestPaint = forceFrameRate = function() {};
} else {
// Capture local references to native APIs, in case a polyfill overrides them.
const setTimeout = window.setTimeout;
const clearTimeout = window.clearTimeout;
if (typeof console !== 'undefined') {
// TODO: Scheduler no longer requires these methods to be polyfilled. But
// maybe we want to continue warning if they don't exist, to preserve the
// option to rely on it in the future?
const requestAnimationFrame = window.requestAnimationFrame;
const cancelAnimationFrame = window.cancelAnimationFrame;
if (typeof requestAnimationFrame !== 'function') {
// Using console['error'] to evade Babel and ESLint
console['error'](
"This browser doesn't support requestAnimationFrame. " +
'Make sure that you load a ' +
'polyfill in older browsers. https://reactjs.org/link/react-polyfills',
);
}
if (typeof cancelAnimationFrame !== 'function') {
// Using console['error'] to evade Babel and ESLint
console['error'](
"This browser doesn't support cancelAnimationFrame. " +
'Make sure that you load a ' +
'polyfill in older browsers. https://reactjs.org/link/react-polyfills',
);
}
}
4、总结
调度中心最核心的代码, 在SchedulerHostConfig.default.js中,大家有空可以去看看源码.