React的scheduleCallback最简单实现

任务调度中requestIdleCallback的不足

scheduleCallback 实现了按时间切片的任务调度, 浏览器自带的API requestIdleCallback能达到时间切片的效果,但react最终未采用,主要由于以下原因

  1. 部分浏览器不支持,如Safar、andriod@40以下的webview等
  2. 精确度不足,浏览器的渲染和事件行为有可能导致现有任务的执行卡顿,即任务有可能被断断续续的打断

浏览器执行以下代码:

    // item元素为蓝色, item11元素为红色
    window.onload = () => {
      document.body.onclick = () => {
        const d = document.createElement('div')
        d.className = 'item11'
        root.append(d)
      }
      const root = document.getElementById('root')

      for (let i = 0; i < 500 * 100; i++) {
        let a = i + 1
        requestIdleCallback(() => {
          const d = document.createElement('div')
          d.className = 'item'
          root.append(d)
          const arr = []
          for (let a = 0; a < 20 * 200; a++) {
            const arr2 = []
            for (let b = 0; b < 10 * 10; b++) {
              arr2.push(b)
            }
            arr.push(arr2)
          }
        })
      }

初始化页面的时候快速连续点击页面得到下面结果:

React的scheduleCallback最简单实现

可以看到点击生成的元素是离散分布的,而按react的scheduleCallback实现的点击结果的频率是更为平整的:
React的scheduleCallback最简单实现

两种scheduleCallback的实现方式

离开了原生的requestIdleCallback,还能想到什么方式去实现将控制权转交给浏览器?
时间切片实时记录当前任务的开始时间,切片时间用完则停止任务,通过异步下一次任务来把控制权转交给浏览器。

实现方式MessageChannel / setTimeout / setImmediate+ while + 递归, 实现requestDDCallback,即下一批次任务执行的再触发,以下实现未实现任务优先级调度、延时任务调度、手动暂停任务等功能;

const t = []

let getCurrentTime
let isWorking
let startTime

let frameInterval = 5
const hasPerformanceNow =
  // $FlowFixMe[method-unbinding]
  typeof performance === 'object' && typeof performance.now === 'function';

if (hasPerformanceNow) {
  const localPerformance = performance;
  getCurrentTime = () => localPerformance.now();
} else {
  const localDate = Date;
  const initialTime = localDate.now();
  getCurrentTime = () => localDate.now() - initialTime;
}
// todo 兼容性判断使用`MessageChannel` / `setTimeout` / `setImmediate`的哪一种
function requestDDCallback(callback) {
  const mess = new MessageChannel()
  mess.port1.onmessage = callback
  mess.port2.postMessage(null)
}

function requestHostCallback() {
  if (!isWorking) {
    requestDDCallback(startUnitWork)
  }
}

function startUnitWork() {
    const hasMore = unitWork()
  if (hasMore) {
    startTime = getCurrentTime()
    requestHostCallback()
  }
}
// timeout来设置任务的过期时间,react中timeout越大优先级越低
function schedule(callback, timeout = -1, hightLevel = false) {
  const startTime_ = getCurrentTime()
  const work = {
    startTime: startTime_,
    callback,
    exprationTime: startTime_ + timeout
  }
  
  if (!hightLevel) {
    t.push(work)
  } else {
    t.unshift(work)
  }
  startTime = getCurrentTime()
  requestHostCallback()
}

function shouldYield() {
  if (getCurrentTime() - startTime < frameInterval) {
    return false
  } else return true
}

function  unitWork() {
  let ct = t[0]
  isWorking = true
  while (ct) {
    if (shouldYield() && ct.exprationTime ) {
      break
    }
    ct.callback()
    t.shift()
    ct = t[0]
  }
  isWorking = false
  let hasMore = t.length !== 0
  return hasMore
}

window.schedule = schedule

对比requestIdleCallback,手动实现的scheduleCallback也存在不足,由于js线程是单线程执行,scheduleCallback无法将任务转给其他的异步插入的js任务如setTimeoutsetInterval等,requestIdleCallback是可以的。

原文链接:https://juejin.cn/post/7332402033281482802 作者:空镜

(0)
上一篇 2024年2月7日 下午4:26
下一篇 2024年2月7日 下午4:36

相关推荐

发表回复

登录后才能评论