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是可以的。

相关推荐
小小小小宇5 小时前
前端并发控制管理
前端
小小小小宇5 小时前
前端SSE笔记
前端
小小小小宇5 小时前
前端 WebSocket 笔记
前端
小小小小宇6 小时前
前端visibilitychange事件
前端
小小小小宇7 小时前
前端Loader笔记
前端
烛阴8 小时前
从0到1掌握盒子模型:精准控制网页布局的秘诀
前端·javascript·css
前端工作日常11 小时前
我理解的`npm pack` 和 `npm install <local-path>`
前端
李剑一11 小时前
说个多年老前端都不知道的标签正确玩法——q标签
前端
嘉小华11 小时前
大白话讲解 Android屏幕适配相关概念(dp、px 和 dpi)
前端
姑苏洛言11 小时前
在开发跑腿小程序集成地图时,遇到的坑,MapContext.includePoints(Object object)接口无效在组件中使用无效?
前端