Support bound function values for ReactElement types (#2518)

Summary:
Release notes: none

Add support for bound function values for `ReactElement` `type`.
Pull Request resolved: https://github.com/facebook/prepack/pull/2518

Differential Revision: D9612253

Pulled By: trueadm

fbshipit-source-id: f6d0b937c5892cfa44297c12a48e73197b50756c
This commit is contained in:
Dominic Gannaway 2018-08-31 07:19:57 -07:00 committed by Facebook Github Bot
parent 54be9b535c
commit 43d480cfad
11 changed files with 323 additions and 43 deletions

View File

@ -14,6 +14,7 @@ import {
AbstractObjectValue,
AbstractValue,
BooleanValue,
BoundFunctionValue,
ConcreteValue,
ECMAScriptSourceFunctionValue,
FunctionValue,
@ -217,7 +218,9 @@ export default function(realm: Realm): void {
0,
(context, [component, config]) => {
let hasValidComponent =
component instanceof ECMAScriptSourceFunctionValue || valueIsKnownReactAbstraction(realm, component);
component instanceof ECMAScriptSourceFunctionValue ||
component instanceof BoundFunctionValue ||
valueIsKnownReactAbstraction(realm, component);
let hasValidConfig =
config instanceof ObjectValue || config === realm.intrinsics.undefined || config === undefined;

View File

@ -11,8 +11,9 @@
import { Realm } from "../realm.js";
import {
ECMAScriptSourceFunctionValue,
AbstractValue,
BoundFunctionValue,
ECMAScriptSourceFunctionValue,
ObjectValue,
AbstractObjectValue,
SymbolValue,
@ -54,7 +55,7 @@ const whitelistedProperties = new Set(["props", "context", "refs", "setState"]);
export function getInitialProps(
realm: Realm,
componentType: ECMAScriptSourceFunctionValue | null,
componentType: ECMAScriptSourceFunctionValue | BoundFunctionValue | null,
{ modelString }: ReactComponentTreeConfig
): AbstractObjectValue {
let componentModel = modelString !== undefined ? (JSON.parse(modelString): ComponentModel) : undefined;
@ -64,9 +65,16 @@ export function getInitialProps(
if (valueIsClassComponent(realm, componentType)) {
propsName = "this.props";
} else {
let formalParameters;
if (componentType instanceof BoundFunctionValue) {
invariant(componentType.$BoundTargetFunction instanceof ECMAScriptSourceFunctionValue);
formalParameters = componentType.$BoundTargetFunction.$FormalParameters;
} else {
formalParameters = componentType.$FormalParameters;
}
// otherwise it's a functional component, where the first paramater of the function is "props" (if it exists)
if (componentType.$FormalParameters.length > 0) {
let firstParam = componentType.$FormalParameters[0];
if (formalParameters.length > 0) {
let firstParam = formalParameters[0];
if (t.isIdentifier(firstParam)) {
propsName = ((firstParam: any): BabelNodeIdentifier).name;
}
@ -80,7 +88,10 @@ export function getInitialProps(
return abstractPropsObject;
}
export function getInitialContext(realm: Realm, componentType: ECMAScriptSourceFunctionValue): AbstractObjectValue {
export function getInitialContext(
realm: Realm,
componentType: ECMAScriptSourceFunctionValue | BoundFunctionValue
): AbstractObjectValue {
let contextName = null;
if (valueIsClassComponent(realm, componentType)) {
// it's a class component, so we need to check the type on for context of the component prototype
@ -91,9 +102,16 @@ export function getInitialContext(realm: Realm, componentType: ECMAScriptSourceF
throw new ExpectedBailOut("context on class components not yet supported");
}
} else {
let formalParameters;
if (componentType instanceof BoundFunctionValue) {
invariant(componentType.$BoundTargetFunction instanceof ECMAScriptSourceFunctionValue);
formalParameters = componentType.$BoundTargetFunction.$FormalParameters;
} else {
formalParameters = componentType.$FormalParameters;
}
// otherwise it's a functional component, where the second paramater of the function is "context" (if it exists)
if (componentType.$FormalParameters.length > 1) {
let secondParam = componentType.$FormalParameters[1];
if (formalParameters.length > 1) {
let secondParam = formalParameters[1];
if (t.isIdentifier(secondParam)) {
contextName = ((secondParam: any): BabelNodeIdentifier).name;
}
@ -125,7 +143,7 @@ function visitClassMethodAstForThisUsage(realm: Realm, method: ECMAScriptSourceF
export function createSimpleClassInstance(
realm: Realm,
componentType: ECMAScriptSourceFunctionValue,
componentType: ECMAScriptSourceFunctionValue | BoundFunctionValue,
props: ObjectValue | AbstractValue,
context: ObjectValue | AbstractValue
): AbstractObjectValue {
@ -196,7 +214,7 @@ function deeplyApplyInstancePrototypeProperties(
export function createClassInstanceForFirstRenderOnly(
realm: Realm,
componentType: ECMAScriptSourceFunctionValue,
componentType: ECMAScriptSourceFunctionValue | BoundFunctionValue,
props: ObjectValue | AbstractValue,
context: ObjectValue | AbstractValue,
evaluatedNode: ReactEvaluatedNode
@ -257,7 +275,7 @@ export function createClassInstanceForFirstRenderOnly(
export function createClassInstance(
realm: Realm,
componentType: ECMAScriptSourceFunctionValue,
componentType: ECMAScriptSourceFunctionValue | BoundFunctionValue,
props: ObjectValue | AbstractValue,
context: ObjectValue | AbstractValue,
classMetadata: ClassComponentMetadata
@ -284,7 +302,7 @@ export function createClassInstance(
export function evaluateClassConstructor(
realm: Realm,
constructorFunc: ECMAScriptSourceFunctionValue,
constructorFunc: ECMAScriptSourceFunctionValue | BoundFunctionValue,
props: ObjectValue | AbstractValue | AbstractObjectValue,
context: ObjectValue | AbstractObjectValue
): { instanceProperties: Set<string>, instanceSymbols: Set<SymbolValue> } {

View File

@ -10,7 +10,7 @@
/* @flow strict-local */
import { type Effects, Realm } from "../realm.js";
import { AbstractValue, ECMAScriptSourceFunctionValue, ObjectValue } from "../values/index.js";
import { AbstractValue, ECMAScriptSourceFunctionValue, ObjectValue, BoundFunctionValue } from "../values/index.js";
import { createAdditionalEffects } from "../serializer/utils.js";
import {
convertFunctionalComponentToComplexClassComponent,
@ -33,7 +33,7 @@ import { Logger } from "../utils/logger.js";
function applyWriteEffectsForOptimizedComponent(
realm: Realm,
componentType: ECMAScriptSourceFunctionValue,
componentType: ECMAScriptSourceFunctionValue | BoundFunctionValue,
_effects: Effects,
componentTreeState: ComponentTreeState,
evaluatedNode: ReactEvaluatedNode,
@ -66,9 +66,17 @@ function applyWriteEffectsForOptimizedComponent(
if (componentTreeState.status === "SIMPLE") {
// if the root component was a class and is now simple, we can convert it from a class
// component to a functional component
convertSimpleClassComponentToFunctionalComponent(realm, componentType, additionalFunctionEffects);
normalizeFunctionalComponentParamaters(componentType);
writeEffects.set(componentType, additionalFunctionEffects);
if (componentType instanceof BoundFunctionValue) {
let targetFunction = componentType.$BoundTargetFunction;
invariant(targetFunction instanceof ECMAScriptSourceFunctionValue);
convertSimpleClassComponentToFunctionalComponent(realm, targetFunction, additionalFunctionEffects);
normalizeFunctionalComponentParamaters(targetFunction);
writeEffects.set(targetFunction, additionalFunctionEffects);
} else {
convertSimpleClassComponentToFunctionalComponent(realm, componentType, additionalFunctionEffects);
normalizeFunctionalComponentParamaters(componentType);
writeEffects.set(componentType, additionalFunctionEffects);
}
} else {
let prototype = Get(realm, componentType, "prototype");
invariant(prototype instanceof ObjectValue);
@ -90,8 +98,15 @@ function applyWriteEffectsForOptimizedComponent(
invariant(renderMethod instanceof ECMAScriptSourceFunctionValue);
writeEffects.set(renderMethod, additionalFunctionEffects);
} else {
normalizeFunctionalComponentParamaters(componentType);
writeEffects.set(componentType, additionalFunctionEffects);
if (componentType instanceof BoundFunctionValue) {
let targetFunction = componentType.$BoundTargetFunction;
invariant(targetFunction instanceof ECMAScriptSourceFunctionValue);
normalizeFunctionalComponentParamaters(targetFunction);
writeEffects.set(targetFunction, additionalFunctionEffects);
} else {
normalizeFunctionalComponentParamaters(componentType);
writeEffects.set(componentType, additionalFunctionEffects);
}
}
}
// apply contextTypes for legacy context
@ -111,7 +126,7 @@ function optimizeReactComponentTreeBranches(
writeEffects: WriteEffects,
environmentRecordIdAfterGlobalCode: number,
logger: Logger,
alreadyEvaluated: Map<ECMAScriptSourceFunctionValue, ReactEvaluatedNode>
alreadyEvaluated: Map<ECMAScriptSourceFunctionValue | BoundFunctionValue, ReactEvaluatedNode>
): void {
if (realm.react.verbose && reconciler.branchedComponentTrees.length > 0) {
logger.logInformation(` Evaluating React component tree branches...`);
@ -151,13 +166,13 @@ function optimizeReactComponentTreeBranches(
export function optimizeReactComponentTreeRoot(
realm: Realm,
componentRoot: ECMAScriptSourceFunctionValue | AbstractValue,
componentRoot: ECMAScriptSourceFunctionValue | BoundFunctionValue | AbstractValue,
config: ReactComponentTreeConfig,
writeEffects: WriteEffects,
environmentRecordIdAfterGlobalCode: number,
logger: Logger,
statistics: ReactStatistics,
alreadyEvaluated: Map<ECMAScriptSourceFunctionValue, ReactEvaluatedNode>
alreadyEvaluated: Map<ECMAScriptSourceFunctionValue | BoundFunctionValue, ReactEvaluatedNode>
): void {
let reconciler = new Reconciler(realm, config, alreadyEvaluated, statistics, logger);
let componentType = getComponentTypeFromRootValue(realm, componentRoot);

View File

@ -103,7 +103,7 @@ export type BranchReactComponentTree = {
};
export type ComponentTreeState = {
componentType: void | ECMAScriptSourceFunctionValue,
componentType: void | ECMAScriptSourceFunctionValue | BoundFunctionValue,
contextTypes: Set<string>,
deadEnds: number,
status: "SIMPLE" | "COMPLEX",
@ -143,7 +143,7 @@ export class Reconciler {
constructor(
realm: Realm,
componentTreeConfig: ReactComponentTreeConfig,
alreadyEvaluated: Map<ECMAScriptSourceFunctionValue, ReactEvaluatedNode>,
alreadyEvaluated: Map<ECMAScriptSourceFunctionValue | BoundFunctionValue, ReactEvaluatedNode>,
statistics: ReactStatistics,
logger?: Logger
) {
@ -160,13 +160,13 @@ export class Reconciler {
statistics: ReactStatistics;
logger: void | Logger;
componentTreeState: ComponentTreeState;
alreadyEvaluated: Map<ECMAScriptSourceFunctionValue, ReactEvaluatedNode>;
alreadyEvaluated: Map<ECMAScriptSourceFunctionValue | BoundFunctionValue, ReactEvaluatedNode>;
componentTreeConfig: ReactComponentTreeConfig;
currentEffectsStack: Array<Effects>;
branchedComponentTrees: Array<BranchReactComponentTree>;
resolveReactComponentTree(
componentType: ECMAScriptSourceFunctionValue,
componentType: ECMAScriptSourceFunctionValue | BoundFunctionValue,
props: ObjectValue | AbstractObjectValue | null,
context: ObjectValue | AbstractObjectValue | null,
evaluatedRootNode: ReactEvaluatedNode
@ -233,7 +233,7 @@ export class Reconciler {
}
_resolveComplexClassComponent(
componentType: ECMAScriptSourceFunctionValue,
componentType: ECMAScriptSourceFunctionValue | BoundFunctionValue,
props: ObjectValue | AbstractObjectValue,
context: ObjectValue | AbstractObjectValue,
classMetadata: ClassComponentMetadata,
@ -268,7 +268,7 @@ export class Reconciler {
}
_resolveSimpleClassComponent(
componentType: ECMAScriptSourceFunctionValue,
componentType: ECMAScriptSourceFunctionValue | BoundFunctionValue,
props: ObjectValue | AbstractObjectValue,
context: ObjectValue | AbstractObjectValue,
branchStatus: BranchStatusEnum,
@ -284,7 +284,7 @@ export class Reconciler {
}
_resolveFunctionalComponent(
componentType: ECMAScriptSourceFunctionValue,
componentType: ECMAScriptSourceFunctionValue | BoundFunctionValue,
props: ObjectValue | AbstractObjectValue,
context: ObjectValue | AbstractObjectValue,
evaluatedNode: ReactEvaluatedNode
@ -293,7 +293,7 @@ export class Reconciler {
}
_getClassComponentMetadata(
componentType: ECMAScriptSourceFunctionValue,
componentType: ECMAScriptSourceFunctionValue | BoundFunctionValue,
props: ObjectValue | AbstractObjectValue,
context: ObjectValue | AbstractObjectValue
): ClassComponentMetadata {
@ -487,7 +487,7 @@ export class Reconciler {
let evaluatedChildNode = createReactEvaluatedNode("FORWARD_REF", getComponentName(this.realm, forwardedComponent));
evaluatedNode.children.push(evaluatedChildNode);
invariant(
forwardedComponent instanceof ECMAScriptSourceFunctionValue,
forwardedComponent instanceof ECMAScriptSourceFunctionValue || forwardedComponent instanceof BoundFunctionValue,
"expect React.forwardRef() to be passed function value"
);
let value = getValueFromFunctionCall(this.realm, forwardedComponent, this.realm.intrinsics.undefined, [
@ -551,7 +551,7 @@ export class Reconciler {
}
_resolveClassComponent(
componentType: ECMAScriptSourceFunctionValue,
componentType: ECMAScriptSourceFunctionValue | BoundFunctionValue,
props: ObjectValue | AbstractObjectValue,
context: ObjectValue | AbstractObjectValue,
branchStatus: BranchStatusEnum,
@ -609,7 +609,7 @@ export class Reconciler {
}
_resolveClassComponentForFirstRenderOnly(
componentType: ECMAScriptSourceFunctionValue,
componentType: ECMAScriptSourceFunctionValue | BoundFunctionValue,
props: ObjectValue | AbstractObjectValue,
context: ObjectValue | AbstractObjectValue,
branchStatus: BranchStatusEnum,
@ -703,7 +703,7 @@ export class Reconciler {
evaluatedNode.message = "RelayContainer";
throw new NewComponentTreeBranch(evaluatedNode);
}
invariant(componentType instanceof ECMAScriptSourceFunctionValue);
invariant(componentType instanceof ECMAScriptSourceFunctionValue || componentType instanceof BoundFunctionValue);
let value;
let childContext = context;
@ -1140,7 +1140,11 @@ export class Reconciler {
switch (componentResolutionStrategy) {
case "NORMAL": {
if (
!(typeValue instanceof ECMAScriptSourceFunctionValue || valueIsKnownReactAbstraction(this.realm, typeValue))
!(
typeValue instanceof ECMAScriptSourceFunctionValue ||
typeValue instanceof BoundFunctionValue ||
valueIsKnownReactAbstraction(this.realm, typeValue)
)
) {
return this._resolveUnknownComponentType(componentType, reactElement, context, branchStatus, evaluatedNode);
}

View File

@ -438,11 +438,13 @@ const skipFunctionProperties = new Set(["length", "prototype", "arguments", "nam
export function convertFunctionalComponentToComplexClassComponent(
realm: Realm,
functionalComponentType: ECMAScriptSourceFunctionValue,
complexComponentType: void | ECMAScriptSourceFunctionValue,
functionalComponentType: ECMAScriptSourceFunctionValue | BoundFunctionValue,
complexComponentType: void | ECMAScriptSourceFunctionValue | BoundFunctionValue,
additionalFunctionEffects: AdditionalFunctionEffects
): void {
invariant(complexComponentType instanceof ECMAScriptSourceFunctionValue);
invariant(
complexComponentType instanceof ECMAScriptSourceFunctionValue || complexComponentType instanceof BoundFunctionValue
);
// get all properties on the functional component that were added in user-code
// we add defaultProps as undefined, as merging a class component's defaultProps on to
// a differnet component isn't right, we can discard defaultProps instead via folding
@ -524,9 +526,18 @@ export function createReactHintObject(
};
}
export function getComponentTypeFromRootValue(realm: Realm, value: Value): ECMAScriptSourceFunctionValue | null {
export function getComponentTypeFromRootValue(
realm: Realm,
value: Value
): ECMAScriptSourceFunctionValue | BoundFunctionValue | null {
let _valueIsKnownReactAbstraction = valueIsKnownReactAbstraction(realm, value);
if (!(value instanceof ECMAScriptSourceFunctionValue || _valueIsKnownReactAbstraction)) {
if (
!(
value instanceof ECMAScriptSourceFunctionValue ||
value instanceof BoundFunctionValue ||
_valueIsKnownReactAbstraction
)
) {
return null;
}
if (_valueIsKnownReactAbstraction) {
@ -542,7 +553,9 @@ export function getComponentTypeFromRootValue(realm: Realm, value: Value): ECMAS
invariant(Array.isArray(reactHint.args));
// componentType is the 1st argument of a ReactRelay container
let componentType = reactHint.args[0];
invariant(componentType instanceof ECMAScriptSourceFunctionValue);
invariant(
componentType instanceof ECMAScriptSourceFunctionValue || componentType instanceof BoundFunctionValue
);
return componentType;
default:
invariant(
@ -553,7 +566,7 @@ export function getComponentTypeFromRootValue(realm: Realm, value: Value): ECMAS
}
invariant(false, "unsupported known React abstraction");
} else {
invariant(value instanceof ECMAScriptSourceFunctionValue);
invariant(value instanceof ECMAScriptSourceFunctionValue || value instanceof BoundFunctionValue);
return value;
}
}

View File

@ -34,6 +34,7 @@ import {
AbstractObjectValue,
AbstractValue,
ArrayValue,
BoundFunctionValue,
ConcreteValue,
ECMAScriptSourceFunctionValue,
FunctionValue,
@ -396,7 +397,7 @@ export class Realm {
// we need to know what React component was passed to this AbstractObjectValue so we can visit it next)
abstractHints: WeakMap<AbstractValue | ObjectValue, ReactHint>,
activeReconciler: any, // inentionally "any", importing the React reconciler class increases Flow's cylic count
classComponentMetadata: Map<ECMAScriptSourceFunctionValue, ClassComponentMetadata>,
classComponentMetadata: Map<ECMAScriptSourceFunctionValue | BoundFunctionValue, ClassComponentMetadata>,
currentOwner?: ObjectValue,
defaultPropsHelper?: ECMAScriptSourceFunctionValue,
emptyArray: void | ArrayValue,

View File

@ -19,6 +19,7 @@ import { ignoreErrorsIn } from "../utils/errors.js";
import {
AbstractObjectValue,
AbstractValue,
BoundFunctionValue,
ECMAScriptSourceFunctionValue,
FunctionValue,
ObjectValue,
@ -87,6 +88,7 @@ export class Functions {
let validConfig = config instanceof ObjectValue || config === realm.intrinsics.undefined;
let validRootComponent =
rootComponent instanceof ECMAScriptSourceFunctionValue ||
rootComponent instanceof BoundFunctionValue ||
(rootComponent instanceof AbstractValue && valueIsKnownReactAbstraction(this.realm, rootComponent));
if (validConfig && validRootComponent) {

View File

@ -128,6 +128,14 @@ it("Simple 24", () => {
runTest(__dirname + "/FunctionalComponents/simple-24.js");
});
it("Bound type", () => {
runTest(__dirname + "/FunctionalComponents/bound-type.js");
});
it("Bound type 2", () => {
runTest(__dirname + "/FunctionalComponents/bound-type2.js");
});
it("Two roots", () => {
runTest(__dirname + "/FunctionalComponents/two-roots.js");
});

View File

@ -0,0 +1,28 @@
var React = require("react");
function Child(props) {
return (
<div>
{props.x}
{this.y}
</div>
);
}
var foo = { y: " world" };
const BoundChild = Child.bind(foo);
function App(props) {
return <BoundChild x={props.x} />;
}
App.getTrials = function(renderer, Root) {
renderer.update(<Root />);
return [["simple bound function", renderer.toJSON()]];
};
if (this.__optimizeReactComponentTree) {
__optimizeReactComponentTree(App);
}
module.exports = App;

View File

@ -0,0 +1,24 @@
var React = require("react");
function App(props) {
return (
<div>
{props.x}
{this.y}
</div>
);
}
var foo = { y: " world" };
const BoundApp = App.bind(foo);
BoundApp.getTrials = function(renderer, Root) {
renderer.update(<Root x={"Hello"} />);
return [["simple bound function", renderer.toJSON()]];
};
if (this.__optimizeReactComponentTree) {
__optimizeReactComponentTree(BoundApp);
}
module.exports = BoundApp;

View File

@ -536,6 +536,170 @@ ReactStatistics {
}
`;
exports[`Bound type 2: (JSX => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "bound anonymous",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Bound type 2: (JSX => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "bound anonymous",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Bound type 2: (createElement => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "bound anonymous",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Bound type 2: (createElement => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "bound anonymous",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Bound type: (JSX => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 2,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "bound anonymous",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 1,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Bound type: (JSX => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 2,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "bound anonymous",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 1,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Bound type: (createElement => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 2,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "bound anonymous",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 1,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Bound type: (createElement => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 2,
"evaluatedRootNodes": Array [
Object {
"children": Array [
Object {
"children": Array [],
"message": "",
"name": "bound anonymous",
"status": "INLINED",
},
],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 1,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Circular reference: (JSX => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,