React源码分析=; scheduler分析
Posted 刘翾
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React源码分析=; scheduler分析相关的知识,希望对你有一定的参考价值。
文章目录
- 1. 前言
- 2. getCurrentTime
- 3. unstable_scheduleCallback函数
- 4. scheduleHostCallbackIfNeeded
- 5. requestHostCallback
- 6. 总结
1. 前言
为了读代码更加有效率,提前看了一篇如何阅读源码的文章:https://zxc0328.github.io/2018/05/01/react-source-reading-howto/
因此此次本人阅读源码主要想看懂以下6个问题:
- ReactDOM.render()是如何挂载到真实DOM上的
- setState实现原理,为什么是异步的
- 生命周期结合2号问题一起看
- react16的fiber架构是什么
- jsx是如何解析的
- react hook是如何做到的
分析代码基于React V16.8.6,react源码目录截图如下图所示:
所需要看的代码一个库就够了,https://github.com/facebook/react/
先看的react-dom代码, 一点点单步到了scheduler
, 这个包的代码看起来不多(可能是我第一次看框架源码, 看的有点恶心… 一堆全局变量, 各个函数来回调用, 看了好几天, 如果下面哪里有问题, 还请各位同行指点), 那就先来梳理下这个包吧.
scheduler
这个包主要是在react做diff做任务分配机制, 核心机制类似于requestidlecallback,
window.requestIdleCallback()
会在浏览器空闲时期依次调用函数, 这就可以让开发者在主事件循环中执行后台或低优先级的任务,而且不会对像动画和用户交互这样延迟敏感的事件产生影响。
但这个函数支持度太惨
react则使用requestAnimationFrame
和postMessage
来模拟实现的requestidlecallback
. 工作原理是调度requestAnimationFrame
,存储帧开始的时间,然后调度postMessage
,后者在绘制后进行调度。
该包主要流程是把所有任务通过双向链表连接起来, 通过requestAnimationFrame
来在浏览器每帧的空闲时间循环处理所有任务, 直到链表为空为止.
2. getCurrentTime
这个函数后面会经常用到的, 先到前面来说下, 先看代码:
// packages\\scheduler\\src\\forks\\SchedulerHostConfig.default.js
const hasNativePerformanceNow =
typeof performance === 'object' && typeof performance.now === 'function';
const localDate = Date;
if (hasNativePerformanceNow)
const Performance = performance;
getCurrentTime = function()
return Performance.now();
;
else
getCurrentTime = function()
// 该方法在 ECMA-262 第五版中被标准化, Date.now() === new Date().getTime();
// 出处 https://developer.mozilla.org/zh-CN/docs/Web/javascript/Reference/Global_Objects/Date/now#Compatibility
return localDate.now();
;
performance.now()
与 Date.now()
不同的是,返回了以微秒(百万分之一秒)为单位的时间,更加精准。
并且与 Date.now()
会受系统程序执行阻塞的影响不同,performance.now()
的时间是以恒定速率递增的,不受系统时间的影响(系统时间可被人为或软件调整)。
注意Date.now()
输出的是 UNIX 时间,即距离 1970 的时间,而performance.now()
输出的是相对于 time origin
(页面初始化: https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#The_time_origin) 的时间。
使用 Date.now()
的差值并非绝对精确,因为计算时间时受系统限制(可能阻塞)。但使用 performance.now() 的差值,并不影响我们计算程序执行的精确时间。
3. unstable_scheduleCallback函数
- 函数前面的unstable表示不稳定的意思, 之后还会有变动.
- 这个方法主要就是将任务组成双向链表, 并按照过期时间作为优先级.
先定义优先级, 代码如下:
// packages\\scheduler\\src\\Scheduler.js
// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// 这是32位系统V8引擎里最大的整数
// Math.pow(2, 30) - 1
// 0b111111111111111111111111111111
var maxSigned31BitInt = 1073741823;
// Times out immediately 立即过期
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY = maxSigned31BitInt;
主函数代码如下:
// packages\\scheduler\\src\\Scheduler.js
// 组成双向链表, 开始安排任务
function unstable_scheduleCallback(priorityLevel, callback, deprecated_options)
var startTime =
currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();
// 过期时间 = 加入时间 + 优先级时间
var expirationTime;
if (
typeof deprecated_options === 'object' &&
deprecated_options !== null &&
typeof deprecated_options.timeout === 'number'
)
expirationTime = startTime + deprecated_options.timeout;
else
// 根据不同的优先级, 赋予不同的过期时间
switch (priorityLevel)
case ImmediatePriority:
expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
expirationTime = startTime + USER_BLOCKING_PRIORITY;
break;
case IdlePriority:
expirationTime = startTime + IDLE_PRIORITY;
break;
case LowPriority:
expirationTime = startTime + LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
// 未完
上面先计算一下callback的过期时间, 接下来创建链表节点, 并组成链表, 代码如下:
// packages\\scheduler\\src\\Scheduler.js
// 续上
// 基于上面的优先级和过期时间创建一个节点
var newNode =
callback,
priorityLevel: priorityLevel,
expirationTime,
next: null,
previous: null,
;
if (firstCallbackNode === null)
// This is the first callback in the list. 如果firstCallbackNode没有, 说明是第一个节点
firstCallbackNode = newNode.next = newNode.previous = newNode;
scheduleHostCallbackIfNeeded(); // 之后再说, 先忽略
else
var next = null;
var node = firstCallbackNode;
do
if (node.expirationTime > expirationTime)
// The new callback expires before this one.
next = node;
break;
node = node.next;
while (node !== firstCallbackNode);
if (next === null)
// No callback with a later expiration was found, which means the new
// callback has the latest expiration in the list.
// 列表中新插入的节点具有最大的到期时间, 插入最后
next = firstCallbackNode;
else if (next === firstCallbackNode)
// The new callback has the earliest expiration in the entire list.
// 新插入的节点具有最小的到期时间, 插在最前面
firstCallbackNode = newNode;
scheduleHostCallbackIfNeeded();
var previous = next.previous;
previous.next = next.previous = newNode;
newNode.next = next;
newNode.previous = previous;
return newNode;
这部分当初第一次看的时候也看了很久, 看明白之后才发现原来创建双向链表居然这么简单, 就是有点绕, 双向链表的定义:
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。
https://baike.baidu.com/item/双向链表/2968731?fr=aladdin
这部分创建初始双链表其实就只有用了一句话firstCallbackNode = newNode.next = newNode.previous = newNode;
, 从右向左赋值, 也就是说这段代码中用的变量全都指向newNode
这个对象的地址.
4. scheduleHostCallbackIfNeeded
// packages\\scheduler\\src\\Scheduler.js
// This is set while performing work, to prevent re-entrancy.
var isPerformingWork = false;
var isHostCallbackScheduled = false;
function scheduleHostCallbackIfNeeded()
if (isPerformingWork)
// Don't schedule work yet; wait until the next time we yield.
// 有个work正在执行
return;
if (firstCallbackNode !== null)
// Schedule the host callback using the earliest expiration in the list.
// firstCallbackNode的过期时间是最早的
var expirationTime = firstCallbackNode.expirationTime;
if (isHostCallbackScheduled)
// Cancel the existing host callback.
// 取消存在的回调函数
cancelHostCallback();
else
isHostCallbackScheduled = true;
requestHostCallback(flushWork, expirationTime);
requestHostCallback
这个函数内部主要是通过本文开头所讲的requestAnimationFrame
和postMessage
来模拟实现的requestidlecallback
调度任务执行这个flushWork
, 那么我们先来看下这个flushWork
.
4.1. flushWork
这个函数的参数didUserCallbackTimeout
只会有两种情况:
- 当前帧没过期;
didUserCallbackTimeout = false
- 当前帧过期且当前任务过期
didUserCallbackTimeout = true
// packages\\scheduler\\src\\Scheduler.js
function flushWork(didUserCallbackTimeout)
// Exit right away if we're currently paused
if (enableSchedulerDebugging && isSchedulerPaused)
return;
// We'll need a new host callback the next time work is scheduled.
// 安排callback完成了
isHostCallbackScheduled = false;
isPerformingWork = true;
const previousDidTimeout = currentHostCallbackDidTimeout;
currentHostCallbackDidTimeout = didUserCallbackTimeout;
try
if (didUserCallbackTimeout)
// 当前帧过期且当前任务过期
// Flush all the expired callbacks without yielding.
while (
firstCallbackNode !== null &&
!(enableSchedulerDebugging && isSchedulerPaused)
)
// TODO Wrap in feature flag
// Read the current time. Flush all the callbacks that expire at or
// earlier than that time. Then read the current time again and repeat.
// This optimizes for as few performance.now calls as possible.
// 读取当前时间, 刷新在该时间之前过期的所有回调
var currentTime = getCurrentTime();
if (firstCallbackNode.expirationTime <= currentTime)
do
flushFirstCallback(); // 见4.2节
while (
firstCallbackNode !== null &&
firstCallbackNode.expirationTime <= currentTime &&
!(enableSchedulerDebugging && isSchedulerPaused)
);
continue;
break;
else
// Keep flushing callbacks until we run out of time in the frame.
// 进入此循环说明, 帧没有过期
// 继续刷新回调,直到我们在帧中耗尽时间。
if (firstCallbackNode !== null)
do
if (enableSchedulerDebugging && isSchedulerPaused)
break;
flushFirstCallback(); // 见4.2节
while (firstCallbackNode !== null && !shouldYieldToHost());
finally
isPerformingWork = false;
currentHostCallbackDidTimeout = previousDidTimeout;
// There's still work remaining. Request another callback.
scheduleHostCallbackIfNeeded();
这部分主要基于当前帧以及当前任务过期时间来决定是否执行flushFirstCallback
函数, 这个函数就是最终的执行任务函数.
4.2. flushFirstCallback
这个函数比较长, 主要功能为: 执行队首任务并把队首任务从链表移除,把第二个任务置为队首任务。执行任务可能产生新的任务,再把新任务插入到任务链表中. 见下方代码:
// packages\\scheduler\\src\\Scheduler.js
// 执行任务, 并更新链表
function flushFirstCallback()
const currentlyFlushingCallback = firstCallbackNode;
// Remove the node from the list before calling the callback. That way the
// list is in a consistent state even if the callback throws.
// 在执行callback之前, 最好先把这个节点从列表中删掉
var next = firstCallbackNode.next;
if (firstCallbackNode === next)
// This is the last callback in the list.
// 由于是双向链表, 这种情况就是现在只剩一个节点了
// 全部设置为null
firstCallbackNode = null;
next = null;
else
// 链表中删除firstCallbackNode
var lastCallbackNode = firstCallbackNode.previous;
firstCallbackNode = lastCallbackNode.next = next;
next.previous = lastCallbackNode;
// 从链表中彻底剥离, 把原对象引用置空
currentlyFlushingCallback.next = currentlyFlushingCallback.previous = null;
// Now it's safe to call the callback.
// 获取属性
var callback = currentlyFlushingCallback.callback;
var expirationTime = currentlyFlushingCallback.expirationTime;
var priorityLevel = currentlyFlushingCallback.priorityLevel;
// 临时保存
var previousPriorityLevel = currentPriorityLevel;
var previousExpirationTime = currentExpirationTime;
currentPriorityLevel = priorityLevel;
currentExpirationTime = expirationTime;
var continuationCallback;
try
const didUserCallbackTimeout =
currentHostCallbackDidTimeout ||
// Immediate priority callbacks are always called as if they timed out
priorityLevel === ImmediatePriority;
continuationCallback = callback(didUserCallbackTimeout);
catch (error)
throw error;
finally
// 还原
currentPriorityLevel = previousPriorityLevel;
currentExpirationTime = previousExpirationTime;
// A callback may return a continuation. The continuation should be scheduled
// with the same priority and expiration as the just-finished callback.
// 如果返回的是函数, 则和刚刚完成的回调函数具有相同的过期时间和优先级
if (typeof continuationCallback === 'function')
var continuationNode: CallbackNode =
callback: continuationCallback,
priorityLevel,
expirationTime,
next: null,
previous: null,
;
// Insert the new callback into the list, sorted by its expiration. This is
// almost the same as the code in `scheduleCallback`, except the callback
// is inserted into the list *before* callbacks of equal expiration instead
// of after.
// 下面和scheduleCallback函数一个逻辑, 只有一点不同, `callback`在等于到期时间的回调之前插入到列表中而不是插入到之后
if (firstCallbackNode === null)
// This is the first callback in the list.
firstCallbackNode = continuationNode.next = continuationNode.previous = continuationNode;
else
var nextAfterContinuation = null;
var node = firstCallbackNode;
do
if (node.expirationTime >= expirationTime)
// This callback expires at or after the continuation. We will insert
// the continuation *before* this callback.
nextAfterContinuation = node;
break;
node = node.next;
while (node !== firstCallbackNode);
if (nextAfterContinuation === null)
// No equal or lower priority callback was found, which means the new
// callback is the lowest priority callback in the list.
// 没有找到相等或者低优先级的callback, 因此放到第一个
nextAfterContinuation = firstCallbackNode;
else if (nextAfterContinuation === firstCallbackNode)
// The new callback is the highest priority callback in the list.
firstCallbackNode = continuationNode;
scheduleHostCallbackIfNeeded();
// 插入操作
var previous = nextAfterContinuation.previous;
previous.next = nextAfterContinuation.previous = continuationNode;
continuationNode.next = nextAfterContinuation;
continuationNode.previous = previous;
5. requestHostCallback
接下来我们最后再看下这个函数, 代码如下:
// packages\\scheduler\\src\\forks\\SchedulerHostConfig.default.js
// absoluteTimeout => 链表node的过期时间(expirationTime)
// callback => flushWork函数
requestHostCallback = function(callback, absoluteTimeout)
scheduledHostCallback = callback;
timeoutTime = absoluteTimeout;
if (isFlushingHostCallback || absoluteTimeout < 0)
// Don't wait for the next frame. Continue working ASAP, in a new event.
// 如果absoluteTimeout时间小于1, 则此次work为ImmediatePriority优先级
// 应该立即执行
port.postMessage(undefined);
else if (!isAnimationFrameScheduled)
// If rAF didn't already schedule one, we need to schedule a frame.
// TODO: If this rAF doesn't materialize because the browser throttles, we
// might want to still have setTimeout trigger rIC as a backup to ensure
// that we keep performing work.
isAnimationFrameScheduled = true;
requestAnimationFrameWithTimeout(animationTick); // animationTick为一个函数, 见5.2节
;
这部分没多少代码
5.1. requestAnimationFrameWithTimeout
我们接下来看下requestAnimationFrameWithTimeout
函数, 代码如下:
// packages\\scheduler\\src\\forks\\SchedulerHostConfig.default.js
// requestAnimationFrame does not run when the tab is in the background. If
// we're backgrounded we prefer for that work to happen so that the page
// continues to load in the background. So we also schedule a 'setTimeout' as
// a fallback.
// TODO: Need a better heuristic for backgrounded work.
// requestAnimationFrame在切换tab之后不再运行,如果切换tab之后
// 我们还想让他在后台继续运行,应使用setTimeout作为兜底操作
const ANIMATION_FRAME_TIMEOUT = 100;
let rAFID;
let rAFTimeoutID;
const requestAnimationFrameWithTimeout = function(callback)
// schedule rAF and also a setTimeout
rAFID = localRequestAnimationFrame(function(timestamp)
// timestamp 实际上就是performance.now()
// cancel the setTimeout, 如果RAF好使就不使用Timeout
// callback实际就是下面的animationTick函数, 见5.2节
localClearTimeout(rAFTimeoutID);
callback(timestamp); // callback就是animationTick函数, 见5.2小节
);
rAFTimeoutID = localSetTimeout(function()
// cancel the requestAnimationFrame
localCancelAnimationFrame(rAFID);
callback(getCurrentTime());
, ANIMATION_FRAME_TIMEOUT);
;
5.2. animationTick
接着上面那个callback(timestamp);
来
React默认使用30fps运行, 在运行时每次都会比对前一次帧运行时间和下次帧运行时间, 来动态调整至合适数值.
let frameDeadline = 0;
let previousFrameTime = 33;
let activeFrameTime = 33;
const animationTick = function(rafTime)
// scheduledHostCallback 就是flushWork函数
if (scheduledHostCallback !== null)
// Eagerly schedule the next animation callback at the beginning of the
// frame. If the scheduler queue is not empty at the end of the frame, it
// will continue flushing inside that callback. If the queue *is* empty,
// then it will exit immediately. Posting the callback at the start of the
// frame ensures it's fired within the earliest possible frame. If we
// waited until the end of the frame to post the callback, we risk the
// browser skipping a frame and not firing the callback until the frame
// after that.
// 在当前的帧的开始安排下一个animation callback
// 如果任务队列在当前帧不为空, 会继续执行下去, 如果队列为空了, 会立即退出
requestAnimationFrameWithTimeout(animationTick);
else
// No pending work. Exit.
isAnimationFrameScheduled = false;
return;
// rafTime raf执行时的时间
let nextFrameTime = rafTime - frameDeadline + activeFrameTime;
if (
nextFrameTime < activeFrameTime &&
previousFrameTime < activeFrameTime &&
!fpsLocked // 如果没有手动把fps数值死的话
)
if (nextFrameTime < 8)
// Defensive coding. We don't support higher frame rates than 120hz.
// If the calculated frame time gets lower than 8, it is probably a bug.
// 不支持比120hz更好的帧率
nextFrameTime = 8;
// If one frame goes long, then the next one can be short to catch up.
// If two frames are short in a row, then that's an indication that we
// actually have a higher frame rate than what we're currently optimizing.
// We adjust our heuristic dynamically accordingly. For example, if we're
// running on 120hz display or 90hz VR display.
// Take the max of the two in case one of them was an anomaly due to
// missed frame deadlines.
// 如果两次帧时间都比较短, 调整当前帧率, 取这两次中大的那个
activeFrameTime =
nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
else
previousFrameTime = nextFrameTime;
frameDeadline = rafTime + activeFrameTime;
if (!isMessageEventScheduled)
isMessageEventScheduled = true;
// postMessage触发任务链表队列, 见下节5.3.
port.postMessage(undefined);
;
5.3. port.postMessage
这个函数就是调用flushWork
, 并传入帧是否过期的地方, 看下方代码:
// We use the postMessage trick to defer idle work until after the repaint.
// 使用postMessage来将工作推迟到重绘之后
const channel = new MessageChannel();
// Channel Messaging API的Channel Messaging接口允许我们创建一个新的消息通道,并通过它的两个MessagePort 属性(port1, port2)发送数据。
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以上是关于React源码分析=; scheduler分析的主要内容,如果未能解决你的问题,请参考以下文章
小记--------spark资源调度机制源码分析-----Schedule