系列文章
React Fiber 源码分析 第一篇 React Fiber 源码分析 第二篇(同步模式)React Fiber 源码分析 第三篇(异步状态)
前言
React Fiber 是 React 在 V16 版本中的大更新,利用了闲余时间看了一些源码,做个小记录~
流程图
源码分析
1. 调用 setState 时,会调用 classComponentUpdater 的 enqueueSetState 方法,同时将新的 state 作为 payload 参数传进 enqueueSetState 会先调用 requestCurrentTime 获取一个 currentTime
function requestCurrentTime() {
// 维护两个时间 一个 renderingTime 一个 currentSechedulerTime
// rederingTime 可以随时更新 currentSechedulerTime 只有在没有新任务的时候才更新
if (isRendering) {
return currentSchedulerTime;
}
findHighestPriorityRoot();
if (nextFlushedExpirationTime === NoWork || nextFlushedExpirationTime === Never) {
recomputeCurrentRendererTime();
currentSchedulerTime = currentRendererTime;
return currentSchedulerTime;
}
return currentSheculerTime
2. 通过获取到的 currentTime, 调用 computeExpirationForFiber,计算该 fiber 的优先级
if (fiber.mode & AsyncMode) {
if (isBatchingInteractiveUpdates) {
// This is an interactive update
expirationTime = computeInteractiveExpiration(currentTime);
} else {
// This is an async update
expirationTime = computeAsyncExpiration(currentTime);
}
…
}
3. 这个函数其他点比较简单,里面主要有下面 这个判断要说明一下,如果是属于异步更新的话,会根据是 交互引起的更新 还是其他更新 来调用不同的函数 computeInteractiveExpiration 和 computeAsyncExpiration,
可以看到这两个函数最后返回的都是 computeExpirationBucket 函数的结果,只是入参不同,computeInteractiveExpiration 的参数是 500,100,computeAsyncExpiration 的参数是 5000,250,然后看 computeExpirationBucket 函数可以看到,第二个参数(500 和 5000)越大,则返回的 expirationTime 越大,也就是说 computeInteractiveExpiration 的更新优先级高于 computeAsyncExpiration, 则交互的优先级高于其他
获得优先级后则和同步更新一样,创建 update 并放进队列,然后调用 sheuduleWork
var classComponentUpdater = {
isMounted: isMounted,
enqueueSetState: function (inst, payload, callback) {
var fiber = get(inst);
// 获得优先级
var currentTime = requestCurrentTime();
var expirationTime = computeExpirationForFiber(currentTime, fiber);
// 创建更新
var update = createUpdate(expirationTime);
update.payload = payload;
if (callback !== undefined && callback !== null) {
update.callback = callback;
}
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
4. 接下来的步骤和同步一样,直到同步调用的是 performSyncWork 函数,而异步调用的是 scheduleCallbackWithExpirationTime 函数
scheduleCallbackWithExpirationTime 函数首先判断是否存在 callback 正在进行中,判断现有 expirationTime 和其优先级,若优先级比较低则直接返回,否则设置现在的 fiber 任务为新的 callback,并把原来的回调从列表中移除
function scheduleCallbackWithExpirationTime(root, expirationTime) {
if (callbackExpirationTime !== NoWork) {
// 判断优先级
if (expirationTime > callbackExpirationTime) {
// Existing callback has sufficient timeout. Exit.
return;
} else {
if (callbackID !== null) {
// 取消, 从回调列表中删除
schedule.unstable_cancelScheduledWork(callbackID);
}
}
// The request callback timer is already running. Don’t start a new one.
}
// 设置新的 callback 和 callbackExiporationTime
callbackExpirationTime = expirationTime;
var currentMs = schedule.unstable_now() – originalStartTimeMs;
var expirationTimeMs = expirationTimeToMs(expirationTime);
// 计算是否超时
var timeout = expirationTimeMs – currentMs;
callbackID = schedule.unstable_scheduleWork(performAsyncWork, { timeout: timeout});
}
5. 接下来调用 schedule.unstable_scheduleWork(performAsyncWork, { timeout: timeout}) 函数,并生成一个节点,存储回调函数和超时时间,插入到回调列表,并根据超时排序, 调用 ensureHostCallBackIsScheduled 函数,最后返回该节点
function unstable_scheduleWork(callback, options) {
var currentTime = exports.unstable_now();
var timesOutAt;
// 获取超时时间
if (options !== undefined && options !== null && options.timeout !== null && options.timeout !== undefined) {
// Check for an explicit timeout
timesOutAt = currentTime + options.timeout;
} else {
// Compute an absolute timeout using the default constant.
timesOutAt = currentTime + DEFERRED_TIMEOUT;
}
// 生成一个节点,存储回调函数和超时时间
var newNode = {
callback: callback,
timesOutAt: timesOutAt,
next: null,
previous: null
};
// 插入到回调列表,并根据超时排序, 最后返回该节点
if (firstCallbackNode === null) {
// This is the first callback in the list.
firstCallbackNode = newNode.next = newNode.previous = newNode;
ensureHostCallbackIsScheduled(firstCallbackNode);
} else {
…var previous = next.previous;
previous.next = next.previous = newNode;
newNode.next = next;
newNode.previous = previous;
}
return newNode;
}
6.ensureHostCallBackIsScheduled 函数如名,相对比较简单
function ensureHostCallbackIsScheduled() {
if (isPerformingWork) {
// Don’t schedule work yet; wait until the next time we yield.
return;
}
// Schedule the host callback using the earliest timeout in the list.
var timesOutAt = firstCallbackNode.timesOutAt;
if (!isHostCallbackScheduled) {
isHostCallbackScheduled = true;
} else {
// Cancel the existing host callback.
cancelCallback();
}
requestCallback(flushWork, timesOutAt);
}
7. 往下看 requestCallback,这里说的如果已经在执行任务的话,就必须有一个错误被抛出(抛出的错误是啥??),同时不要等待下一帧,尽快开始新事件
如果如果当前没有调度帧回调函数,我们需要进行一个调度帧回调函数, 并设置 isAnimationFrameScheduled 为 true, 接着执行 requestAnimationFrameWithTimeout; 函数
requestCallback = function (callback, absoluteTimeout) {
scheduledCallback = callback;
timeoutTime = absoluteTimeout;
if (isPerformingIdleWork) {
// 如果已经在执行任务的话,就必须有一个错误被抛出(抛出的错误是啥??),同时不要等待下一帧,尽快开始新事件
window.postMessage(messageKey, ‘*’);
} else if (!isAnimationFrameScheduled) {
isAnimationFrameScheduled = true;
requestAnimationFrameWithTimeout(animationTick);
}
};
8.requestAnimationFrameWithTimeout 函数就是执行一个异步操作,执行完毕后,假设此时又有 N 个回调任务进入,同时原来的回调还没有进行,则回到 scheduleCallbackWithExpirationTime 函数上,
分为两个分支:
假设优先级低于目前的回调任务,则直接返回(已经把 root 加到 root 队列中)
优先级高于目前的回调任务,将目前的回调任务从列表中移除,并将 callBackID 设为传入的回调,接下来的路线与上面一致,假设该传入的回调超时最早,则会进入到 cancelCallback 函数,重置各变量,并进入到 requestCallback 函数,此时除了赋值操作,没有其他动作
到了这时候,已经把新的回调替换正在进行的回调到回调列表。函数正常执行,调用 callback, 即 animationTick 函数
cancelCallback = function () {
scheduledCallback = null;
isIdleScheduled = false;
timeoutTime = -1;
};
var ANIMATION_FRAME_TIMEOUT = 100;
var rAFID;
var rAFTimeoutID;
var requestAnimationFrameWithTimeout = function (callback) {
// schedule rAF and also a setTimeout
rAFID = localRequestAnimationFrame(function (timestamp) {
// cancel the setTimeout
localClearTimeout(rAFTimeoutID);
callback(timestamp);
});
rAFTimeoutID = localSetTimeout(function () {
// cancel the requestAnimationFrame
localCancelAnimationFrame(rAFID);
callback(exports.unstable_now());
}, ANIMATION_FRAME_TIMEOUT);
};
9.animationTick 一个是把 isAnimationFrameScheduled 状态设为 false,即不在调度帧回调的状态,同时计算帧到期时间 frameDeadline , 判断是否在帧回调的状态,否的话调用 window.postMessage , 并设置 isIdleScheduled 状态为 true
假设此时,有 N 个回调进入,分为两个情况:1. 假设优先级低于目前的回调任务,则直接返回(已经把 root 加到 root 队列中)2. 优先级高于目前的回调任务,将目前的回调任务从列表中移除,并将 callBackID 设为传入的回调,接下来的路线与上面一致,一直到 animationTick 函数,因为 postMessage 比 setTImeout 更快执行,所以此时 isIdleScheduled 为 false,和之前一样正常执行。
var animationTick = function (rafTime) {
isAnimationFrameScheduled = false;
…
…
// 每帧到期时间为 33ms
frameDeadline = rafTime + activeFrameTime;
if (!isIdleScheduled) {
isIdleScheduled = true;
window.postMessage(messageKey, ‘*’);
}
};
10.postMessage 会执行 idleTick,首先把 isIdleScheduleddidTimeout 置为 false,
先判断帧到期时间和超时时间是否小于当前时间,如果是的话,则置 didTimeout 为 true, 如果帧到期,但超时时间小于当前时间,则置 isAnimationFrameScheduled 为 false,并调用 requestAnimationFrameWithTimeout, 即进入下一帧如果帧未到期,则调用 callbak 函数,并把 isPerformingIdleWork 置为 true
idleTick 会先执行 callback,完成后才将 isPerformingIdleWork 置为 false,执行 callback 的时候会传入 didTimeout 作为参数,callback 为 flushWork
var idleTick = function (event) {
…
isIdleScheduled = false;
var currentTime = exports.unstable_now();
var didTimeout = false;
if (frameDeadline – currentTime <= 0) {
// 帧过期
if (timeoutTime !== -1 && timeoutTime <= currentTime) {
// 回调超时
didTimeout = true;
} else {
// No timeout.
if (!isAnimationFrameScheduled) {
// 到下一帧继续任务
isAnimationFrameScheduled = true;
requestAnimationFrameWithTimeout(animationTick);
}
// Exit without invoking the callback.
return;
}
}
timeoutTime = -1;
var callback = scheduledCallback;
scheduledCallback = null;
if (callback !== null) {
isPerformingIdleWork = true;
try {
callback(didTimeout);
} finally {
isPerformingIdleWork = false;
}
}
};
11.flushwork 首先把 isPerformingWork 置为 true,然后把 didTimeout 赋值给 deallinObject 对象,接下来进行判断如果已经过了帧的结束期,则判断链表中有哪个节点已超时,并循环调用 flushFirstCallback 函数解决超时节点,如果还没有过帧的结束期,则调用 flushFirstCallback 函数处理链表中的第一个节点,循环处理一直到该帧结束
最后,flushwork 函数会将 isPerformingWork 置为 false,并判断是否还有任务 有则执行 ensureHostCallbackIsScheduled 函数
function flushWork(didTimeout) {
isPerformingWork = true;
deadlineObject.didTimeout = didTimeout;
try {
if (didTimeout) {
while (firstCallbackNode !== null) {
var currentTime = exports.unstable_now();
if (firstCallbackNode.timesOutAt <= currentTime) {
do {
flushFirstCallback();
} while (firstCallbackNode !== null && firstCallbackNode.timesOutAt <= currentTime);
continue;
}
break;
}
} else {
// Keep flushing callbacks until we run out of time in the frame.
if (firstCallbackNode !== null) {
do {
flushFirstCallback();
} while (firstCallbackNode !== null && getFrameDeadline() – exports.unstable_now() > 0);
}
}
} finally {
isPerformingWork = false;
if (firstCallbackNode !== null) {
// There’s still work remaining. Request another callback.
ensureHostCallbackIsScheduled(firstCallbackNode);
} else {
isHostCallbackScheduled = false;
}
}
}
12. 继续往下看,则是 flushFirstCallback 函数,先把该节点从链表中清掉,然后调用 callback 函数,并带入 deadlineObject 作为参数
function flushFirstCallback(node) {
var flushedNode = firstCallbackNode;
// 从链表中清理掉该节点,这样哪怕出错了,也能保留原链表状态
var next = firstCallbackNode.next;
if (firstCallbackNode === next) {
// This is the last callback in the list.
firstCallbackNode = null;
next = null;
} else {
var previous = firstCallbackNode.previous;
firstCallbackNode = previous.next = next;
next.previous = previous;
}
flushedNode.next = flushedNode.previous = null;
// Now it’s safe to call the callback.
var callback = flushedNode.callback;
callback(deadlineObject);
}
13. 接下来的就是 performAsyncWork 函数,如果 didTimeout 为 true,则表明至少有一个更新已过期,迭代所有 root 任务,把已过期的 root 的 nextExpirationTimeToWorkOn 重置为当前时间 currentTime. 然后调用 performWork 函数
function performAsyncWork(dl) {
if (dl.didTimeout) {
// 刷新所有 root 的 nextEpirationTimeToWorkOn
if (firstScheduledRoot !== null) {
recomputeCurrentRendererTime();
var root = firstScheduledRoot;
do {
didExpireAtExpirationTime(root, currentRendererTime);
// The root schedule is circular, so this is never null.
root = root.nextScheduledRoot;
} while (root !== firstScheduledRoot);
}
}
performWork(NoWork, dl);
}
14.performWork 函数在之前已经分析过了,这里主要看存在 deadline 时的操作,在帧未到期 或者 当前渲染时间大于等于 nextFlushedExpirationTime 时才执行 performWorkOnRoot, 并将 currentRendererTime >= nextFlushedExpirationTime 作为第三个参数传入,一直循环处理任务,最后清除 callbackExpirationTime, callBackId, 同时,如果还有任务的话,则继续调用 scheduleCallbackWithExpirationTime(nextFlushedRoot, nextFlushedExpirationTime); 函数进入到回调
function performWork(minExpirationTime, dl) {
deadline = dl;
// Keep working on roots until there’s no more work, or until we reach
// the deadline.
findHighestPriorityRoot();
if (deadline !== null) {
recomputeCurrentRendererTime();
currentSchedulerTime = currentRendererTime;while (nextFlushedRoot !== null && nextFlushedExpirationTime !== NoWork && (minExpirationTime === NoWork || minExpirationTime >= nextFlushedExpirationTime) && (!deadlineDidExpire || currentRendererTime >= nextFlushedExpirationTime)) {
performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, currentRendererTime >= nextFlushedExpirationTime);
findHighestPriorityRoot();
recomputeCurrentRendererTime();
currentSchedulerTime = currentRendererTime;
}
}
if (deadline !== null) {
callbackExpirationTime = NoWork;
callbackID = null;
}
// If there’s work left over, schedule a new callback.
if (nextFlushedExpirationTime !== NoWork) {
scheduleCallbackWithExpirationTime(nextFlushedRoot, nextFlushedExpirationTime);
}
// Clean-up.
deadline = null;
deadlineDidExpire = false;
finishRendering();
}
15. 接下来看异步状态下的 performWorkOnRoot 函数。基本操作和同步一样,在进入到 renderRoot(root, _isYieldy, isExpired); 函数时,会根据是否已超时将 isYieldy 置为 true 或者 false, 异步状态下未超时为 false,renderRoot 和同步一样,最后执行 workLoop(isYieldy)workLoop 在未过期的情况下,会执行 shouldYield() 函数来判断是否执行 nextUnitOfWork, 和同步一样,这里只需要关注 shouldYied 函数
function workLoop(isYieldy) {
if (!isYieldy) {
// Flush work without yielding
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
} else {
// Flush asynchronous work until the deadline runs out of time.
while (nextUnitOfWork !== null && !shouldYield()) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}
}
16.shouldYield 函数,如果 deadlineDidExpire 为 true, 即帧已到期,直接返回 true, 如果 deadline 不存在,并且帧未到期,则返回 false, 可以执行单元否则将 deadlineDidExpire 置为 true
function shouldYield() {
if (deadlineDidExpire) {
return true;
}
if (deadline === null || deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
// Disregard deadline.didTimeout. Only expired work should be flushed
// during a timeout. This path is only hit for non-expired work.
return false;
}
deadlineDidExpire = true;
return true;
}
总结
源码分析到这里就结束啦,下一篇做一个总结,不然就是流水账一样的,容易忘记