331 lines
12 KiB
JavaScript
331 lines
12 KiB
JavaScript
/**
|
|
* Copyright 2013-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*
|
|
*
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
var ReactFiberBeginWork = require('./ReactFiberBeginWork');
|
|
var ReactFiberCompleteWork = require('./ReactFiberCompleteWork');
|
|
var ReactFiberCommitWork = require('./ReactFiberCommitWork');
|
|
|
|
var _require = require('./ReactFiber'),
|
|
cloneFiber = _require.cloneFiber;
|
|
|
|
var _require2 = require('./ReactPriorityLevel'),
|
|
NoWork = _require2.NoWork,
|
|
LowPriority = _require2.LowPriority,
|
|
AnimationPriority = _require2.AnimationPriority,
|
|
SynchronousPriority = _require2.SynchronousPriority;
|
|
|
|
var timeHeuristicForUnitOfWork = 1;
|
|
|
|
module.exports = function (config) {
|
|
// Use a closure to circumvent the circular dependency between the scheduler
|
|
// and ReactFiberBeginWork. Don't know if there's a better way to do this.
|
|
var scheduler = void 0;
|
|
function getScheduler() {
|
|
return scheduler;
|
|
}
|
|
|
|
var _ReactFiberBeginWork = ReactFiberBeginWork(config, getScheduler),
|
|
beginWork = _ReactFiberBeginWork.beginWork;
|
|
|
|
var _ReactFiberCompleteWo = ReactFiberCompleteWork(config),
|
|
completeWork = _ReactFiberCompleteWo.completeWork;
|
|
|
|
var _ReactFiberCommitWork = ReactFiberCommitWork(config),
|
|
commitWork = _ReactFiberCommitWork.commitWork;
|
|
|
|
var scheduleAnimationCallback = config.scheduleAnimationCallback;
|
|
var scheduleDeferredCallback = config.scheduleDeferredCallback;
|
|
|
|
// The default priority to use for updates.
|
|
var defaultPriority = LowPriority;
|
|
|
|
// The next work in progress fiber that we're currently working on.
|
|
var nextUnitOfWork = null;
|
|
var nextPriorityLevel = NoWork;
|
|
|
|
// Linked list of roots with scheduled work on them.
|
|
var nextScheduledRoot = null;
|
|
var lastScheduledRoot = null;
|
|
|
|
function findNextUnitOfWork() {
|
|
// Clear out roots with no more work on them.
|
|
while (nextScheduledRoot && nextScheduledRoot.current.pendingWorkPriority === NoWork) {
|
|
nextScheduledRoot.isScheduled = false;
|
|
if (nextScheduledRoot === lastScheduledRoot) {
|
|
nextScheduledRoot = null;
|
|
lastScheduledRoot = null;
|
|
nextPriorityLevel = NoWork;
|
|
return null;
|
|
}
|
|
nextScheduledRoot = nextScheduledRoot.nextScheduledRoot;
|
|
}
|
|
// TODO: This is scanning one root at a time. It should be scanning all
|
|
// roots for high priority work before moving on to lower priorities.
|
|
var root = nextScheduledRoot;
|
|
var highestPriorityRoot = null;
|
|
var highestPriorityLevel = NoWork;
|
|
while (root) {
|
|
if (highestPriorityLevel === NoWork || highestPriorityLevel > root.current.pendingWorkPriority) {
|
|
highestPriorityLevel = root.current.pendingWorkPriority;
|
|
highestPriorityRoot = root;
|
|
}
|
|
// We didn't find anything to do in this root, so let's try the next one.
|
|
root = root.nextScheduledRoot;
|
|
}
|
|
if (highestPriorityRoot) {
|
|
nextPriorityLevel = highestPriorityLevel;
|
|
return cloneFiber(highestPriorityRoot.current, highestPriorityLevel);
|
|
}
|
|
|
|
nextPriorityLevel = NoWork;
|
|
return null;
|
|
}
|
|
|
|
function commitAllWork(finishedWork) {
|
|
// Commit all the side-effects within a tree.
|
|
// TODO: Error handling.
|
|
var effectfulFiber = finishedWork.firstEffect;
|
|
while (effectfulFiber) {
|
|
var current = effectfulFiber.alternate;
|
|
commitWork(current, effectfulFiber);
|
|
var next = effectfulFiber.nextEffect;
|
|
// Ensure that we clean these up so that we don't accidentally keep them.
|
|
// I'm not actually sure this matters because we can't reset firstEffect
|
|
// and lastEffect since they're on every node, not just the effectful
|
|
// ones. So we have to clean everything as we reuse nodes anyway.
|
|
effectfulFiber.nextEffect = null;
|
|
effectfulFiber = next;
|
|
}
|
|
}
|
|
|
|
function resetWorkPriority(workInProgress) {
|
|
var newPriority = NoWork;
|
|
// progressedChild is going to be the child set with the highest priority.
|
|
// Either it is the same as child, or it just bailed out because it choose
|
|
// not to do the work.
|
|
var child = workInProgress.progressedChild;
|
|
while (child) {
|
|
// Ensure that remaining work priority bubbles up.
|
|
if (child.pendingWorkPriority !== NoWork && (newPriority === NoWork || newPriority > child.pendingWorkPriority)) {
|
|
newPriority = child.pendingWorkPriority;
|
|
}
|
|
child = child.sibling;
|
|
}
|
|
workInProgress.pendingWorkPriority = newPriority;
|
|
}
|
|
|
|
function completeUnitOfWork(workInProgress) {
|
|
while (true) {
|
|
// The current, flushed, state of this fiber is the alternate.
|
|
// Ideally nothing should rely on this, but relying on it here
|
|
// means that we don't need an additional field on the work in
|
|
// progress.
|
|
var current = workInProgress.alternate;
|
|
var next = completeWork(current, workInProgress);
|
|
|
|
resetWorkPriority(workInProgress);
|
|
|
|
// The work is now done. We don't need this anymore. This flags
|
|
// to the system not to redo any work here.
|
|
workInProgress.pendingProps = null;
|
|
workInProgress.updateQueue = null;
|
|
|
|
var returnFiber = workInProgress['return'];
|
|
|
|
if (returnFiber) {
|
|
// Ensure that the first and last effect of the parent corresponds
|
|
// to the children's first and last effect. This probably relies on
|
|
// children completing in order.
|
|
if (!returnFiber.firstEffect) {
|
|
returnFiber.firstEffect = workInProgress.firstEffect;
|
|
}
|
|
if (workInProgress.lastEffect) {
|
|
if (returnFiber.lastEffect) {
|
|
returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
|
|
}
|
|
returnFiber.lastEffect = workInProgress.lastEffect;
|
|
}
|
|
}
|
|
|
|
if (next) {
|
|
// If completing this work spawned new work, do that next.
|
|
return next;
|
|
} else if (workInProgress.sibling) {
|
|
// If there is more work to do in this returnFiber, do that next.
|
|
return workInProgress.sibling;
|
|
} else if (returnFiber) {
|
|
// If there's no more work in this returnFiber. Complete the returnFiber.
|
|
workInProgress = returnFiber;
|
|
continue;
|
|
} else {
|
|
// If we're at the root, there's no more work to do. We can flush it.
|
|
var _root = workInProgress.stateNode;
|
|
if (_root.current === workInProgress) {
|
|
throw new Error('Cannot commit the same tree as before. This is probably a bug ' + 'related to the return field.');
|
|
}
|
|
_root.current = workInProgress;
|
|
// TODO: We can be smarter here and only look for more work in the
|
|
// "next" scheduled work since we've already scanned passed. That
|
|
// also ensures that work scheduled during reconciliation gets deferred.
|
|
// const hasMoreWork = workInProgress.pendingWorkPriority !== NoWork;
|
|
commitAllWork(workInProgress);
|
|
var nextWork = findNextUnitOfWork();
|
|
// if (!nextWork && hasMoreWork) {
|
|
// TODO: This can happen when some deep work completes and we don't
|
|
// know if this was the last one. We should be able to keep track of
|
|
// the highest priority still in the tree for one pass. But if we
|
|
// terminate an update we don't know.
|
|
// throw new Error('FiberRoots should not have flagged more work if there is none.');
|
|
// }
|
|
return nextWork;
|
|
}
|
|
}
|
|
}
|
|
|
|
function performUnitOfWork(workInProgress) {
|
|
// The current, flushed, state of this fiber is the alternate.
|
|
// Ideally nothing should rely on this, but relying on it here
|
|
// means that we don't need an additional field on the work in
|
|
// progress.
|
|
var current = workInProgress.alternate;
|
|
var next = beginWork(current, workInProgress, nextPriorityLevel);
|
|
|
|
if (next) {
|
|
// If this spawns new work, do that next.
|
|
return next;
|
|
} else {
|
|
// Otherwise, complete the current work.
|
|
return completeUnitOfWork(workInProgress);
|
|
}
|
|
}
|
|
|
|
function performDeferredWork(deadline) {
|
|
if (!nextUnitOfWork) {
|
|
nextUnitOfWork = findNextUnitOfWork();
|
|
}
|
|
while (nextUnitOfWork) {
|
|
if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
|
|
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
|
|
if (!nextUnitOfWork) {
|
|
// Find more work. We might have time to complete some more.
|
|
nextUnitOfWork = findNextUnitOfWork();
|
|
}
|
|
} else {
|
|
scheduleDeferredCallback(performDeferredWork);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
function scheduleDeferredWork(root, priority) {
|
|
// We must reset the current unit of work pointer so that we restart the
|
|
// search from the root during the next tick, in case there is now higher
|
|
// priority work somewhere earlier than before.
|
|
if (priority <= nextPriorityLevel) {
|
|
nextUnitOfWork = null;
|
|
}
|
|
|
|
// Set the priority on the root, without deprioritizing
|
|
if (root.current.pendingWorkPriority === NoWork || priority <= root.current.pendingWorkPriority) {
|
|
root.current.pendingWorkPriority = priority;
|
|
}
|
|
|
|
if (root.isScheduled) {
|
|
// If we're already scheduled, we can bail out.
|
|
return;
|
|
}
|
|
root.isScheduled = true;
|
|
if (lastScheduledRoot) {
|
|
// Schedule ourselves to the end.
|
|
lastScheduledRoot.nextScheduledRoot = root;
|
|
lastScheduledRoot = root;
|
|
} else {
|
|
// We're the only work scheduled.
|
|
nextScheduledRoot = root;
|
|
lastScheduledRoot = root;
|
|
scheduleDeferredCallback(performDeferredWork);
|
|
}
|
|
}
|
|
|
|
function performAnimationWork() {
|
|
// Always start from the root
|
|
nextUnitOfWork = findNextUnitOfWork();
|
|
while (nextUnitOfWork && nextPriorityLevel !== NoWork) {
|
|
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
|
|
if (!nextUnitOfWork) {
|
|
// Keep searching for animation work until there's no more left
|
|
nextUnitOfWork = findNextUnitOfWork();
|
|
}
|
|
// Stop if the next unit of work is low priority
|
|
if (nextPriorityLevel > AnimationPriority) {
|
|
scheduleDeferredCallback(performDeferredWork);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
function scheduleAnimationWork(root, priorityLevel) {
|
|
// Set the priority on the root, without deprioritizing
|
|
if (root.current.pendingWorkPriority === NoWork || priorityLevel <= root.current.pendingWorkPriority) {
|
|
root.current.pendingWorkPriority = priorityLevel;
|
|
}
|
|
|
|
if (root.isScheduled) {
|
|
// If we're already scheduled, we can bail out.
|
|
return;
|
|
}
|
|
root.isScheduled = true;
|
|
if (lastScheduledRoot) {
|
|
// Schedule ourselves to the end.
|
|
lastScheduledRoot.nextScheduledRoot = root;
|
|
lastScheduledRoot = root;
|
|
} else {
|
|
// We're the only work scheduled.
|
|
nextScheduledRoot = root;
|
|
lastScheduledRoot = root;
|
|
scheduleAnimationCallback(performAnimationWork);
|
|
}
|
|
}
|
|
|
|
function scheduleWork(root) {
|
|
if (defaultPriority === SynchronousPriority) {
|
|
throw new Error('Not implemented yet');
|
|
}
|
|
|
|
if (defaultPriority === NoWork) {
|
|
return;
|
|
}
|
|
if (defaultPriority > AnimationPriority) {
|
|
scheduleDeferredWork(root, defaultPriority);
|
|
return;
|
|
}
|
|
scheduleAnimationWork(root, defaultPriority);
|
|
}
|
|
|
|
function performWithPriority(priorityLevel, fn) {
|
|
var previousDefaultPriority = defaultPriority;
|
|
defaultPriority = priorityLevel;
|
|
try {
|
|
fn();
|
|
} finally {
|
|
defaultPriority = previousDefaultPriority;
|
|
}
|
|
}
|
|
|
|
scheduler = {
|
|
scheduleWork: scheduleWork,
|
|
scheduleDeferredWork: scheduleDeferredWork,
|
|
performWithPriority: performWithPriority
|
|
};
|
|
return scheduler;
|
|
}; |