Refactor of ReactElement React Props equivalence logic (#2138)

Summary:
Release notes: none

After studying the issues in https://github.com/facebook/prepack/pull/2137, I understood that we currently make some assumptions in terms of the ReactElement equivalence system that are incompatible with what we want from Prepack going forward. This PR aims at fixing those assumptions so they fall more into line with the changes https://github.com/facebook/prepack/pull/2137.

This PR makes the following changes:
- All React equivalence is now handled explicitly in the ReactSet/ReactElementSet/ReactPropsSet to ensure all the logic is located together as one and not sprinkled around the codebase.
- We use `hardModifyReactObjectPropertyBinding` to modify final objects that React controls rather than `Properties.Set`. This function bypasses the normal route of putting the property changes on the effects – thus making it so the property changes are permanent and cannot be reverted. Given the object is immutable, there should never be a case where the property may change anyway – we only change the property as part of the equivalence phase and during reconciliation where the React internals are at play. This is only safe for objects explicitly created by React and marked as final by React – no other object can go through this codepath. Any attempt to use this dangerous function on a non-final object will result in an invariant.
-  Various bug fixes were fixed that were found when #2137 was merged and existing tests failed.
- Removed all the logic around `makeNotFinal` and use `hardModifyReactObjectPropertyBinding` instead.
Closes https://github.com/facebook/prepack/pull/2138

Reviewed By: gaearon

Differential Revision: D8547558

Pulled By: trueadm

fbshipit-source-id: 192da9169947adadc41f43997dfbe0adb73cec3b
This commit is contained in:
Dominic Gannaway 2018-06-20 14:09:44 -07:00 committed by Facebook Github Bot
parent 958fa43d6c
commit 0cdafba476
12 changed files with 523 additions and 284 deletions

View File

@ -10,7 +10,7 @@
/* @flow strict-local */
import type { Realm } from "../../realm.js";
import { AbstractValue, NativeFunctionValue, StringValue, ObjectValue } from "../../values/index.js";
import { AbstractValue, ArrayValue, NativeFunctionValue, StringValue, ObjectValue } from "../../values/index.js";
import { createMockReact, createMockReactDOM, createMockReactDOMServer } from "./react-mocks.js";
import { createMockReactRelay } from "./relay-mocks.js";
import { createAbstract } from "../prepack/utils.js";
@ -26,6 +26,12 @@ export default function(realm: Realm): void {
if (realm.react.enabled) {
// Create it eagerly so it's created outside effect branches
realm.react.defaultPropsHelper = createDefaultPropsHelper(realm);
let emptyArray = new ArrayValue(realm);
emptyArray.makeFinal();
realm.react.emptyArray = emptyArray;
let emptyObject = new ObjectValue(realm, realm.intrinsics.ObjectPrototype);
emptyObject.makeFinal();
realm.react.emptyObject = emptyObject;
}
// module.exports support

View File

@ -13,17 +13,18 @@ import type { Realm } from "../../realm.js";
import { parseExpression } from "babylon";
import { ValuesDomain } from "../../domains/index.js";
import {
ObjectValue,
ECMAScriptSourceFunctionValue,
Value,
AbstractObjectValue,
AbstractValue,
ECMAScriptSourceFunctionValue,
FunctionValue,
NullValue,
NumberValue,
ObjectValue,
Value,
} from "../../values/index.js";
import { Environment } from "../../singletons.js";
import { createReactHintObject, getReactSymbol, isReactElement } from "../../react/utils.js";
import { createReactElement } from "../../react/elements.js";
import { cloneElement, createReactElement } from "../../react/elements.js";
import { Properties, Create, To } from "../../singletons.js";
import { renderToString } from "../../react/experimental-server-rendering/rendering.js";
import * as t from "babel-types";
@ -32,7 +33,7 @@ import { updateIntrinsicNames, addMockFunctionToObject } from "./utils.js";
// most of the code here was taken from https://github.com/facebook/react/blob/master/packages/react/src/ReactElement.js
let reactCode = `
function createReact(REACT_ELEMENT_TYPE, REACT_FRAGMENT_TYPE, REACT_PORTAL_TYPE, ReactCurrentOwner) {
function createReact(REACT_ELEMENT_TYPE, REACT_FRAGMENT_TYPE, REACT_PORTAL_TYPE, ReactCurrentOwner, create) {
function makeEmptyFunction(arg) {
return function() {
return arg;
@ -55,22 +56,6 @@ let reactCode = `
__source: true,
};
var ReactElement = function(type, key, ref, self, source, owner, props) {
return {
// This tag allow us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,
// Built-in properties that belong on the element
type: type,
key: key,
ref: ref,
props: props,
// Record the component responsible for creating this element.
_owner: owner,
};
};
function hasValidRef(config) {
return config.ref !== undefined;
}
@ -329,71 +314,6 @@ let reactCode = `
return result;
}
function cloneElement(element, config, children) {
var propName;
// Original props are copied
var props = Object.assign({}, element.props);
// Reserved names are extracted
var key = element.key;
var ref = element.ref;
// Self is preserved since the owner is preserved.
var self = element._self;
// Source is preserved since cloneElement is unlikely to be targeted by a
// transpiler, and the original source is probably a better indicator of the
// true owner.
var source = element._source;
// Owner will be preserved, unless ref is overridden
var owner = element._owner;
if (config != null) {
if (hasValidRef(config)) {
// Silently steal the ref from the parent.
ref = config.ref;
owner = ReactCurrentOwner.current;
}
if (hasValidKey(config)) {
key = '' + config.key;
}
// Remaining properties override existing props
var defaultProps;
if (element.type && element.type.defaultProps) {
defaultProps = element.type.defaultProps;
}
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
if (config[propName] === undefined && defaultProps !== undefined) {
// Resolve default props
props[propName] = defaultProps[propName];
} else {
props[propName] = config[propName];
}
}
}
}
// Children can be more than one argument, and those are transferred onto
// the newly allocated props object.
var childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
var childArray = new Array(childrenLength);
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
return ReactElement(element.type, key, ref, self, source, owner, props);
}
function isValidElement(object) {
return (
typeof object === 'object' &&
@ -446,7 +366,6 @@ let reactCode = `
Component,
PureComponent,
Fragment: REACT_FRAGMENT_TYPE,
cloneElement,
isValidElement,
version: "16.2.0",
PropTypes: ReactPropTypes,
@ -482,7 +401,6 @@ export function createMockReact(realm: Realm, reactRequireName: string): ObjectV
"PropTypes",
"Children",
"isValidElement",
"cloneElement",
{ name: "Component", updatePrototype: true },
{ name: "PureComponent", updatePrototype: true },
]);
@ -523,6 +441,42 @@ export function createMockReact(realm: Realm, reactRequireName: string): ObjectV
}
);
addMockFunctionToObject(
realm,
reactValue,
reactRequireName,
"cloneElement",
(context, [element, config, ...children]) => {
invariant(element instanceof ObjectValue);
// if config is undefined/null, use an empy object
if (config === realm.intrinsics.undefined || config === realm.intrinsics.null || config === undefined) {
config = realm.intrinsics.null;
}
if (config instanceof AbstractValue && !(config instanceof AbstractObjectValue)) {
config = To.ToObject(realm, config);
}
invariant(config instanceof ObjectValue || config instanceof AbstractObjectValue || config instanceof NullValue);
if (Array.isArray(children)) {
if (children.length === 0) {
children = undefined;
} else if (children.length === 1) {
children = children[0];
} else {
let array = Create.ArrayCreate(realm, 0);
let length = children.length;
for (let i = 0; i < length; i++) {
Create.CreateDataPropertyOrThrow(realm, array, "" + i, children[i]);
}
children = array;
children.makeFinal();
}
}
return cloneElement(realm, element, config, children);
}
);
addMockFunctionToObject(
realm,
reactValue,

View File

@ -10,171 +10,42 @@
/* @flow */
import { Realm } from "../realm.js";
import {
AbstractValue,
ArrayValue,
FunctionValue,
NumberValue,
ObjectValue,
StringValue,
SymbolValue,
Value,
} from "../values/index.js";
import { ObjectValue, Value } from "../values/index.js";
import invariant from "../invariant.js";
import { isReactElement, getProperty } from "./utils";
import { HashSet } from "../methods/index.js";
import { ReactEquivalenceSet } from "./ReactEquivalenceSet.js";
type ReactElementValueMapKey = Value | number | string;
type ReactElementValueMap = Map<ReactElementValueMapKey, ReactElementNode>;
type ReactElementKeyMapKey = string | number | SymbolValue;
type ReactElementKeyMap = Map<ReactElementKeyMapKey, ReactElementValueMap>;
type ReactElementNode = {
map: ReactElementKeyMap,
value: ObjectValue | ArrayValue | null,
};
// ReactElementSet keeps records around of the values
// of ReactElement/JSX nodes so we can return the same immutable values
// where possible, i.e. <div /> === <div />
//
// Rather than uses hashes, this class uses linked Maps to track equality of objects.
// It does this by recursively iterating through objects, by their properties/symbols and using
// each property key as a map, and then from that map, each value as a map. The value
// then links to the subsequent property/symbol in the object. This approach ensures insertion
// is maintained through all objects.
export default class ReactElementSet {
constructor(realm: Realm, equivalenceSet: HashSet<AbstractValue>) {
export class ReactElementSet {
constructor(realm: Realm, reactEquivalenceSet: ReactEquivalenceSet) {
this.realm = realm;
this.equivalenceSet = equivalenceSet;
this.reactElementRoot = new Map();
this.objectRoot = new Map();
this.arrayRoot = new Map();
this.emptyArray = new ArrayValue(realm);
this.emptyObject = new ObjectValue(realm, realm.intrinsics.ObjectPrototype);
this.reactEquivalenceSet = reactEquivalenceSet;
}
realm: Realm;
reactElementRoot: ReactElementKeyMap;
objectRoot: ReactElementKeyMap;
arrayRoot: ReactElementKeyMap;
equivalenceSet: HashSet<AbstractValue>;
emptyArray: ArrayValue;
emptyObject: ObjectValue;
_createNode(): ReactElementNode {
return {
map: new Map(),
value: null,
};
}
_getKey(key: ReactElementKeyMapKey, map: ReactElementKeyMap, visitedValues: Set<Value>): ReactElementValueMap {
if (!map.has(key)) {
map.set(key, new Map());
}
return ((map.get(key): any): ReactElementValueMap);
}
_getValue(val: ReactElementValueMapKey, map: ReactElementValueMap, visitedValues: Set<Value>): ReactElementNode {
if (val instanceof StringValue || val instanceof NumberValue) {
val = val.value;
} else if (val instanceof AbstractValue) {
val = this.equivalenceSet.add(val);
} else if (val instanceof ArrayValue) {
val = this._getArrayValue(val, visitedValues);
} else if (val instanceof ObjectValue && !(val instanceof FunctionValue)) {
val = this._getObjectValue(val, visitedValues);
}
if (!map.has(val)) {
map.set(val, this._createNode());
}
return ((map.get(val): any): ReactElementNode);
}
// for objects: [key/symbol] -> [key/symbol]... as nodes
_getObjectValue(object: ObjectValue, visitedValues: Set<Value>): ObjectValue {
if (visitedValues.has(object)) return object;
visitedValues.add(object);
if (isReactElement(object)) {
return this.add(object);
}
let currentMap = this.objectRoot;
let result;
for (let [propName] of object.properties) {
currentMap = this._getKey(propName, currentMap, visitedValues);
let prop = getProperty(this.realm, object, propName);
result = this._getValue(prop, currentMap, visitedValues);
currentMap = result.map;
}
for (let [symbol] of object.symbols) {
currentMap = this._getKey(symbol, currentMap, visitedValues);
let prop = getProperty(this.realm, object, symbol);
result = this._getValue(prop, currentMap, visitedValues);
currentMap = result.map;
}
if (result === undefined) {
return this.emptyObject;
}
if (result.value === null) {
result.value = object;
}
return result.value;
}
// for arrays: [0] -> [1] -> [2]... as nodes
_getArrayValue(array: ArrayValue, visitedValues: Set<Value>): ArrayValue {
if (visitedValues.has(array)) return array;
if (array.intrinsicName) return array;
visitedValues.add(array);
let lengthValue = getProperty(this.realm, array, "length");
invariant(lengthValue instanceof NumberValue);
let length = lengthValue.value;
let currentMap = this.arrayRoot;
let result;
for (let i = 0; i < length; i++) {
currentMap = this._getKey(i, currentMap, visitedValues);
let element = getProperty(this.realm, array, "" + i);
result = this._getValue(element, currentMap, visitedValues);
currentMap = result.map;
}
if (result === undefined) {
return this.emptyArray;
}
if (result.value === null) {
result.value = array;
}
return result.value;
}
reactEquivalenceSet: ReactEquivalenceSet;
add(reactElement: ObjectValue, visitedValues: Set<Value> | void): ObjectValue {
if (!visitedValues) visitedValues = new Set();
let currentMap = this.reactElementRoot;
let reactEquivalenceSet = this.reactEquivalenceSet;
let currentMap = reactEquivalenceSet.reactElementRoot;
// type
currentMap = this._getKey("type", currentMap, visitedValues);
let type = getProperty(this.realm, reactElement, "type");
let result = this._getValue(type, currentMap, visitedValues);
currentMap = reactEquivalenceSet.getKey("type", currentMap, visitedValues);
let type = reactEquivalenceSet.getEquivalentPropertyValue(reactElement, "type");
let result = reactEquivalenceSet.getValue(type, currentMap, visitedValues);
currentMap = result.map;
// key
currentMap = this._getKey("key", currentMap, visitedValues);
let key = getProperty(this.realm, reactElement, "key");
result = this._getValue(key, currentMap, visitedValues);
currentMap = reactEquivalenceSet.getKey("key", currentMap, visitedValues);
let key = reactEquivalenceSet.getEquivalentPropertyValue(reactElement, "key");
result = reactEquivalenceSet.getValue(key, currentMap, visitedValues);
currentMap = result.map;
// ref
currentMap = this._getKey("ref", currentMap, visitedValues);
let ref = getProperty(this.realm, reactElement, "ref");
result = this._getValue(ref, currentMap, visitedValues);
currentMap = reactEquivalenceSet.getKey("ref", currentMap, visitedValues);
let ref = reactEquivalenceSet.getEquivalentPropertyValue(reactElement, "ref");
result = reactEquivalenceSet.getValue(ref, currentMap, visitedValues);
currentMap = result.map;
// props
currentMap = this._getKey("props", currentMap, visitedValues);
let props = getProperty(this.realm, reactElement, "props");
result = this._getValue(props, currentMap, visitedValues);
currentMap = result.map;
currentMap = reactEquivalenceSet.getKey("props", currentMap, visitedValues);
let props = reactEquivalenceSet.getEquivalentPropertyValue(reactElement, "props");
result = reactEquivalenceSet.getValue(props, currentMap, visitedValues);
if (result.value === null) {
result.value = reactElement;

View File

@ -0,0 +1,195 @@
/**
* Copyright (c) 2017-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.
*/
/* @flow */
import { Realm } from "../realm.js";
import {
AbstractValue,
ArrayValue,
FunctionValue,
NumberValue,
ObjectValue,
StringValue,
SymbolValue,
Value,
} from "../values/index.js";
import invariant from "../invariant.js";
import { hardModifyReactObjectPropertyBinding, isReactElement, isReactPropsObject, getProperty } from "./utils";
import { ResidualReactElementVisitor } from "../serializer/ResidualReactElementVisitor.js";
export type ReactSetValueMapKey = Value | number | string;
export type ReactSetValueMap = Map<ReactSetValueMapKey, ReactSetNode>;
export type ReactSetKeyMapKey = string | number | Symbol | SymbolValue;
export type ReactSetKeyMap = Map<ReactSetKeyMapKey, ReactSetValueMap>;
export type ReactSetNode = {
map: ReactSetKeyMap,
value: ObjectValue | ArrayValue | null,
};
export const temporalAliasSymbol = Symbol("temporalAlias");
// ReactEquivalenceSet keeps records around of the values
// of ReactElement/JSX nodes so we can return the same immutable values
// where possible, i.e. <div /> === <div />
//
// Rather than uses hashes, this class uses linked Maps to track equality of objects.
// It does this by recursively iterating through objects, by their properties/symbols and using
// each property key as a map, and then from that map, each value as a map. The value
// then links to the subsequent property/symbol in the object. This approach ensures insertion
// is maintained through all objects.
export class ReactEquivalenceSet {
constructor(realm: Realm, residualReactElementVisitor: ResidualReactElementVisitor) {
this.realm = realm;
this.residualReactElementVisitor = residualReactElementVisitor;
this.objectRoot = new Map();
this.arrayRoot = new Map();
this.reactElementRoot = new Map();
this.reactPropsRoot = new Map();
}
realm: Realm;
objectRoot: ReactSetKeyMap;
arrayRoot: ReactSetKeyMap;
reactElementRoot: ReactSetKeyMap;
reactPropsRoot: ReactSetKeyMap;
residualReactElementVisitor: ResidualReactElementVisitor;
_createNode(): ReactSetNode {
return {
map: new Map(),
value: null,
};
}
getKey(key: ReactSetKeyMapKey, map: ReactSetKeyMap, visitedValues: Set<Value>): ReactSetValueMap {
if (!map.has(key)) {
map.set(key, new Map());
}
return ((map.get(key): any): ReactSetValueMap);
}
getValue(val: ReactSetValueMapKey, map: ReactSetValueMap, visitedValues: Set<Value>): ReactSetNode {
if (val instanceof StringValue || val instanceof NumberValue) {
val = val.value;
} else if (val instanceof AbstractValue) {
val = this.residualReactElementVisitor.residualHeapVisitor.equivalenceSet.add(val);
} else if (val instanceof ArrayValue) {
val = this._getArrayValue(val, visitedValues);
} else if (val instanceof ObjectValue && !(val instanceof FunctionValue)) {
val = this._getObjectValue(val, visitedValues);
}
if (!map.has(val)) {
map.set(val, this._createNode());
}
return ((map.get(val): any): ReactSetNode);
}
// for objects: [key/symbol] -> [key/symbol]... as nodes
_getObjectValue(object: ObjectValue, visitedValues: Set<Value>): ObjectValue {
if (visitedValues.has(object)) return object;
visitedValues.add(object);
if (isReactElement(object)) {
return this.residualReactElementVisitor.reactElementEquivalenceSet.add(object);
}
let currentMap = this.objectRoot;
let result;
for (let [propName] of object.properties) {
currentMap = this.getKey(propName, currentMap, visitedValues);
let prop = this.getEquivalentPropertyValue(object, propName);
result = this.getValue(prop, currentMap, visitedValues);
currentMap = result.map;
}
for (let [symbol] of object.symbols) {
currentMap = this.getKey(symbol, currentMap, visitedValues);
let prop = getProperty(this.realm, object, symbol);
result = this.getValue(prop, currentMap, visitedValues);
currentMap = result.map;
}
let temporalAlias = object.temporalAlias;
if (temporalAlias !== undefined) {
// Snapshotting uses temporalAlias to on ObjectValues, so if
// they have a temporalAlias then we need to treat it as a field
currentMap = this.getKey(temporalAliasSymbol, currentMap, visitedValues);
result = this.getValue(temporalAlias, currentMap, visitedValues);
}
if (result === undefined) {
// If we have a temporalAlias, we can never return an empty object
if (temporalAlias === undefined && this.realm.react.emptyObject !== undefined) {
return this.realm.react.emptyObject;
}
return object;
}
if (result.value === null) {
result.value = object;
}
return result.value;
}
// for arrays: [0] -> [1] -> [2]... as nodes
_getArrayValue(array: ArrayValue, visitedValues: Set<Value>): ArrayValue {
if (visitedValues.has(array)) return array;
if (array.intrinsicName) return array;
visitedValues.add(array);
let lengthValue = getProperty(this.realm, array, "length");
invariant(lengthValue instanceof NumberValue);
let length = lengthValue.value;
let currentMap = this.arrayRoot;
let result;
for (let i = 0; i < length; i++) {
currentMap = this.getKey(i, currentMap, visitedValues);
let element = this.getEquivalentPropertyValue(array, "" + i);
result = this.getValue(element, currentMap, visitedValues);
currentMap = result.map;
}
if (result === undefined) {
if (this.realm.react.emptyArray !== undefined) {
return this.realm.react.emptyArray;
}
return array;
}
if (result.value === null) {
result.value = array;
}
return result.value;
}
getEquivalentPropertyValue(object: ObjectValue, propName: string): Value {
let prop = getProperty(this.realm, object, propName);
let isFinal = object.mightBeFinalObject();
let equivalentProp;
if (prop instanceof ObjectValue && isReactElement(prop)) {
equivalentProp = this.residualReactElementVisitor.reactElementEquivalenceSet.add(prop);
if (prop !== equivalentProp && isFinal) {
hardModifyReactObjectPropertyBinding(this.realm, object, propName, equivalentProp);
}
} else if (prop instanceof ObjectValue && isReactPropsObject(prop)) {
equivalentProp = this.residualReactElementVisitor.reactPropsEquivalenceSet.add(prop);
if (prop !== equivalentProp && isFinal) {
hardModifyReactObjectPropertyBinding(this.realm, object, propName, equivalentProp);
}
} else if (prop instanceof AbstractValue) {
equivalentProp = this.residualReactElementVisitor.residualHeapVisitor.equivalenceSet.add(prop);
if (prop !== equivalentProp && isFinal) {
hardModifyReactObjectPropertyBinding(this.realm, object, propName, equivalentProp);
}
}
return equivalentProp || prop;
}
}

View File

@ -0,0 +1,59 @@
/**
* Copyright (c) 2017-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.
*/
/* @flow */
import { Realm } from "../realm.js";
import { ObjectValue, Value } from "../values/index.js";
import invariant from "../invariant.js";
import { ReactEquivalenceSet, temporalAliasSymbol } from "./ReactEquivalenceSet.js";
export class ReactPropsSet {
constructor(realm: Realm, reactEquivalenceSet: ReactEquivalenceSet) {
this.realm = realm;
this.reactEquivalenceSet = reactEquivalenceSet;
}
realm: Realm;
reactEquivalenceSet: ReactEquivalenceSet;
add(props: ObjectValue, visitedValues: Set<Value> | void): ObjectValue {
if (!visitedValues) visitedValues = new Set();
let reactEquivalenceSet = this.reactEquivalenceSet;
let currentMap = reactEquivalenceSet.reactPropsRoot;
let result;
for (let [propName] of props.properties) {
currentMap = reactEquivalenceSet.getKey(propName, currentMap, visitedValues);
let prop = reactEquivalenceSet.getEquivalentPropertyValue(props, propName);
result = reactEquivalenceSet.getValue(prop, currentMap, visitedValues);
currentMap = result.map;
}
let temporalAlias = props.temporalAlias;
if (temporalAlias !== undefined) {
// Snapshotting uses temporalAlias to on ObjectValues, so if
// they have a temporalAlias then we need to treat it as a field
currentMap = reactEquivalenceSet.getKey(temporalAliasSymbol, currentMap, visitedValues);
result = reactEquivalenceSet.getValue(temporalAlias, currentMap, visitedValues);
}
if (result === undefined) {
// If we have a temporalAlias, we can never return an empty object
if (temporalAlias === undefined && this.realm.react.emptyObject !== undefined) {
return this.realm.react.emptyObject;
}
return props;
}
if (result.value === null) {
result.value = props;
}
invariant(result.value instanceof ObjectValue);
return result.value;
}
}

View File

@ -152,7 +152,9 @@ function applyBranchedLogicValue(realm: Realm, value: Value): Value {
} else if (value instanceof ObjectValue && isReactElement(value)) {
return addKeyToReactElement(realm, value);
} else if (value instanceof ArrayValue) {
return mapArrayValue(realm, value, elementValue => applyBranchedLogicValue(realm, elementValue));
let newArray = mapArrayValue(realm, value, elementValue => applyBranchedLogicValue(realm, elementValue));
newArray.makeFinal();
return newArray;
} else if (value instanceof AbstractValue && value.kind === "conditional") {
let [condValue, consequentVal, alternateVal] = value.args;
invariant(condValue instanceof AbstractValue);

View File

@ -11,7 +11,15 @@
import type { Realm } from "../realm.js";
import { ValuesDomain } from "../domains/index.js";
import { AbstractValue, AbstractObjectValue, ArrayValue, NumberValue, ObjectValue, Value } from "../values/index.js";
import {
AbstractValue,
AbstractObjectValue,
ArrayValue,
NullValue,
NumberValue,
ObjectValue,
Value,
} from "../values/index.js";
import { Create, Properties } from "../singletons.js";
import invariant from "../invariant.js";
import { Get } from "../methods/index.js";
@ -19,6 +27,7 @@ import {
applyObjectAssignConfigsForReactElement,
createInternalReactElement,
flagPropsWithNoPartialKeyOrRef,
hardModifyReactObjectPropertyBinding,
getProperty,
hasNoPartialKeyOrRef,
} from "./utils.js";
@ -111,9 +120,10 @@ function createPropsObject(
props = Create.ObjectCreate(realm, realm.intrinsics.ObjectPrototype);
applyObjectAssignConfigsForReactElement(realm, props, args);
props.makeFinal();
if (children !== undefined) {
Properties.Set(realm, props, "children", children, true);
hardModifyReactObjectPropertyBinding(realm, props, "children", children);
}
// handle default props on a partial/abstract config
@ -135,7 +145,7 @@ function createPropsObject(
defaultPropsEvaluated++;
// if the value we have is undefined, we can apply the defaultProp
if (propBinding.descriptor && propBinding.descriptor.value === realm.intrinsics.undefined) {
Properties.Set(realm, props, propName, Get(realm, defaultProps, propName), true);
hardModifyReactObjectPropertyBinding(realm, props, propName, Get(realm, defaultProps, propName));
}
}
}
@ -154,7 +164,7 @@ function createPropsObject(
// exist
for (let [propName, binding] of props.properties) {
if (binding.descriptor !== undefined && binding.descriptor.value === realm.intrinsics.undefined) {
Properties.Set(realm, props, propName, AbstractValue.createFromType(realm, Value), true);
hardModifyReactObjectPropertyBinding(realm, props, propName, AbstractValue.createFromType(realm, Value));
}
}
// if we have children and they are abstract, they might be undefined at runtime
@ -168,7 +178,7 @@ function createPropsObject(
Get(realm, defaultProps, "children"),
children
);
Properties.Set(realm, props, "children", conditionalChildren, true);
hardModifyReactObjectPropertyBinding(realm, props, "children", conditionalChildren);
}
let defaultPropsHelper = realm.react.defaultPropsHelper;
invariant(defaultPropsHelper !== undefined);
@ -279,6 +289,88 @@ function splitReactElementsByConditionalConfig(
);
}
export function cloneElement(
realm: Realm,
reactElement: ObjectValue,
config: ObjectValue | AbstractObjectValue | NullValue,
children: void | Value
): ObjectValue {
let props = Create.ObjectCreate(realm, realm.intrinsics.ObjectPrototype);
const setProp = (name: string, value: Value): void => {
if (name !== "__self" && name !== "__source" && name !== "key" && name !== "ref") {
invariant(props instanceof ObjectValue);
hardModifyReactObjectPropertyBinding(realm, props, name, value);
}
};
applyObjectAssignConfigsForReactElement(realm, props, [config]);
props.makeFinal();
let key = getProperty(realm, reactElement, "key");
let ref = getProperty(realm, reactElement, "ref");
let type = getProperty(realm, reactElement, "type");
if (!(config instanceof NullValue)) {
let possibleKey = Get(realm, config, "key");
if (possibleKey !== realm.intrinsics.null && possibleKey !== realm.intrinsics.undefined) {
// if the config has been marked as having no partial key or ref and the possible key
// is abstract, yet the config doesn't have a key property, then the key can remain null
let keyNotNeeded =
hasNoPartialKeyOrRef(realm, config) &&
possibleKey instanceof AbstractValue &&
config instanceof ObjectValue &&
!config.properties.has("key");
if (!keyNotNeeded) {
key = computeBinary(realm, "+", realm.intrinsics.emptyString, possibleKey);
}
}
let possibleRef = Get(realm, config, "ref");
if (possibleRef !== realm.intrinsics.null && possibleRef !== realm.intrinsics.undefined) {
// if the config has been marked as having no partial key or ref and the possible ref
// is abstract, yet the config doesn't have a ref property, then the ref can remain null
let refNotNeeded =
hasNoPartialKeyOrRef(realm, config) &&
possibleRef instanceof AbstractValue &&
config instanceof ObjectValue &&
!config.properties.has("ref");
if (!refNotNeeded) {
ref = possibleRef;
}
}
let defaultProps =
type instanceof ObjectValue || type instanceof AbstractObjectValue
? Get(realm, type, "defaultProps")
: realm.intrinsics.undefined;
if (defaultProps instanceof ObjectValue) {
for (let [propKey, binding] of defaultProps.properties) {
if (binding && binding.descriptor && binding.descriptor.enumerable) {
if (Get(realm, props, propKey) === realm.intrinsics.undefined) {
setProp(propKey, Get(realm, defaultProps, propKey));
}
}
}
} else if (defaultProps instanceof AbstractObjectValue) {
invariant(false, "TODO: we need to eventually support this");
}
}
if (children !== undefined) {
hardModifyReactObjectPropertyBinding(realm, props, "children", children);
} else {
let elementProps = getProperty(realm, reactElement, "props");
invariant(elementProps instanceof ObjectValue);
let elementChildren = getProperty(realm, elementProps, "children");
hardModifyReactObjectPropertyBinding(realm, props, "children", elementChildren);
}
return createInternalReactElement(realm, type, key, ref, props);
}
export function createReactElement(
realm: Realm,
type: Value,
@ -297,6 +389,7 @@ export function createReactElement(
return splitReactElementsByConditionalConfig(realm, condValue, consequentVal, alternateVal, type, children);
}
let { key, props, ref } = createPropsObject(realm, type, config, children);
realm.react.reactProps.add(props);
return createInternalReactElement(realm, type, key, ref, props);
}
@ -306,7 +399,7 @@ type ElementTraversalVisitor = {
visitRef: (keyValue: Value) => void,
visitAbstractOrPartialProps: (propsValue: AbstractValue | ObjectValue) => void,
visitConcreteProps: (propsValue: ObjectValue) => void,
visitChildNode: (childValue: Value) => void | Value,
visitChildNode: (childValue: Value) => void,
};
export function traverseReactElement(
@ -340,35 +433,11 @@ export function traverseReactElement(
childrenLengthValue = childrenLength.value;
for (let i = 0; i < childrenLengthValue; i++) {
let child = getProperty(realm, childrenValue, "" + i);
invariant(
child instanceof Value,
`ReactElement "props.children[${i}]" failed to visit due to a non-value`
);
let equivalentChild = traversalVisitor.visitChildNode(child);
// If the visitor returns an equivalentChild that is of different
// value then we should update our ReactElement children with the
// new value. This is because we've already visisted the same
// child before (ReactElement or abstract value)
if (equivalentChild !== undefined && equivalentChild !== child) {
Properties.Set(realm, childrenValue, "" + i, equivalentChild, true);
}
traversalVisitor.visitChildNode(child);
}
}
} else {
let equivalentChildren = traversalVisitor.visitChildNode(childrenValue);
// If the visitor returns an equivalentChild that is of different
// value then we should update our ReactElement children with the
// new value. This is because we've already visisted the same
// child before (ReactElement or abstract value)
if (equivalentChildren !== undefined && equivalentChildren !== childrenValue) {
// "props" are immutable objects, but need to mutate them in this instance
// so we need to temporarily make them not final so we can do so. Given
// we're in the visitor/serialization stage, this shouldn't have any
// affect on the application, it's simply to improve optimization
propsValue.makeNotFinal();
Properties.Set(realm, propsValue, "children", equivalentChildren, true);
propsValue.makeFinal();
}
traversalVisitor.visitChildNode(childrenValue);
}
}
}

View File

@ -34,6 +34,7 @@ import {
doNotOptimizeComponent,
evaluateWithNestedParentEffects,
flattenChildren,
hardModifyReactObjectPropertyBinding,
getComponentName,
getComponentTypeFromRootValue,
getLocationFromValue,
@ -1082,9 +1083,11 @@ export class Reconciler {
if (resolvedChildren !== childrenValue) {
let newProps = cloneProps(this.realm, propsValue, resolvedChildren);
reactElement.makeNotFinal();
Properties.Set(this.realm, reactElement, "props", newProps, true);
reactElement.makeFinal();
// This is safe to do as we clone a new ReactElement as part of reconcilation
// so we will never be mutating an object used by something else. Furthermore,
// the ReactElement is "immutable" so it can never change and only React controls
// this object.
hardModifyReactObjectPropertyBinding(this.realm, reactElement, "props", newProps);
}
}
}
@ -1415,9 +1418,11 @@ export class Reconciler {
}
return arrayValue;
}
return mapArrayValue(this.realm, arrayValue, elementValue =>
let children = mapArrayValue(this.realm, arrayValue, elementValue =>
this._resolveDeeply(componentType, elementValue, context, "NEW_BRANCH", evaluatedNode)
);
children.makeFinal();
return children;
}
hasEvaluatedRootNode(componentType: ECMAScriptSourceFunctionValue, evaluateNode: ReactEvaluatedNode): boolean {

View File

@ -91,6 +91,20 @@ export function isReactElement(val: Value): boolean {
return false;
}
export function isReactPropsObject(val: Value): boolean {
if (!(val instanceof ObjectValue)) {
return false;
}
let realm = val.$Realm;
if (!realm.react.enabled) {
return false;
}
if (realm.react.reactProps.has(val)) {
return true;
}
return false;
}
export function getReactSymbol(symbolKey: ReactSymbolTypes, realm: Realm): SymbolValue {
let reactSymbol = realm.react.symbols.get(symbolKey);
if (reactSymbol !== undefined) {
@ -595,6 +609,7 @@ function recursivelyFlattenArray(realm: Realm, array, targetArray): void {
export function flattenChildren(realm: Realm, array: ArrayValue): ArrayValue {
let flattenedChildren = Create.ArrayCreate(realm, 0);
recursivelyFlattenArray(realm, array, flattenedChildren);
flattenedChildren.makeFinal();
return flattenedChildren;
}
@ -980,6 +995,7 @@ export function cloneProps(realm: Realm, props: ObjectValue, newChildren?: Value
applyClonedTemporalAlias(realm, props, clonedProps);
}
clonedProps.makeFinal();
realm.react.reactProps.add(clonedProps);
return clonedProps;
}
@ -1119,3 +1135,43 @@ export function cloneReactElement(realm: Realm, reactElement: ObjectValue, shoul
}
return createInternalReactElement(realm, typeValue, keyValue, refValue, propsValue);
}
// This function changes an object's property value by changing it's binding
// and descriptor, thus bypassing the binding detection system. This is a
// dangerous function and should only be used on objects created by React.
// It's primary use is to update ReactElement / React props properties
// during the visitor equivalence stage as an optimization feature.
// It will invariant if used on objects that are not final.
export function hardModifyReactObjectPropertyBinding(
realm: Realm,
object: ObjectValue,
propName: string,
value: Value
): void {
invariant(
object.mightBeFinalObject() && !object.mightNotBeFinalObject(),
"hardModifyReactObjectPropertyBinding can only be used on final objects!"
);
let binding = object.properties.get(propName);
if (binding === undefined) {
binding = {
object,
descriptor: {
configurable: true,
enumerable: true,
value: undefined,
writable: true,
},
key: propName,
};
}
let descriptor = binding.descriptor;
invariant(descriptor !== undefined);
let newDescriptor = Object.assign({}, descriptor, {
value,
});
let newBinding = Object.assign({}, binding, {
descriptor: newDescriptor,
});
object.properties.set(propName, newBinding);
}

View File

@ -257,6 +257,8 @@ export class Realm {
classComponentMetadata: new Map(),
currentOwner: undefined,
defaultPropsHelper: undefined,
emptyArray: undefined,
emptyObject: undefined,
enabled: opts.reactEnabled || false,
hoistableFunctions: new WeakMap(),
hoistableReactElements: new WeakMap(),
@ -266,6 +268,7 @@ export class Realm {
output: opts.reactOutput || "create-element",
propsWithNoPartialKeyOrRef: new WeakSet(),
reactElements: new WeakMap(),
reactProps: new WeakSet(),
symbols: new Map(),
usedReactElementKeys: new Set(),
verbose: opts.reactVerbose || false,
@ -352,6 +355,8 @@ export class Realm {
classComponentMetadata: Map<ECMAScriptSourceFunctionValue, ClassComponentMetadata>,
currentOwner?: ObjectValue,
defaultPropsHelper?: ECMAScriptSourceFunctionValue,
emptyArray: void | ArrayValue,
emptyObject: void | ObjectValue,
enabled: boolean,
hoistableFunctions: WeakMap<FunctionValue, boolean>,
hoistableReactElements: WeakMap<ObjectValue, boolean>,
@ -364,6 +369,7 @@ export class Realm {
output?: ReactOutputTypes,
propsWithNoPartialKeyOrRef: WeakSet<ObjectValue | AbstractObjectValue>,
reactElements: WeakMap<ObjectValue, { createdDuringReconcilation: boolean, firstRenderOnly: boolean }>,
reactProps: WeakSet<ObjectValue>,
symbols: Map<ReactSymbolTypes, SymbolValue>,
usedReactElementKeys: Set<string>,
verbose: boolean,

View File

@ -62,7 +62,7 @@ import {
withDescriptorValue,
} from "./utils.js";
import { Environment, To } from "../singletons.js";
import { isReactElement, valueIsReactLibraryObject } from "../react/utils.js";
import { isReactElement, isReactPropsObject, valueIsReactLibraryObject } from "../react/utils.js";
import { ResidualReactElementVisitor } from "./ResidualReactElementVisitor.js";
import { GeneratorDAG } from "./GeneratorDAG.js";
@ -999,9 +999,13 @@ export class ResidualHeapVisitor {
if (val.temporalAlias !== undefined) {
return this.visitEquivalentValue(val.temporalAlias);
}
let equivalentReactElementValue = this.residualReactElementVisitor.equivalenceSet.add(val);
let equivalentReactElementValue = this.residualReactElementVisitor.reactElementEquivalenceSet.add(val);
if (this._mark(equivalentReactElementValue)) this.visitValueObject(equivalentReactElementValue);
return (equivalentReactElementValue: any);
} else if (val instanceof ObjectValue && isReactPropsObject(val)) {
let equivalentReactPropsValue = this.residualReactElementVisitor.reactPropsEquivalenceSet.add(val);
if (this._mark(equivalentReactPropsValue)) this.visitValueObject(equivalentReactPropsValue);
return (equivalentReactPropsValue: any);
}
this.visitValue(val);
return val;

View File

@ -16,7 +16,9 @@ import { determineIfReactElementCanBeHoisted } from "../react/hoisting.js";
import { traverseReactElement } from "../react/elements.js";
import { canExcludeReactElementObjectProperty, getProperty, getReactSymbol } from "../react/utils.js";
import invariant from "../invariant.js";
import ReactElementSet from "../react/ReactElementSet.js";
import { ReactEquivalenceSet } from "../react/ReactEquivalenceSet.js";
import { ReactElementSet } from "../react/ReactElementSet.js";
import { ReactPropsSet } from "../react/ReactPropsSet.js";
import type { ReactOutputTypes } from "../options.js";
export class ResidualReactElementVisitor {
@ -25,14 +27,18 @@ export class ResidualReactElementVisitor {
this.residualHeapVisitor = residualHeapVisitor;
this.reactOutput = realm.react.output || "create-element";
this.someReactElement = undefined;
this.equivalenceSet = new ReactElementSet(realm, residualHeapVisitor.equivalenceSet);
this.reactEquivalenceSet = new ReactEquivalenceSet(realm, this);
this.reactElementEquivalenceSet = new ReactElementSet(realm, this.reactEquivalenceSet);
this.reactPropsEquivalenceSet = new ReactPropsSet(realm, this.reactEquivalenceSet);
}
realm: Realm;
residualHeapVisitor: ResidualHeapVisitor;
reactOutput: ReactOutputTypes;
someReactElement: void | ObjectValue;
equivalenceSet: ReactElementSet;
reactEquivalenceSet: ReactEquivalenceSet;
reactElementEquivalenceSet: ReactElementSet;
reactPropsEquivalenceSet: ReactPropsSet;
visitReactElement(reactElement: ObjectValue): void {
let reactElementData = this.realm.react.reactElements.get(reactElement);
@ -74,7 +80,7 @@ export class ResidualReactElementVisitor {
}
},
visitChildNode: (childValue: Value) => {
return this.residualHeapVisitor.visitEquivalentValue(childValue);
this.residualHeapVisitor.visitValue(childValue);
},
});
@ -86,10 +92,16 @@ export class ResidualReactElementVisitor {
}
withCleanEquivalenceSet(func: () => void) {
let oldReactElementEquivalenceSet = this.equivalenceSet;
this.equivalenceSet = new ReactElementSet(this.realm, this.residualHeapVisitor.equivalenceSet);
let reactEquivalenceSet = this.reactEquivalenceSet;
let reactElementEquivalenceSet = this.reactElementEquivalenceSet;
let reactPropsEquivalenceSet = this.reactPropsEquivalenceSet;
this.reactEquivalenceSet = new ReactEquivalenceSet(this.realm, this);
this.reactElementEquivalenceSet = new ReactElementSet(this.realm, this.reactEquivalenceSet);
this.reactPropsEquivalenceSet = new ReactPropsSet(this.realm, this.reactEquivalenceSet);
func();
// Cleanup
this.equivalenceSet = oldReactElementEquivalenceSet;
this.reactEquivalenceSet = reactEquivalenceSet;
this.reactElementEquivalenceSet = reactElementEquivalenceSet;
this.reactPropsEquivalenceSet = reactPropsEquivalenceSet;
}
}