阅读 645

一眼就懂的 React 调度算法

 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​

文章分类
前端
文章标签