react-native
Version:
A framework for building native apps using React
349 lines (312 loc) • 12.3 kB
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.
*
* @providesModule ReactFiberScheduler
* @flow
*/
;
import type { Fiber } from 'ReactFiber';
import type { FiberRoot } from 'ReactFiberRoot';
import type { HostConfig } from 'ReactFiberReconciler';
import type { PriorityLevel } from 'ReactPriorityLevel';
var ReactFiberBeginWork = require('ReactFiberBeginWork');
var ReactFiberCompleteWork = require('ReactFiberCompleteWork');
var ReactFiberCommitWork = require('ReactFiberCommitWork');
var { cloneFiber } = require('ReactFiber');
var {
NoWork,
LowPriority,
AnimationPriority,
SynchronousPriority,
} = require('ReactPriorityLevel');
var timeHeuristicForUnitOfWork = 1;
export type Scheduler = {
scheduleDeferredWork: (root : FiberRoot, priority : PriorityLevel) => void
};
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
// 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.
let scheduler;
function getScheduler(): Scheduler {
return scheduler;
}
const { beginWork } = ReactFiberBeginWork(config, getScheduler);
const { completeWork } = ReactFiberCompleteWork(config);
const { commitWork } = ReactFiberCommitWork(config);
const scheduleAnimationCallback = config.scheduleAnimationCallback;
const scheduleDeferredCallback = config.scheduleDeferredCallback;
// The default priority to use for updates.
let defaultPriority : PriorityLevel = LowPriority;
// The next work in progress fiber that we're currently working on.
let nextUnitOfWork : ?Fiber = null;
let nextPriorityLevel : PriorityLevel = NoWork;
// Linked list of roots with scheduled work on them.
let nextScheduledRoot : ?FiberRoot = null;
let lastScheduledRoot : ?FiberRoot = 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.
let root = nextScheduledRoot;
let highestPriorityRoot = null;
let 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 : Fiber) {
// Commit all the side-effects within a tree.
// TODO: Error handling.
let effectfulFiber = finishedWork.firstEffect;
while (effectfulFiber) {
const current = effectfulFiber.alternate;
commitWork(current, effectfulFiber);
const 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 : Fiber) {
let 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.
let 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 : Fiber) : ?Fiber {
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.
const current = workInProgress.alternate;
const 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;
const 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.
const root : FiberRoot = (workInProgress.stateNode : any);
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);
const 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 : Fiber) : ?Fiber {
// 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.
const current = workInProgress.alternate;
const 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 : FiberRoot, priority : PriorityLevel) {
// 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: FiberRoot, priorityLevel : 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 : FiberRoot) {
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 : PriorityLevel, fn : Function) {
const previousDefaultPriority = defaultPriority;
defaultPriority = priorityLevel;
try {
fn();
} finally {
defaultPriority = previousDefaultPriority;
}
}
scheduler = {
scheduleWork: scheduleWork,
scheduleDeferredWork: scheduleDeferredWork,
performWithPriority: performWithPriority,
};
return scheduler;
};