Closed koba04 closed 7 years ago
render(element : ReactElement<any>, container : DOMContainerElement) {
warnAboutUnstableUse();
if (!container._reactRootContainer) {
container._reactRootContainer = DOMRenderer.mountContainer(element, container);
} else {
DOMRenderer.updateContainer(element, container._reactRootContainer);
}
},
In mounting phase, render
uses DOMRenderer.mountContainer
for mounting a component.
var DOMRenderer = ReactFiberReconciler({
updateContainer(container : Container, children : HostChildren<Instance>) : void {
container.innerHTML = '';
recursivelyAppendChildren(container, children);
},
createInstance(type : string, props : Props, children : HostChildren<Instance>) : Instance {
const domElement = document.createElement(type);
recursivelyAppendChildren(domElement, children);
if (typeof props.children === 'string') {
domElement.textContent = props.children;
}
return domElement;
},
prepareUpdate(
domElement : Instance,
oldProps : Props,
newProps : Props,
children : HostChildren<Instance>
) : boolean {
return true;
},
commitUpdate(domElement : Instance, oldProps : Props, newProps : Props, children : HostChildren<Instance>) : void {
domElement.innerHTML = '';
recursivelyAppendChildren(domElement, children);
if (typeof newProps.children === 'string') {
domElement.textContent = newProps.children;
}
},
deleteInstance(instance : Instance) : void {
// Noop
},
scheduleAnimationCallback: window.requestAnimationFrame,
scheduleDeferredCallback: window.requestIdleCallback,
});
DOMRenderer
is created by ReactFiberReconciler
, which is passed configs such as updateContainer
, createInstance
, prepareUpdate
, commitUpdate
, deleteInstance
, scheduleAnimationCallback
and scheduleDeferredCallback
.
You can see scheduleAnimationCallback
equals to window.requestAnimationFrame
, scheduleDeferredCallback
equals to window.requestIdleCallback
.
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) : Reconciler<C> {
var { scheduleWork, performWithPriority } = ReactFiberScheduler(config);
return {
mountContainer(element : ReactElement<any>, containerInfo : C) : OpaqueNode {
const root = createFiberRoot(containerInfo);
const container = root.current;
// TODO: Use pending work/state instead of props.
// TODO: This should not override the pendingWorkPriority if there is
// higher priority work in the subtree.
container.pendingProps = element;
scheduleWork(root);
// It may seem strange that we don't return the root here, but that will
// allow us to have containers that are in the middle of the tree instead
// of being roots.
return container;
},
updateContainer(element : ReactElement<any>, container : OpaqueNode) : void {
// TODO: If this is a nested container, this won't be the root.
const root : FiberRoot = (container.stateNode : any);
// TODO: Use pending work/state instead of props.
root.current.pendingProps = element;
scheduleWork(root);
},
unmountContainer(container : OpaqueNode) : void {
// TODO: If this is a nested container, this won't be the root.
const root : FiberRoot = (container.stateNode : any);
// TODO: Use pending work/state instead of props.
root.current.pendingProps = [];
scheduleWork(root);
},
performWithPriority,
getPublicRootInstance(container : OpaqueNode) : (C | null) {
return null;
},
};
};
ReactFiberReconciler
returns an object, which has mountContainer
, updateContainer
, unmountContainer
, performWithPriority
and getPublicRootInstance
methods.
ReactDOMFiber
calls mountContainer
. Let's get into the mountContainer
🚀
mountContainer
calls createFiberRoot
with containerInfo
. In this case, containerInfo
is a DOM Element mounting a ReactElement.
exports.createFiberRoot = function(containerInfo : any) : FiberRoot {
// Cyclic construction. This cheats the type system right now because
// stateNode is any.
const uninitializedFiber = createHostContainerFiber();
const root = {
current: uninitializedFiber,
containerInfo: containerInfo,
isScheduled: false,
nextScheduledRoot: null,
};
uninitializedFiber.stateNode = root;
return root;
};
createFiberRoot
creates FiberRoot
with HostContainerFiber
.
createHostContainerFiber
is
exports.createHostContainerFiber = function() {
const fiber = createFiber(HostContainer, null);
return fiber;
};
createHostContainerFiber
creates a Fiber for HostContainer
.
HostContainer
is a Fiber for a root of a host tree.
module.exports = {
IndeterminateComponent: 0, // Before we know whether it is functional or class
FunctionalComponent: 1,
ClassComponent: 2,
HostContainer: 3, // Root of a host tree. Could be nested inside another node.
HostComponent: 4,
CoroutineComponent: 5,
CoroutineHandlerPhase: 6,
YieldComponent: 7,
};
createFiber
creates a Fiber.
var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber {
return {
// Instance
tag: tag,
key: key,
type: null,
stateNode: null,
// Fiber
return: null,
child: null,
sibling: null,
ref: null,
pendingProps: null,
memoizedProps: null,
updateQueue: null,
memoizedState: null,
callbackList: null,
output: null,
nextEffect: null,
firstEffect: null,
lastEffect: null,
pendingWorkPriority: NoWork,
progressedPriority: NoWork,
progressedChild: null,
alternate: null,
};
};
As the result, rootFiber
is like this
rootFiber = {
current: hostContainerFiber
containerInfo: rootOfHostTree,
isScheduled: false,
nextScheduledRoot: null,
};
// circular reference (what is this for?)
rootFiber.current.stateNode = rootFiber;
go back mountContainer
, now we've got fiberRoot, a magic of ReactFiber seems to be in scheduleWork(root)
const root = createFiberRoot(containerInfo);
const container = root.current;
// TODO: Use pending work/state instead of props.
// TODO: This should not override the pendingWorkPriority if there is
// higher priority work in the subtree.
container.pendingProps = element;
scheduleWork(root);
// It may seem strange that we don't return the root here, but that will
// allow us to have containers that are in the middle of the tree instead
// of being roots.
return container;
Before scheduleWork
, rootFiber is like this:
After scheduleWork
, rootFiber is like this:
scheduleWork
is in ReactFiberScheduler
.
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);
}
In this case, defaultPriority is LowPriority
What is Priority
?
module.exports = {
NoWork: 0, // No work is pending.
SynchronousPriority: 1, // For controlled text inputs. Synchronous side-effects.
AnimationPriority: 2, // Needs to complete before the next frame.
HighPriority: 3, // Interaction that needs to complete pretty soon to feel responsive.
LowPriority: 4, // Data fetching, or result from updating stores.
OffscreenPriority: 5, // Won't be visible but do the work in case it becomes visible.
};
So scheduleWork
calls scheduleDeferredWork
because LowPriority
is greater than AnimationPriority
.
scheduleDeferredWork
is like this
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);
}
}
priority
is LowPriority
, nextPriorityLevel
is NoWork
, root.current.pendingWorkPriority
is 0
, then root.current.pendingWorkPriority
becomes LowPriority
.
root.isScheduled
becomes true
and lastScheduledRoot
is false
, So nextScheduledRoot
and lastScheduledRoot
become rootFiber
and calls scheduleDeferredCallback
with performDeferredWork
.
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;
}
}
}
nextUnitOfWork
is null
, so findNextUnitOfWork
is called to get a nextUnitOfWork.
before while
loop, let's get into findNextUnitOfWork
.
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;
}
findNextUnitOfWork
returns nextUnitOfWork
, which is a Fiber ReactFiber should work on.
In this case, it returns a cloned Fiber from nextScheduledRoot.current
.
the returned Fiber is passed to performUnitOfWork
, which is to process the Fiber.
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);
}
}
performUnitOfWork
calls beginWork
. If Fiber hasn't a next work, it calls completeUnitOfWork
to complete the current work.
beginWork
is in ReactFiberBeginWork.js
.
function beginWork(current : ?Fiber, workInProgress : Fiber, priorityLevel : PriorityLevel) : ?Fiber {
if (workInProgress.pendingWorkPriority === NoWork ||
workInProgress.pendingWorkPriority > priorityLevel) {
return bailoutOnLowPriority(current, workInProgress);
}
if (workInProgress.progressedPriority === priorityLevel) {
// If we have progressed work on this priority level already, we can
// proceed this that as the child.
workInProgress.child = workInProgress.progressedChild;
}
if ((workInProgress.pendingProps === null || (
workInProgress.memoizedProps !== null &&
workInProgress.pendingProps === workInProgress.memoizedProps
)) &&
workInProgress.updateQueue === null) {
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
switch (workInProgress.tag) {
case IndeterminateComponent:
return mountIndeterminateComponent(current, workInProgress);
case FunctionalComponent:
return updateFunctionalComponent(current, workInProgress);
case ClassComponent:
return updateClassComponent(current, workInProgress);
case HostContainer:
reconcileChildren(current, workInProgress, workInProgress.pendingProps);
// A yield component is just a placeholder, we can just run through the
// next one immediately.
if (workInProgress.child) {
return beginWork(
workInProgress.child.alternate,
workInProgress.child,
priorityLevel
);
}
return null;
case HostComponent:
if (workInProgress.stateNode && config.beginUpdate) {
config.beginUpdate(workInProgress.stateNode);
}
return updateHostComponent(current, workInProgress);
case CoroutineHandlerPhase:
// This is a restart. Reset the tag to the initial phase.
workInProgress.tag = CoroutineComponent;
// Intentionally fall through since this is now the same.
case CoroutineComponent:
updateCoroutineComponent(current, workInProgress);
// This doesn't take arbitrary time so we could synchronously just begin
// eagerly do the work of workInProgress.child as an optimization.
if (workInProgress.child) {
return beginWork(
workInProgress.child.alternate,
workInProgress.child,
priorityLevel
);
}
return workInProgress.child;
case YieldComponent:
// A yield component is just a placeholder, we can just run through the
// next one immediately.
if (workInProgress.sibling) {
return beginWork(
workInProgress.sibling.alternate,
workInProgress.sibling,
priorityLevel
);
}
return null;
default:
throw new Error('Unknown unit of work tag');
}
}
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 commitWork(current : ?Fiber, finishedWork : Fiber) : void {
switch (finishedWork.tag) {
case ClassComponent: {
// Clear updates from current fiber. This must go before the callbacks
// are reset, in case an update is triggered from inside a callback. Is
// this safe? Relies on the assumption that work is only committed if
// the update queue is empty.
if (finishedWork.alternate) {
finishedWork.alternate.updateQueue = null;
}
if (finishedWork.callbackList) {
const { callbackList } = finishedWork;
finishedWork.callbackList = null;
callCallbacks(callbackList, finishedWork.stateNode);
}
// TODO: Fire componentDidMount/componentDidUpdate, update refs
return;
}
case HostContainer: {
// TODO: Attach children to root container.
const children = finishedWork.output;
const root : FiberRoot = finishedWork.stateNode;
const containerInfo : C = root.containerInfo;
updateContainer(containerInfo, children);
return;
}
case HostComponent: {
if (finishedWork.stateNode == null || !current) {
throw new Error('This should only be done during updates.');
}
// Commit the work prepared earlier.
const child = finishedWork.child;
const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child;
const newProps = finishedWork.memoizedProps;
const oldProps = current.memoizedProps;
const instance : I = finishedWork.stateNode;
commitUpdate(instance, oldProps, newProps, children);
return;
}
default:
throw new Error('This unit of work tag should not have side-effects.');
}
}
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 commitWork(current : ?Fiber, finishedWork : Fiber) : void {
switch (finishedWork.tag) {
case ClassComponent: {
// Clear updates from current fiber. This must go before the callbacks
// are reset, in case an update is triggered from inside a callback. Is
// this safe? Relies on the assumption that work is only committed if
// the update queue is empty.
if (finishedWork.alternate) {
finishedWork.alternate.updateQueue = null;
}
if (finishedWork.callbackList) {
const { callbackList } = finishedWork;
finishedWork.callbackList = null;
callCallbacks(callbackList, finishedWork.stateNode);
}
// TODO: Fire componentDidMount/componentDidUpdate, update refs
return;
}
case HostContainer: {
// TODO: Attach children to root container.
const children = finishedWork.output;
const root : FiberRoot = finishedWork.stateNode;
const containerInfo : C = root.containerInfo;
updateContainer(containerInfo, children);
return;
}
case HostComponent: {
if (finishedWork.stateNode == null || !current) {
throw new Error('This should only be done during updates.');
}
// Commit the work prepared earlier.
const child = finishedWork.child;
const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child;
const newProps = finishedWork.memoizedProps;
const oldProps = current.memoizedProps;
const instance : I = finishedWork.stateNode;
commitUpdate(instance, oldProps, newProps, children);
return;
}
default:
throw new Error('This unit of work tag should not have side-effects.');
}
}
This issue is for to trace ReactDOMFiber rendering.
🚀 🚀
This is based on
v15.4.0-rc.3