ReactReactrequestIdleCallback
左杰requestIdleCallback 44444 41241
它提供了一种机制,允许开发者在浏览器空闲时运行低优先级的任务,而不会影响关键任务和动画的性能。
requestIdleCallback 执行阶段
浏览器一针里面做的任务
- 处理事件的回调: 用户的点击 input
- 处理计时器的回调,event loop
- 开始渲染 begin帧
- 执行requestAnimationFrame 动画回调
- 计算机页面布局计算 合并到主线程
- 绘制 回流和重绘
- 如果此时还有空闲时间,执行requestIdleCallback (这个是有条件的!)
requestIdleCallback 基本用法
requestIdleCallback 接受一个回调函数 callback 并且在回调函数中会注入参数 deadline
deadline有两个值:
● deadline.timeRemaining() 返回是否还有空闲时间(毫秒)
● deadline.didTimeout 返回是否因为超时被强制执行(布尔值)
options:
● { timeout: 1000 } 指定回调的最大等待时间(以毫秒为单位)。如果在指定的 timeout 时间内没有空闲时间,回调会强制执行,避免任务无限期推迟
这个案例模拟了在浏览器空闲时,渲染1000条dom元素,非常流畅
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const nums = 30000; const arr = [];
function generateDom() { for (let i = 0; i < nums; i++) { arr.push(function() { document.body.innerHTML += `<div>${i + 1}</div>`; }); } } generateDom();
function workLoop(deadline) { if (deadline.timeRemaining() > 1 && arr.length > 0) { const fn = arr.shift(); fn(); } requestIdleCallback(workLoop); }
requestIdleCallback(workLoop,{ timeout: 1000});
|
常考的面试题
为什么React不用原生requestIdleCallback实现?
- 兼容性差 Safari 并不支持 https://caniuse.com
- 控制精细度 React 要根据组件优先级、更新的紧急程度等信息,更精确地安排渲染的工作
- 执行时机requestIdleCallback(callback) 回调函数的执行间隔是 50ms(W3C规定),也就是 20FPS,1秒内执行20次,间隔较长。
- 差异性 每个浏览器实现该API的方式不同,导致执行时机有差异有的快有的慢

requestIdleCallback的替代方案?
MessageChannel
选择 MessageChannel 的原因,是首先异步得是个宏任务,因为宏任务中会在下次事件循环中执行,不会阻塞当前页面的更新。MessageChannel 是一个宏任务。
没选常见的 setTimeout,是因为MessageChannel 能较快执行,在 0~1ms 内触发,像 setTimeout 即便设置 timeout 为 0 还是需要 4~5ms。相同时间下,MessageChannel 能够完成更多的任务。

MDN
若浏览器不支持 MessageChannel,还是得降级为 setTimeout。
MessageChannel基本用法
MessageChanne设计初衷是为了方便 我们在不同的上下文之间进行通讯,例如web Worker,iframe 它提供了两个端口(port1 和 port2),通过这些端口,消息可以在两个独立的线程之间双向传递
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const channel = new MessageChannel(); const port1 = channel.port1; const port2 = channel.port2;
port1.onmessage = (event) => { console.log('Received by port1:', event.data); port1.postMessage('Reply from port1'); };
port2.onmessage = (event) => { console.log('Received by port2:', event.data); };
port2.postMessage('Message from port2');
|
实现react简易版调度器
React调度器给每一个任务分配了优先级
- ImmediatePriority : 立即执行的优先级,级别最高
- UserBlockingPriority : 用户阻塞级别的优先级
- NormalPriority : 正常的优先级
- LowPriority : 低优先级
- IdlePriority : 最低阶的优先级
同时还给每个任务设置了过期时间,过期时间越短,优先级越高
taskQueue 为数组,存储每个任务的信息,包括优先级,过期时间,回调函数
isPerformingWork 为布尔值,表示当前是否在执行任务
port 为MessageChannel,用于发送和接收消息
然后将任务添加到队列里面,并且添加进去的时候还需要根据优先级进行排序,然后调用workLoop执行任务
对应的react源码地址:
在 React 源码中有目录 packages/scheduler,该目录就是 React 任务调度模块相关。
该目录 src 下
Scheduler.js 实现了任务调度相关逻辑
SchedulerMinHeap.js 实现了堆
React 会有一系列规则定义每个任务的优先级,最后的表现就是 React 会将每个任务包装为一个任务对象 newTask 319行,该对象会存在两个属性 id 和 sortIndex,其中 sortIndex 标识当前任务的优先级, id 标识每个任务的先后顺序。
React 中存放任务的数组 taskQueue 会被模拟为一个小顶堆,该小顶堆的 compare 逻辑是优先比较 sortIndex(即任务的优先级),如果 sortIndex 相同,则比较 id(即任务创建的先后顺序)。
因为堆的性质是维护一个最值在堆顶,所以每次堆顶任务(对应任务数组中的第一个元素)就是当前任务队列中优先级最高的任务,这样只需要每次获取堆顶任务执行即可。
堆顶任务取出之后,只需要对小顶堆进行弹堆操作后自上向下的平衡调整,则堆顶又维护了当前任务队列中优先级最高的任务。
这样通过堆维护任务队列,每次获取优先级最高的任务的时间复杂度是 O(1) 的,插入和弹出操作后的平衡调整时间复杂度是 O(logn)的,整体是非常高效的。
对应的源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
| const ImmediatePriority = 1; const UserBlockingPriority = 2; const NormalPriority = 3; const LowPriority = 4; const IdlePriority = 5;
function getCurrentTime() { return performance.now(); }
class SimpleScheduler { constructor() { this.taskQueue = []; this.isPerformingWork = false;
const channel = new MessageChannel(); this.port = channel.port2; channel.port1.onmessage = this.performWorkUntilDeadline.bind(this); }
scheduleCallback(priorityLevel, callback) { const curTime = getCurrentTime(); let timeout; switch (priorityLevel) { case ImmediatePriority: timeout = -1; break; case UserBlockingPriority: timeout = 250; break; case LowPriority: timeout = 10000; break; case IdlePriority: timeout = 1073741823; break; case NormalPriority: default: timeout = 5000; break; }
const task = { callback, priorityLevel, expirationTime: curTime + timeout };
this.push(this.taskQueue, task); this.schedulePerformWorkUntilDeadline(); }
schedulePerformWorkUntilDeadline() { if (!this.isPerformingWork) { this.isPerformingWork = true; this.port.postMessage(null); } }
performWorkUntilDeadline() { this.isPerformingWork = true; this.workLoop(); this.isPerformingWork = false; }
workLoop() { let curTask = this.peek(this.taskQueue); while (curTask) { const callback = curTask.callback; if (typeof callback === 'function') { callback(); } this.pop(this.taskQueue); curTask = this.peek(this.taskQueue); } }
peek(queue) { return queue[0] || null; }
push(queue, task) { queue.push(task); queue.sort((a, b) => a.expirationTime - b.expirationTime); }
pop(queue) { return queue.shift(); } }
const scheduler = new SimpleScheduler();
scheduler.scheduleCallback(LowPriority, () => { console.log('Task 1: Low Priority'); });
scheduler.scheduleCallback(ImmediatePriority, () => { console.log('Task 2: Immediate Priority'); });
scheduler.scheduleCallback(IdlePriority, () => { console.log('Task 3: Idle Priority'); });
scheduler.scheduleCallback(UserBlockingPriority, () => { console.log('Task 4: User Blocking Priority'); });
scheduler.scheduleCallback(NormalPriority, () => { console.log('Task 5: Normal Priority'); });
|
执行顺序为 2 4 5 1 3