Fiber side-effects

This adds tracking of side-effects that gets scheduled during an
update.

As the tree gets reconciled, the side-effectful fibers are linked
together in an ordered singly linked list. That way we can walk
the linked list to commit only the work that needs to be
synchronous - quickly.

We also store first and last nodes within a fiber. That
way when we reuse an already processed subtree, we can reuse that
subset of the linked list.
This commit is contained in:
Sebastian Markbage
2016-06-29 15:20:54 -07:00
parent 291f8e30a9
commit f84a8eabc7
4 changed files with 111 additions and 5 deletions
+21
View File
@@ -82,6 +82,16 @@ export type Fiber = Instance & {
// if this returns multiple values. Such as a fragment.
output: any, // This type will be more specific once we overload the tag.
// Singly linked list fast path to the next fiber with side-effects.
nextEffect: ?Fiber,
// The first and last fiber with side-effect within this subtree. This allows
// us to reuse a slice of the linked list when we reuse the work done within
// this fiber.
firstEffect: ?Fiber,
lastEffect: ?Fiber,
// This will be used to quickly determine if a subtree has no pending changes.
pendingWorkPriority: PriorityLevel,
@@ -129,6 +139,10 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber {
memoizedProps: null,
output: null,
nextEffect: null,
firstEffect: null,
lastEffect: null,
pendingWorkPriority: NoWork,
hasWorkInProgress: false,
@@ -157,6 +171,13 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
alt.ref = alt.ref;
alt.pendingProps = fiber.pendingProps;
alt.pendingWorkPriority = priorityLevel;
// Whenever we clone, we do so to get a new work in progress.
// This ensures that we've reset these in the new tree.
alt.nextEffect = null;
alt.firstEffect = null;
alt.lastEffect = null;
return alt;
}
@@ -0,0 +1,38 @@
/**
* 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 ReactFiberCommitWork
* @flow
*/
'use strict';
import type { Fiber } from 'ReactFiber';
var ReactTypeOfWork = require('ReactTypeOfWork');
var {
ClassComponent,
HostContainer,
HostComponent,
} = ReactTypeOfWork;
exports.commitWork = function(finishedWork : Fiber) : void {
switch (finishedWork.tag) {
case ClassComponent:
// TODO: Fire componentDidMount/componentDidUpdate, update refs
return;
case HostContainer:
// TODO: Attach children to root container.
return;
case HostComponent:
console.log('commit updates to host component', finishedWork.type);
return;
default:
throw new Error('This unit of work tag should not have side-effects.');
}
};
@@ -30,6 +30,16 @@ var {
YieldComponent,
} = ReactTypeOfWork;
function markForPostEffect(workInProgress : Fiber) {
// Schedule a side-effect on this fiber, after the children's side-effects.
if (workInProgress.lastEffect) {
workInProgress.lastEffect.nextEffect = workInProgress;
} else {
workInProgress.firstEffect = workInProgress;
}
workInProgress.lastEffect = workInProgress;
}
function transferOutput(child : ?Fiber, returnFiber : Fiber) {
// If we have a single result, we just pass that through as the output to
// avoid unnecessary traversal. When we have multiple output, we just pass
@@ -108,6 +118,11 @@ exports.completeWork = function(current : ?Fiber, workInProgress : Fiber) : ?Fib
return null;
case HostComponent:
transferOutput(workInProgress.child, workInProgress);
if (workInProgress.alternate) {
// If we have an alternate, that means this is an update and we need to
// schedule a side-effect to do the updates.
markForPostEffect(workInProgress);
}
console.log('/host component', workInProgress.type);
return null;
case CoroutineComponent:
@@ -19,6 +19,7 @@ import type { HostConfig } from 'ReactFiberReconciler';
var { cloneFiber } = require('ReactFiber');
var { beginWork } = require('ReactFiberBeginWork');
var { completeWork } = require('ReactFiberCompleteWork');
var { commitWork } = require('ReactFiberCommitWork');
var { findNextUnitOfWorkAtPriority } = require('ReactFiberPendingWork');
var {
@@ -78,6 +79,22 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) {
return null;
}
function commitAllWork(finishedWork : Fiber) {
// Commit all the side-effects within a tree.
// TODO: Error handling.
let effectfulFiber = finishedWork.firstEffect;
while (effectfulFiber) {
commitWork(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 completeUnitOfWork(workInProgress : Fiber) : ?Fiber {
while (true) {
// The current, flushed, state of this fiber is the alternate.
@@ -96,11 +113,25 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) {
const returnFiber = workInProgress.return;
// Ensure that remaining work priority bubbles up.
if (returnFiber && workInProgress.pendingWorkPriority !== NoWork &&
(returnFiber.pendingWorkPriority === NoWork ||
returnFiber.pendingWorkPriority > workInProgress.pendingWorkPriority)) {
returnFiber.pendingWorkPriority = workInProgress.pendingWorkPriority;
if (returnFiber) {
// Ensure that remaining work priority bubbles up.
if (workInProgress.pendingWorkPriority !== NoWork &&
(returnFiber.pendingWorkPriority === NoWork ||
returnFiber.pendingWorkPriority > workInProgress.pendingWorkPriority)) {
returnFiber.pendingWorkPriority = workInProgress.pendingWorkPriority;
}
// 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) {
@@ -121,6 +152,7 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) {
// also ensures that work scheduled during reconciliation gets deferred.
// const hasMoreWork = workInProgress.pendingWorkPriority !== NoWork;
console.log('----- COMPLETED with remaining work:', workInProgress.pendingWorkPriority);
commitAllWork(workInProgress);
const nextWork = findNextUnitOfWork();
// if (!nextWork && hasMoreWork) {
// TODO: This can happen when some deep work completes and we don't