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

相关推荐
Wect几秒前
React 性能优化精讲
前端·react.js·性能优化
追风筝的人er34 分钟前
SpringBoot+Vue3 企业考勤如何处理法定假期?节假日方案、调休补班与工作日判断链路拆解
前端·vue.js·后端
无敌的黑星星44 分钟前
Java8 CompletableFuture 实战指南
linux·前端·python
雁鸣零落1 小时前
如何在 Chrome 中查看其他浏览器的书签?书签空间订阅与侧边栏只读切换指南
前端·chrome·edge浏览器
hpoenixf1 小时前
一天上线 + 零返工:我如何给复杂前端需求建立“安全感”
前端
广州华水科技2 小时前
单北斗GNSS变形监测系统在水利工程安全保障中的应用与优势分析
前端
yqcoder2 小时前
CSS 外边距重叠(Margin Collapsing):现象、原理与完美解决方案
前端·css
山楂树の3 小时前
图像标注大坑:img图片 + Canvas 叠加标注,同步放大后标注位置偏移、对不齐?详解修复方案及亚像素处理原理
前端·css·学习·canva可画
本山德彪3 小时前
我做了一个拼豆图纸生成器,把照片秒变图纸
前端
DTrader4 小时前
用TS无法实盘量化? - 实盘均线策略
前端·api