封装通用的时间切片函数

372 阅读2分钟

介绍

我们所熟知的时间切片函数有很多,例如:

  • requestAnimationFrame: 在浏览器在下一次重绘之前更新动画。
  • requestIdleCallback: 允许你在浏览器的空闲时段执行低优先级的任务。
  • Promise.then: 放入微队列等待当前事件循环结束后执行回调函数同时清空微队列。
  • setTimeout: 延时一段时间并等待当前事件循环结束后执行回调函数。

存在的问题

它们或多或少都存在一些问题,如下:

  • requestIdleCallback: 浏览器支持程度极其不友好。
  • requestAnimationFrame: 不同浏览器在实现上不一致。
  • Promise.then: 每次事件循环都会把微队列全部清空并不适合时间切片。
  • setTimeout: 嵌套层级超过4层每次延时最少 40 ms。

如何解决

基于上述的问题我们考虑使用 MessageChannel 去创建一个宏任务,如果浏览器不支持则采用回退机制使用 setTimeout创建宏任务。

为什么使用 MessageChannel 而不是 requestAnimationFrame?

因为不同的浏览器在实现 requestAnimationFrame 时可能会有所不同。根据搜索结果,Chrome、Firefox 等浏览器符合标准,会在 style/layout/paint 之前触发 requestAnimationFrame 的回调。但是 IE、Edge、Safari 等浏览器可能会在 style/layout/paint 之后触发回调。这种差异可能导致在不同浏览器中执行相同的动画代码时出现不同的效果。

代码实现:

const targetMap = new Map(); // 存放外界传递的回调函数。
const taskId = {value: 0}; // 每个回调函数需要一个对应的id

let schedulePerformWorkUntilDeadline;

// 根据浏览器支持情况,选择合适的调度策略。
if (typeof MessageChannel !== 'undefined') {
    const channel = new MessageChannel();
    const port = channel.port2;
    channel.port1.onmessage = performWorkUntilDeadline;
    schedulePerformWorkUntilDeadline = (taskId) => {
        port.postMessage(taskId);
    };
} else if (typeof setTimeout === 'function') {
    schedulePerformWorkUntilDeadline = (taskId) => {
        setTimeout(() => performWorkUntilDeadline({data: taskId}), 0);
    }
}

function performWorkUntilDeadline(event) {
    // 在映射中拿到对应的回调函数执行
    const callback = targetMap.get(event.data);
    callback();

    setTimeout(() => schedulePerformWorkUntilDeadline(event.data), 1000 / 16.6);
}

export function useTimeSlice(callback) {
    targetMap.set(taskId.value, callback); // 由于 MessageChannel 的postMessage方法无法发送函数所以需要建立映射关系方便后续找到对应的处理函数。
    schedulePerformWorkUntilDeadline(taskId.value); // 该函数运行后生成宏任务运行回调函数。
    taskId.value++; // 当一切都做完之后在增加任务id,防止多次调用useTimeSlice只能生效一个的问题。
}