React | 2020-06-23 02:00:32 893次 2次
react 的调度,采用 优先级调度(Priority),代码量大且复杂,看了下 fre 中的调度实现(最短剩余时间优先),比较精简且适合快速学习。
问题产生:GUI渲染线程与JS引擎是互斥的,所以需要避免 js 长时间占用导致页面绘制卡顿。
调度核心:频繁发起一个宏任务,根据事件循环机制避免 js 长时间占用(这里需要 fiber 的架构模式)。
代码实现:
const queue = [] // react中为 5ms,fre中为16ms 是多少目前看无所谓 const threshold = 1000 / 60 const callbacks = [] let deadline = 0 // 收集 flushWork 并触发一个宏任务 export const schedule = (cb) => callbacks.push(cb) === 1 && postMessage() // 对外暴露的入口,进行任务收集 export const scheduleWork = (callback, time) => { const job = { callback, time, } queue.push(job) schedule(flushWork) } // 不兼容 MessageChannel 则使用 setTimeout const postMessage = (() => { const cb = () => callbacks.splice(0, callbacks.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 的获取,这里代码展示解答一下:
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 长时间占用导致页面绘制卡顿,其他问题暂不分析。
2人赞