react 的调度,采用 优先级调度(Priority),代码量大且复杂,看了下 fre 中的调度实现(最短剩余时间优先),比较精简且适合快速学习。
问题产生:GUI渲染线程与JS引擎是互斥的,所以需要避免 js 长时间占用导致页面绘制卡顿。
调度核心:频繁发起一个宏任务,根据事件循环机制避免 js 长时间占用(这里需要 fiber 的架构模式)。
代码实现:
const queue = []
// react中为 5ms,fre中为16ms 是多少目前看无所谓
const threshold = 1000 / 60
const unit = []
let deadline = 0
// 收集 flushWork 并触发一个宏任务
export const schedule = (cb) => unit.push(cb) === 1 && postMessage()
// 对外暴露的入口,进行任务收集
export const scheduleWork = (callback, time) => {
const job = {
callback,
time,
}
queue.push(job)
schedule(flushWork)
}
// 不兼容 MessageChannel 则使用 setTimeout
const postMessage = (() => {
const cb = () => unit.splice(0, unit.length).forEach((c) => c())
if (typeof MessageChannel !== 'undefined') {
const { port1, port2 } = new MessageChannel()
port1.onmessage = cb
return () => port2.postMessage(null)
}
return () => setTimeout(cb)
})()
// 这里执行传入的任务
const flush = (initTime) => {
let currentTime = initTime
let job = peek(queue)
while (job) {
const timeout = job.time + 3000 <= currentTime
// 超过了 16 ms 立即终止 交还控制权给浏览器一下
if (!timeout && shouldYield()) break
const callback = job.callback
job.callback = null
// 这里的 next 存在则意味着fiber的中断 下段代码进行相关解释
const next = callback(timeout)
if (next) {
job.callback = next
} else {
queue.shift()
}
job = peek(queue)
currentTime = getTime()
}
return !!job
}
// 还有任务一直递归执行
const flushWork = () => {
const currentTime = getTime()
deadline = currentTime + threshold
flush(currentTime) && schedule(flushWork)
}
// 是否过期
export const shouldYield = () => {
return getTime() >= deadline
}
export const getTime = () => performance.now()
// 最短剩余时间优先执行(react根据优先级进行的过期时间排序)
const peek = (queue) => {
queue.sort((a, b) => a.time - b.time)
return queue[0]
}
这是调度的所有逻辑,短小精悍,核心逻辑和 react 中几乎一致。
上面有个问题是 next 的获取,next如何还存在则继续执行next,证明 cpu 拥挤,组件没有渲染完成。如果 next 没有了证明这个任务渲染完成要出队,然后再去取最小时间的任务继续执行。这里代码展示解答一下:
function workLoopConcurrent() {
// 这里会进行fiber的分片,那么中断后如何再继续执行呢?
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
调度入口(react 中的实现):
function ensureRootIsScheduled(){
...
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
...
}
function performConcurrentWorkOnRoot(root, didTimeout){
...
if (root.callbackNode === originalCallbackNode) {
// 这里的返回值就是调度那里的 next
// 这样被中断的 fiber 就可以再继续执行workLoopConcurrent 进入循环和时间分片判断
return performConcurrentWorkOnRoot.bind(null, root);
}
...
}
看下 fre 的精简实现:
export const dispatchUpdate = (fiber?: IFiber) => {
...
scheduleWork(reconcileWork.bind(null, fiber), fiber.time)
...
}
const reconcileWork = (WIP, timeout: boolean): boolean => {
while (WIP && (!shouldYield() || timeout)) WIP = reconcile(WIP)
// 返回自身
if (WIP && !timeout) return reconcileWork.bind(null, WIP)
if (preCommit) commitWork(preCommit)
return null
}
来张图辅助理解:
总之,调度解决的问题就是要避免 js 长时间占用导致页面绘制卡顿,其他问题暂不分析。
更多源码分析请查看
www.gitsu.cn
github.com/yisar/fre
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!