React的scheduleCallback最简单实现

任务调度中requestIdleCallback的不足

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

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

浏览器执行以下代码:

js 复制代码
    // 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实现的点击结果的频率是更为平整的:

两种scheduleCallback的实现方式

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

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

js 复制代码
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是可以的。

相关推荐
七月的冰红茶7 分钟前
【threejs】第一人称视角之八叉树碰撞检测
前端·threejs
爱掉发的小李23 分钟前
前端开发中的输出问题
开发语言·前端·javascript
祝余呀1 小时前
HTML初学者第四天
前端·html
浮桥2 小时前
vue3实现pdf文件预览 - vue-pdf-embed
前端·vue.js·pdf
七夜zippoe2 小时前
前端开发中的难题及解决方案
前端·问题
Hockor4 小时前
用 Kimi K2 写前端是一种什么体验?还支持 Claude Code 接入?
前端
杨进军4 小时前
React 实现 useMemo
前端·react.js·前端框架
海底火旺4 小时前
浏览器渲染全过程解析
前端·javascript·浏览器
你听得到114 小时前
揭秘Flutter图片编辑器核心技术:从状态驱动架构到高保真图像处理
android·前端·flutter
驴肉板烧凤梨牛肉堡4 小时前
浏览器是否支持webp图像的判断
前端