Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fiber] Add support for simple updates and fiber pooling #6981

Merged
merged 2 commits into from
Jun 7, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 34 additions & 3 deletions src/renderers/shared/fiber/ReactChildFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,26 @@ var {
var ReactFiber = require('ReactFiber');
var ReactReifiedYield = require('ReactReifiedYield');

function createSubsequentChild(parent : Fiber, previousSibling : Fiber, newChildren) : Fiber {
function createSubsequentChild(parent : Fiber, nextReusable : ?Fiber, previousSibling : Fiber, newChildren) : Fiber {
if (typeof newChildren !== 'object' || newChildren === null) {
return previousSibling;
}

switch (newChildren.$$typeof) {
case REACT_ELEMENT_TYPE: {
const element = (newChildren : ReactElement<any>);
if (nextReusable &&
element.type === nextReusable.type &&
element.key === nextReusable.key) {
// TODO: This is not sufficient since previous siblings could be new.
// Will fix reconciliation properly later.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, do type and key match whenever nextReusable is set?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, only if they line up exactly. This logic is broken though. It is not a complete impl yet.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I meant in this (incomplete) version.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the unit tests this is currently true but not necessarily. nextReusable lines up with the index in the array but the key and type might not.

const clone = ReactFiber.cloneFiber(nextReusable);
clone.input = element.props;
clone.child = nextReusable.child;
clone.sibling = null;
previousSibling.sibling = clone;
return clone;
}
const child = ReactFiber.createFiberFromElement(element);
previousSibling.sibling = child;
child.parent = parent;
Expand Down Expand Up @@ -64,7 +76,11 @@ function createSubsequentChild(parent : Fiber, previousSibling : Fiber, newChild
if (Array.isArray(newChildren)) {
let prev : Fiber = previousSibling;
for (var i = 0; i < newChildren.length; i++) {
prev = createSubsequentChild(parent, prev, newChildren[i]);
let reusable = null;
if (prev.alternate) {
reusable = prev.alternate.sibling;
}
prev = createSubsequentChild(parent, reusable, prev, newChildren[i]);
}
return prev;
} else {
Expand All @@ -81,6 +97,17 @@ function createFirstChild(parent, newChildren) {
switch (newChildren.$$typeof) {
case REACT_ELEMENT_TYPE: {
const element = (newChildren : ReactElement<any>);
const existingChild : ?Fiber = parent.child;
if (existingChild &&
element.type === existingChild.type &&
element.key === existingChild.key) {
// Get the clone of the existing fiber.
const clone = ReactFiber.cloneFiber(existingChild);
clone.input = element.props;
clone.child = existingChild.child;
clone.sibling = null;
return clone;
}
const child = ReactFiber.createFiberFromElement(element);
child.parent = parent;
return child;
Expand Down Expand Up @@ -114,7 +141,11 @@ function createFirstChild(parent, newChildren) {
prev = createFirstChild(parent, newChildren[i]);
first = prev;
} else {
prev = createSubsequentChild(parent, prev, newChildren[i]);
let reusable = null;
if (prev.alternate) {
reusable = prev.alternate.sibling;
}
prev = createSubsequentChild(parent, reusable, prev, newChildren[i]);
}
}
return first;
Expand Down
80 changes: 69 additions & 11 deletions src/renderers/shared/fiber/ReactFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,59 +25,90 @@ var ReactElement = require('ReactElement');

import type { ReactCoroutine, ReactYield } from 'ReactCoroutine';

export type Fiber = {
// An Instance is shared between all versions of a component. We can easily
// break this out into a separate object to avoid copying so much to the
// alternate versions of the tree. We put this on a single object for now to
// minimize the number of objects created during the initial render.
type Instance = {

// Tag identifying the type of fiber.
tag: number,

// Singly Linked List Tree Structure.
parent: ?Fiber, // Consider a regenerated temporary parent stack instead.
child: ?Fiber,
sibling: ?Fiber,
// The parent Fiber used to create this one. The type is constrained to the
// Instance part of the Fiber since it is not safe to traverse the tree from
// the instance.
parent: ?Instance, // Consider a regenerated temporary parent stack instead.

// Unique identifier of this child.
key: ?string,
key: null | string,

// The function/class/module associated with this fiber.
type: any,

// The local state associated with this fiber.
stateNode: ?Object,

};

// A Fiber is work on a Component that needs to be done or was done. There can
// be more than one per component.
export type Fiber = Instance & {

// Singly Linked List Tree Structure.
child: ?Fiber,
sibling: ?Fiber,

// The ref last used to attach this node.
// I'll avoid adding an owner field for prod and model that as functions.
ref: null | (handle : ?Object) => void,

// Input is the data coming into process this fiber. Arguments. Props.
input: any, // This type will be more specific once we overload the tag.
// TODO: I think that there is a way to merge input and memoizedInput somehow.
memoizedInput: any, // The input used to create the output.
// Output is the return value of this fiber, or a linked list of return values
// if this returns multiple values. Such as a fragment.
output: any, // This type will be more specific once we overload the tag.

// This will be used to quickly determine if a subtree has no pending changes.
hasPendingChanges: bool,

// The local state associated with this fiber.
stateNode: ?Object,
// This is a pooled version of a Fiber. Every fiber that gets updated will
// eventually have a pair. There are cases when we can clean up pairs to save
// memory if we need to.
alternate: ?Fiber,

};

var createFiber = function(tag : number, key : null | string) : Fiber {
return {

// Instance

tag: tag,

parent: null,
child: null,
sibling: null,

key: key,

type: null,

stateNode: null,

// Fiber

child: null,
sibling: null,

ref: null,

input: null,
memoizedInput: null,
output: null,

hasPendingChanges: true,

stateNode: null,
alternate: null,

};
};
Expand All @@ -86,6 +117,33 @@ function shouldConstruct(Component) {
return !!(Component.prototype && Component.prototype.isReactComponent);
}

// This is used to create an alternate fiber to do work on.
exports.cloneFiber = function(fiber : Fiber) : Fiber {
// We use a double buffering pooling technique because we know that we'll only
// ever need at most two versions of a tree. We pool the "other" unused node
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there some dev-only (or test-only, even) assertions we could make to ensure that we're not somehow clobbering over other work and this truly acts like a fresh clone? Without that I feel like we are likely to mess something up.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we build a test where this happens, I think we're likely to fail the test anyway.

It is true that this is super difficult to reason about and I think there might even be a problem in the code right now because of the use of "alternate" in ReactChildFiber. To make this work without the pooling, I'd have to fix those callsites. I've been looking at it last night but it is harder than it seems because the return value for "next" becomes a tuple if you want to code completely without the use of it.

That means that alternate is also acting as "current" version.

It would be nice to assert dev only but not sure how to do that efficiently. Do you have any ideas?

// that we're free to reuse. This is lazily created to avoid allocating extra
// objects for things that are never updated. It also allow us to reclaim the
// extra memory if needed.
if (fiber.alternate) {
return fiber.alternate;
}
// This should not have an alternate already
var alt = createFiber(fiber.tag, fiber.key);

if (fiber.parent) {
// TODO: This assumes the parent's alternate is already created.
// Stop using the alternates of parents once we have a parent stack.
// $FlowFixMe: This downcast is not safe. It is intentionally an error.
alt.parent = fiber.parent.alternate;
}

alt.type = fiber.type;
alt.stateNode = fiber.stateNode;
alt.alternate = fiber;
fiber.alternate = alt;
return alt;
};

exports.createFiberFromElement = function(element : ReactElement) {
const fiber = exports.createFiberFromElementType(element.type, element.key);
fiber.input = element.props;
Expand Down
96 changes: 55 additions & 41 deletions src/renderers/shared/fiber/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,100 +27,114 @@ var {
YieldComponent,
} = ReactTypesOfWork;

function updateFunctionalComponent(unitOfWork) {
var fn = unitOfWork.type;
var props = unitOfWork.input;
console.log('perform work on:', fn.name);
function updateFunctionalComponent(workInProgress) {
var fn = workInProgress.type;
var props = workInProgress.input;
console.log('update fn:', fn.name);
var nextChildren = fn(props);

unitOfWork.child = ReactChildFiber.reconcileChildFibers(
unitOfWork,
unitOfWork.child,
workInProgress.child = ReactChildFiber.reconcileChildFibers(
workInProgress,
workInProgress.child,
nextChildren
);
}

function updateHostComponent(unitOfWork) {
console.log('host component', unitOfWork.type, typeof unitOfWork.input.children === 'string' ? unitOfWork.input.children : '');
function updateHostComponent(workInProgress) {
console.log('host component', workInProgress.type, typeof workInProgress.input.children === 'string' ? workInProgress.input.children : '');

var nextChildren = unitOfWork.input.children;
unitOfWork.child = ReactChildFiber.reconcileChildFibers(
unitOfWork,
unitOfWork.child,
var nextChildren = workInProgress.input.children;
workInProgress.child = ReactChildFiber.reconcileChildFibers(
workInProgress,
workInProgress.child,
nextChildren
);
}

function mountIndeterminateComponent(unitOfWork) {
var fn = unitOfWork.type;
var props = unitOfWork.input;
function mountIndeterminateComponent(workInProgress) {
var fn = workInProgress.type;
var props = workInProgress.input;
var value = fn(props);
if (typeof value === 'object' && value && typeof value.render === 'function') {
console.log('performed work on class:', fn.name);
// Proceed under the assumption that this is a class instance
unitOfWork.tag = ClassComponent;
workInProgress.tag = ClassComponent;
} else {
console.log('performed work on fn:', fn.name);
// Proceed under the assumption that this is a functional component
unitOfWork.tag = FunctionalComponent;
workInProgress.tag = FunctionalComponent;
}
unitOfWork.child = ReactChildFiber.reconcileChildFibers(
unitOfWork,
unitOfWork.child,
workInProgress.child = ReactChildFiber.reconcileChildFibers(
workInProgress,
workInProgress.child,
value
);
}

function updateCoroutineComponent(unitOfWork) {
var coroutine = (unitOfWork.input : ?ReactCoroutine);
function updateCoroutineComponent(workInProgress) {
var coroutine = (workInProgress.input : ?ReactCoroutine);
if (!coroutine) {
throw new Error('Should be resolved by now');
}
console.log('begin coroutine', unitOfWork.type.name);
unitOfWork.child = ReactChildFiber.reconcileChildFibers(
unitOfWork,
unitOfWork.child,
console.log('begin coroutine', workInProgress.type.name);
workInProgress.child = ReactChildFiber.reconcileChildFibers(
workInProgress,
workInProgress.child,
coroutine.children
);
}

function beginWork(unitOfWork : Fiber) : ?Fiber {
switch (unitOfWork.tag) {
function beginWork(workInProgress : Fiber) : ?Fiber {
const alt = workInProgress.alternate;
if (alt && workInProgress.input === alt.memoizedInput) {
// The most likely scenario is that the previous copy of the tree contains
// the same input as the new one. In that case, we can just copy the output
// and children from that node.
workInProgress.output = alt.output;
workInProgress.child = alt.child;
return null;
}
if (workInProgress.input === workInProgress.memoizedInput) {
// In a ping-pong scenario, this version could actually contain the
// old input. In that case, we can just bail out.
return null;
}
switch (workInProgress.tag) {
case IndeterminateComponent:
mountIndeterminateComponent(unitOfWork);
mountIndeterminateComponent(workInProgress);
break;
case FunctionalComponent:
updateFunctionalComponent(unitOfWork);
updateFunctionalComponent(workInProgress);
break;
case ClassComponent:
console.log('class component', unitOfWork.input.type.name);
console.log('class component', workInProgress.input.type.name);
break;
case HostComponent:
updateHostComponent(unitOfWork);
updateHostComponent(workInProgress);
break;
case CoroutineHandlerPhase:
// This is a restart. Reset the tag to the initial phase.
unitOfWork.tag = CoroutineComponent;
workInProgress.tag = CoroutineComponent;
// Intentionally fall through since this is now the same.
case CoroutineComponent:
updateCoroutineComponent(unitOfWork);
updateCoroutineComponent(workInProgress);
// This doesn't take arbitrary time so we could synchronously just begin
// eagerly do the work of unitOfWork.child as an optimization.
if (unitOfWork.child) {
return beginWork(unitOfWork.child);
// eagerly do the work of workInProgress.child as an optimization.
if (workInProgress.child) {
return beginWork(workInProgress.child);
}
break;
case YieldComponent:
// A yield component is just a placeholder, we can just run through the
// next one immediately.
if (unitOfWork.sibling) {
return beginWork(unitOfWork.sibling);
if (workInProgress.sibling) {
return beginWork(workInProgress.sibling);
}
return null;
default:
throw new Error('Unknown unit of work tag');
}
return unitOfWork.child;
return workInProgress.child;
}

exports.beginWork = beginWork;