本文副标题是 Request Schedule 源码解析一
。在本章中会介绍 requestIdleCallback
的用法以及其缺陷, 接着对 React 团队对该 api 的 hack 部分的源码进行剖析。在下一篇中会结合优先级对 React 的调度算法进行宏观的解释, 欢迎关注个人博客 。
React 调度算法
与 requestIdleCallback
这个 api 息息相关。requestIdleCallback
的作用是是在浏览器一帧的剩余空闲时间内执行优先度相对较低的任务, 其用法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var tasksNum = 10000 requestIdleCallback(unImportWork) function unImportWork (deadline ) { while (deadline.timeRemaining() && tasksNum > 0 ) { console .log(`执行了${10000 - tasksNum + 1 } 个任务` ) tasksNum-- } if (tasksNum > 0 ) { requestIdleCallback(unImportWork) } }
deadline
有两个参数
timeRemaining()
: 当前帧还剩下多少时间didTimeout
: 是否超时另外 requestIdleCallback
后如果跟上第二个参数 {timeout: ...}
则会强制浏览器在当前帧执行完后执行。
requestIdleCallback 的缺陷 requestIdleCallback is called only 20 times per second - Chrome on my 6x2 core Linux machine, it’s not really useful for UI work。—— from Releasing Suspense
也就是说 requestIdleCallback
的 FPS 只有 20
, 这远远低于页面流畅度的要求!(一般 FPS 为 60 时对用户来说是感觉流程的, 即一帧时间为 16.7 ms), 这也是 React 需要自己实现 requestIdleCallback
的原因。
源码解析之 requestIdleCallback 非 DOM 环境 在不能操作 DOM 的环境下, 可以借助 setTimeout
来模拟 requestIdleCallback
的实现。
1 2 3 4 5 6 7 requestIdleCallback = (callback ) => { setTimeout(callback({ timeRemaining() { return Infinity } })) }
下面将 React 源码中关于服务端的实现也呈现出来:
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 let _callback = null ;const _flushCallback = function (didTimeout ) { if (_callback !== null ) { try { _callback(didTimeout); } finally { _callback = null ; } } }; requestHostCallback = function (cb ) { if (_callback !== null ) { setTimeout(requestHostCallback, 0 , cb); } else { _callback = cb; setTimeout(_flushCallback, 0 , false ); } }; cancelHostCallback = function ( ) { _callback = null ; }; shouldYieldToHost = function ( ) { return false ; };
DOM 环境 在浏览器端的环境下, 介绍一个与 requestIdleCallback
功能相近的 api —— requestAnimationFrame(callback)
, 其会在下次重绘前执行指定的回调函数,因此这个 api 在动效领域得到了广泛的使用。下面通过一个简单的 demo 来认识它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 let framelet n = 5 function callback (timeStamp ) { console .log(timeStamp) while (n > 0 ) { requestAnimationFrame(callback) console .log('测试执行顺序' ) n-- } } frame = requestAnimationFrame(callback)
执行上述代码, 控制台(chrome)打印如下数据:
1 2 3 4 5 6 7 先输出 5 次 '测试执行顺序' 1795953.649 1795970.318 1795986.987 1796003.656 1796020.325 ...
可以看到在浏览器上一帧的时间大致为 16ms
。同时可以看到 requestAnimation(callback)
中的 callback 也是异步的(只不过它是基于帧与帧间的异步), 所以上述打印结果是先打印出 5 次 ‘测试执行顺序’ 后再依次打印出 5 个时间戳。
requestHostCallback
(也就是 requestIdleCallback) 这部分源码的实现比较复杂, 可以将其分解为以下几个重要的步骤(有一些细节点可以看注释):
步骤一: 如果有优先级更高的任务, 则通过 postMessage
触发步骤四, 否则如果 requestAnimationFrame
在当前帧没有安排任务, 则开始一个帧的流程; 步骤二: 在一个帧的流程中调用 requestAnimationFrameWithTimeout
函数, 该函数调用了 requestAnimationFrame
, 并对执行时间超过 100ms
的任务用 setTimeout
放到下一个事件队列中处理; 步骤三: 执行 requestAnimationFrame
中的回调函数 animationTick
, 在该回调函数中得到当前帧的截止时间 frameDeadline
, 并通过 postMessage
触发步骤四; 步骤四: 通过 onmessage
接受 postMessage
指令, 触发消息事件的执行。在 onmessage
函数中根据 frameDeadline - currentTime <= 0
判断任务是否可以在当前帧执行,如果可以的话执行该任务, 否则进入下一帧的调用。 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 124 125 126 127 128 129 export let requestHostCallback;export let cancelHostCallback;export let shouldYieldToHost;export let getCurrentTime;const ANIMATION_FRAME_TIMEOUT = 100 ;let rAFID;let rAFTimeoutID;const requestAnimationFrameWithTimeout = function (callback ) { rAFID = requestAnimationFrame(function (timestamp ) { clearTimeout(rAFTimeoutID); callback(timestamp); }); rAFTimeoutID = setTimeout(function ( ) { cancelAnimationFrame(rAFID); callback(getCurrentTime()); }, ANIMATION_FRAME_TIMEOUT); }; getCurrentTime = function ( ) { return performance.now(); }; let scheduledHostCallback = null ; let isMessageEventScheduled = false ; let timeoutTime = -1 ;let isAnimationFrameScheduled = false ;let isFlushingHostCallback = false ;let frameDeadline = 0 ; let previousFrameTime = 33 ; let activeFrameTime = 33 ;shouldYieldToHost = function ( ) { return frameDeadline <= getCurrentTime(); }; const channel = new MessageChannel();const port = channel.port2;channel.port1.onmessage = function (event ) { isMessageEventScheduled = false ; const prevScheduledCallback = scheduledHostCallback; const prevTimeoutTime = timeoutTime; scheduledHostCallback = null ; timeoutTime = -1 ; const currentTime = getCurrentTime(); let didTimeout = false ; if (frameDeadline - currentTime <= 0 ) { if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) { didTimeout = true ; } else { if (!isAnimationFrameScheduled) { isAnimationFrameScheduled = true ; requestAnimationFrameWithTimeout(animationTick); } scheduledHostCallback = prevScheduledCallback; timeoutTime = prevTimeoutTime; return ; } } if (prevScheduledCallback !== null ) { isFlushingHostCallback = true ; try { prevScheduledCallback(didTimeout); } finally { isFlushingHostCallback = false ; } } }; const animationTick = function (rafTime ) { if (scheduledHostCallback !== null ) { requestAnimationFrameWithTimeout(animationTick); } else { isAnimationFrameScheduled = false ; return ; } let nextFrameTime = rafTime - frameDeadline + activeFrameTime; if (nextFrameTime < activeFrameTime && previousFrameTime < activeFrameTime) { activeFrameTime = nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime; } else { previousFrameTime = nextFrameTime; } frameDeadline = rafTime + activeFrameTime; if (!isMessageEventScheduled) { isMessageEventScheduled = true ; port.postMessage(undefined ); } }; requestHostCallback = function (callback, absoluteTimeout ) { scheduledHostCallback = callback; timeoutTime = absoluteTimeout; if (isFlushingHostCallback || absoluteTimeout < 0 ) { port.postMessage(undefined ); } else if (!isAnimationFrameScheduled) { isAnimationFrameScheduled = true ; requestAnimationFrameWithTimeout(animationTick); } }; cancelHostCallback = function ( ) { scheduledHostCallback = null ; isMessageEventScheduled = false ; timeoutTime = -1 ; };
相关资料