一眼就懂的 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
}

 来张图辅助理解:

react调度

总之,调度解决的问题就是要避免 js 长时间占用导致页面绘制卡顿,其他问题暂不分析。

更多源码分析请查看:

React源码分析 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值