React源码分析(一)=> scheduler分析
内容导读
互联网集市收集整理的这篇技术教程文章主要介绍了React源码分析(一)=> scheduler分析,小编现在分享给大家,供广大互联网技能从业者学习和参考。文章包含14934字,纯文字阅读大概需要22分钟。
内容图文
![React源码分析(一)=> scheduler分析](/upload/InfoBanner/zyjiaocheng/810/97d186f7c01c4931a3eafc9e77ced472.jpg)
文章目录
- 1. 前言
- 2. getCurrentTime
- 3. unstable_scheduleCallback函数
- 4. scheduleHostCallbackIfNeeded
- 5. requestHostCallback
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);
});
rAFTimeoutID = localSetTimeout(function() {
// cancel the requestAnimationFrame
localCancelAnimationFrame(rAFID);
callback(getCurrentTime());
}, ANIMATION_FRAME_TIMEOUT);
};
5.2. animationTick
我要回家, 赶火车了今天先写到这里, 明天继续更
内容总结
以上是互联网集市为您收集整理的React源码分析(一)=> scheduler分析全部内容,希望文章能够帮你解决React源码分析(一)=> scheduler分析所遇到的程序开发问题。 如果觉得互联网集市技术教程内容还不错,欢迎将互联网集市网站推荐给程序员好友。
内容备注
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 gblab@vip.qq.com 举报,一经查实,本站将立刻删除。
内容手机端
扫描二维码推送至手机访问。