[Fiber] String refs and owner tracking (#8099)

* Implement string refs using callback closures

* Merge Fiber type to avoid Flow intersection bugs

Still one remaining type error that I'm not sure how to fix

* Fix Flow issue with an unsafe cast

* Fix missing semicolon

* Add a type import I missed earlier
This commit is contained in:
Andrew Clark
2016-10-26 14:37:32 +01:00
committed by Dan Abramov
parent ce2dee32fd
commit d5eff3b0eb
8 changed files with 101 additions and 24 deletions
@@ -13,6 +13,7 @@
'use strict';
import type { ReactInstance } from 'ReactInstanceType';
import type { Fiber } from 'ReactFiber';
/**
* Keeps track of the current owner.
@@ -26,7 +27,7 @@ var ReactCurrentOwner = {
* @internal
* @type {ReactComponent}
*/
current: (null: null | ReactInstance),
current: (null: null | ReactInstance | Fiber),
};
@@ -315,9 +315,10 @@ var ReactComponentTreeHook = {
}
var currentOwner = ReactCurrentOwner.current;
var id = currentOwner && currentOwner._debugID;
info += ReactComponentTreeHook.getStackAddendumByID(id);
if (currentOwner && typeof currentOwner._debugID === 'number') {
var id = currentOwner && currentOwner._debugID;
info += ReactComponentTreeHook.getStackAddendumByID(id);
}
return info;
},
+5 -1
View File
@@ -18,6 +18,8 @@ var ReactInstanceMap = require('ReactInstanceMap');
var invariant = require('invariant');
var warning = require('warning');
import type { ReactInstance } from 'ReactInstanceType';
/**
* ReactNative vs ReactWeb
* -----------------------
@@ -50,7 +52,8 @@ var warning = require('warning');
function findNodeHandle(componentOrHandle: any): ?number {
if (__DEV__) {
var owner = ReactCurrentOwner.current;
// TODO: fix this unsafe cast to work with Fiber.
var owner = ((ReactCurrentOwner.current: any): ReactInstance | null);
if (owner !== null) {
warning(
owner._warnedAboutRefsInRender,
@@ -61,6 +64,7 @@ function findNodeHandle(componentOrHandle: any): ?number {
'componentDidUpdate instead.',
owner.getName() || 'A component'
);
owner._warnedAboutRefsInRender = true;
}
}
+32 -5
View File
@@ -28,6 +28,7 @@ var ReactReifiedYield = require('ReactReifiedYield');
var ReactTypeOfSideEffect = require('ReactTypeOfSideEffect');
var ReactTypeOfWork = require('ReactTypeOfWork');
var emptyObject = require('emptyObject');
var getIteratorFn = require('getIteratorFn');
const {
@@ -47,6 +48,7 @@ const {
const isArray = Array.isArray;
const {
ClassComponent,
HostText,
CoroutineComponent,
YieldComponent,
@@ -62,6 +64,31 @@ const {
Deletion,
} = ReactTypeOfSideEffect;
function transferRef(current: ?Fiber, workInProgress: Fiber, element: ReactElement<any>) {
if (typeof element.ref === 'string') {
if (element._owner) {
const ownerFiber : ?Fiber = (element._owner : any);
if (ownerFiber && ownerFiber.tag === ClassComponent) {
const stringRef = element.ref;
// Check if previous string ref matches new string ref
if (current && current.ref && current.ref._stringRef === stringRef) {
workInProgress.ref = current.ref;
return;
}
const inst = ownerFiber.stateNode;
const ref = function(value) {
const refs = inst.refs === emptyObject ? (inst.refs = {}) : inst.refs;
refs[stringRef] = value;
};
ref._stringRef = stringRef;
workInProgress.ref = ref;
}
}
} else {
workInProgress.ref = element.ref;
}
}
// This wrapper function exists because I expect to clone the code in each path
// to be able to optimize each path individually by branching early. This needs
// a compiler or we can do it manually. Helpers that don't need this branching
@@ -221,13 +248,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
if (current == null || current.type !== element.type) {
// Insert
const created = createFiberFromElement(element, priority);
created.ref = element.ref;
transferRef(current, created, element);
created.return = returnFiber;
return created;
} else {
// Move based on index
const existing = useFiber(current, priority);
existing.ref = element.ref;
transferRef(current, existing, element);
existing.pendingProps = element.props;
existing.return = returnFiber;
return existing;
@@ -319,7 +346,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
const created = createFiberFromElement(newChild, priority);
created.ref = newChild.ref;
transferRef(null, created, newChild);
created.return = returnFiber;
return created;
}
@@ -653,7 +680,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
if (child.type === element.type) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, priority);
existing.ref = element.ref;
transferRef(child, existing, element);
existing.pendingProps = element.props;
existing.return = returnFiber;
return existing;
@@ -668,7 +695,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
}
const created = createFiberFromElement(element, priority);
created.ref = element.ref;
transferRef(currentFirstChild, created, element);
created.return = returnFiber;
return created;
}
+14 -12
View File
@@ -39,11 +39,18 @@ var {
NoEffect,
} = require('ReactTypeOfSideEffect');
// 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 = {
// 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 = {
// These first fields are conceptually members of an Instance. This used to
// be split into a separate type and intersected with the other Fiber fields,
// but until Flow fixes its intersection bugs, we've merged them into a
// single type.
// 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.
// Tag identifying the type of fiber.
tag: TypeOfWork,
@@ -61,11 +68,7 @@ type Instance = {
// parent : Instance -> return The parent happens to be the same as the
// return fiber since we've merged the fiber and instance.
};
// 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 & {
// Remaining fields belong to Fiber
// The Fiber to return to after finishing processing this one.
// This is effectively the parent, but there can be multiple parents (two)
@@ -80,7 +83,7 @@ export type Fiber = Instance & {
// 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,
ref: null | (((handle : ?Object) => void) & { _stringRef: ?string }),
// Input is the data coming into process this fiber. Arguments. Props.
pendingProps: any, // This type will be more specific once we overload the tag.
@@ -327,4 +330,3 @@ exports.createFiberFromYield = function(yieldNode : ReactYield, priorityLevel :
fiber.pendingProps = {};
return fiber;
};
@@ -16,6 +16,7 @@ import type { ReactCoroutine } from 'ReactCoroutine';
import type { Fiber } from 'ReactFiber';
import type { HostConfig } from 'ReactFiberReconciler';
import type { PriorityLevel } from 'ReactPriorityLevel';
import ReactCurrentOwner from 'ReactCurrentOwner';
var {
mountChildFibersInPlace,
@@ -151,7 +152,12 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>, s
}
}
var nextChildren = fn(props);
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
var nextChildren = fn(props);
} else {
var nextChildren = fn(props);
}
reconcileChildren(current, workInProgress, nextChildren);
return workInProgress.child;
}
@@ -176,6 +182,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>, s
}
// Rerender
const instance = workInProgress.stateNode;
ReactCurrentOwner.current = workInProgress;
const nextChildren = instance.render();
reconcileChildren(current, workInProgress, nextChildren);
return workInProgress.child;
@@ -237,12 +244,20 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>, s
}
var fn = workInProgress.type;
var props = workInProgress.pendingProps;
var value = fn(props);
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
var value = fn(props);
} else {
var value = fn(props);
}
if (typeof value === 'object' && value && typeof value.render === 'function') {
// Proceed under the assumption that this is a class instance
workInProgress.tag = ClassComponent;
adoptClassInstance(workInProgress, value);
mountClassInstance(workInProgress);
ReactCurrentOwner.current = workInProgress;
value = value.render();
} else {
// Proceed under the assumption that this is a functional component
@@ -20,6 +20,7 @@ import type { PriorityLevel } from 'ReactPriorityLevel';
var ReactFiberBeginWork = require('ReactFiberBeginWork');
var ReactFiberCompleteWork = require('ReactFiberCompleteWork');
var ReactFiberCommitWork = require('ReactFiberCommitWork');
var ReactCurrentOwner = require('ReactCurrentOwner');
var { cloneFiber } = require('ReactFiber');
@@ -306,6 +307,8 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
}
ReactCurrentOwner.current = null;
return next;
}
@@ -1072,4 +1072,28 @@ describe('ReactIncrementalSideEffects', () => {
// TODO: Test that mounts, updates, refs, unmounts and deletions happen in the
// expected way for aborted and resumed render life-cycles.
it('supports string refs', () => {
var fooInstance = null;
class Bar extends React.Component {
componentDidMount() {
this.test = 'test';
}
render() {
return <div />;
}
}
class Foo extends React.Component {
render() {
fooInstance = this;
return <Bar ref="bar" />;
}
}
ReactNoop.render(<Foo />);
ReactNoop.flush();
expect(fooInstance.refs.bar.test).toEqual('test');
});
});