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任务如setTimeout、setInterval等,requestIdleCallback是可以的。

相关推荐
子兮曰5 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰5 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万5 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝5 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋5 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁5 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95275 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大5 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师5 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学5 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端