mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
19092ac8c3
* Re-add old Fabric Offscreen impl behind flag There's a chance that #21960 will affect layout in a way that we don't expect, so I'm adding back the old implementation so we can toggle the feature with a flag. The flag should read from the ReactNativeFeatureFlags shim so that we can change it at runtime. I'll do that separately. * Import dynamic RN flags from external module Internal feature flags that we wish to control with a GK can now be imported from an external module, which I've called "ReactNativeInternalFeatureFlags". We'll need to add this module to the downstream repo. We can't yet use this in our tests, because we don't have a test configuration that runs against the React Native feature flags fork. We should set up that up the same way we did for www.
1244 lines
35 KiB
JavaScript
1244 lines
35 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
/**
|
|
* This is a renderer of React that doesn't have a render target output.
|
|
* It is useful to demonstrate the internals of the reconciler in isolation
|
|
* and for testing semantics of reconciliation separate from the host
|
|
* environment.
|
|
*/
|
|
|
|
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
|
|
import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue';
|
|
import type {ReactNodeList, OffscreenMode} from 'shared/ReactTypes';
|
|
import type {RootTag} from 'react-reconciler/src/ReactRootTags';
|
|
|
|
import * as Scheduler from 'scheduler/unstable_mock';
|
|
import {REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
|
|
import isArray from 'shared/isArray';
|
|
import {
|
|
DefaultEventPriority,
|
|
IdleEventPriority,
|
|
ConcurrentRoot,
|
|
LegacyRoot,
|
|
} from 'react-reconciler/constants';
|
|
|
|
type Container = {
|
|
rootID: string,
|
|
children: Array<Instance | TextInstance>,
|
|
pendingChildren: Array<Instance | TextInstance>,
|
|
...
|
|
};
|
|
type Props = {
|
|
prop: any,
|
|
hidden: boolean,
|
|
children?: mixed,
|
|
bottom?: null | number,
|
|
left?: null | number,
|
|
right?: null | number,
|
|
top?: null | number,
|
|
...
|
|
};
|
|
type Instance = {|
|
|
type: string,
|
|
id: number,
|
|
parent: number,
|
|
children: Array<Instance | TextInstance>,
|
|
text: string | null,
|
|
prop: any,
|
|
hidden: boolean,
|
|
context: HostContext,
|
|
|};
|
|
type TextInstance = {|
|
|
text: string,
|
|
id: number,
|
|
parent: number,
|
|
hidden: boolean,
|
|
context: HostContext,
|
|
|};
|
|
type HostContext = Object;
|
|
|
|
const NO_CONTEXT = {};
|
|
const UPPERCASE_CONTEXT = {};
|
|
const UPDATE_SIGNAL = {};
|
|
if (__DEV__) {
|
|
Object.freeze(NO_CONTEXT);
|
|
Object.freeze(UPDATE_SIGNAL);
|
|
}
|
|
|
|
function createReactNoop(reconciler: Function, useMutation: boolean) {
|
|
let instanceCounter = 0;
|
|
let hostDiffCounter = 0;
|
|
let hostUpdateCounter = 0;
|
|
let hostCloneCounter = 0;
|
|
|
|
function appendChildToContainerOrInstance(
|
|
parentInstance: Container | Instance,
|
|
child: Instance | TextInstance,
|
|
): void {
|
|
const prevParent = child.parent;
|
|
if (prevParent !== -1 && prevParent !== parentInstance.id) {
|
|
throw new Error('Reparenting is not allowed');
|
|
}
|
|
child.parent = parentInstance.id;
|
|
const index = parentInstance.children.indexOf(child);
|
|
if (index !== -1) {
|
|
parentInstance.children.splice(index, 1);
|
|
}
|
|
parentInstance.children.push(child);
|
|
}
|
|
|
|
function appendChildToContainer(
|
|
parentInstance: Container,
|
|
child: Instance | TextInstance,
|
|
): void {
|
|
if (typeof parentInstance.rootID !== 'string') {
|
|
// Some calls to this aren't typesafe.
|
|
// This helps surface mistakes in tests.
|
|
throw new Error(
|
|
'appendChildToContainer() first argument is not a container.',
|
|
);
|
|
}
|
|
appendChildToContainerOrInstance(parentInstance, child);
|
|
}
|
|
|
|
function appendChild(
|
|
parentInstance: Instance,
|
|
child: Instance | TextInstance,
|
|
): void {
|
|
if (typeof (parentInstance: any).rootID === 'string') {
|
|
// Some calls to this aren't typesafe.
|
|
// This helps surface mistakes in tests.
|
|
throw new Error('appendChild() first argument is not an instance.');
|
|
}
|
|
appendChildToContainerOrInstance(parentInstance, child);
|
|
}
|
|
|
|
function insertInContainerOrInstanceBefore(
|
|
parentInstance: Container | Instance,
|
|
child: Instance | TextInstance,
|
|
beforeChild: Instance | TextInstance,
|
|
): void {
|
|
const index = parentInstance.children.indexOf(child);
|
|
if (index !== -1) {
|
|
parentInstance.children.splice(index, 1);
|
|
}
|
|
const beforeIndex = parentInstance.children.indexOf(beforeChild);
|
|
if (beforeIndex === -1) {
|
|
throw new Error('This child does not exist.');
|
|
}
|
|
parentInstance.children.splice(beforeIndex, 0, child);
|
|
}
|
|
|
|
function insertInContainerBefore(
|
|
parentInstance: Container,
|
|
child: Instance | TextInstance,
|
|
beforeChild: Instance | TextInstance,
|
|
) {
|
|
if (typeof parentInstance.rootID !== 'string') {
|
|
// Some calls to this aren't typesafe.
|
|
// This helps surface mistakes in tests.
|
|
throw new Error(
|
|
'insertInContainerBefore() first argument is not a container.',
|
|
);
|
|
}
|
|
insertInContainerOrInstanceBefore(parentInstance, child, beforeChild);
|
|
}
|
|
|
|
function insertBefore(
|
|
parentInstance: Instance,
|
|
child: Instance | TextInstance,
|
|
beforeChild: Instance | TextInstance,
|
|
) {
|
|
if (typeof (parentInstance: any).rootID === 'string') {
|
|
// Some calls to this aren't typesafe.
|
|
// This helps surface mistakes in tests.
|
|
throw new Error('insertBefore() first argument is not an instance.');
|
|
}
|
|
insertInContainerOrInstanceBefore(parentInstance, child, beforeChild);
|
|
}
|
|
|
|
function clearContainer(container: Container): void {
|
|
container.children.splice(0);
|
|
}
|
|
|
|
function removeChildFromContainerOrInstance(
|
|
parentInstance: Container | Instance,
|
|
child: Instance | TextInstance,
|
|
): void {
|
|
const index = parentInstance.children.indexOf(child);
|
|
if (index === -1) {
|
|
throw new Error('This child does not exist.');
|
|
}
|
|
parentInstance.children.splice(index, 1);
|
|
}
|
|
|
|
function removeChildFromContainer(
|
|
parentInstance: Container,
|
|
child: Instance | TextInstance,
|
|
): void {
|
|
if (typeof parentInstance.rootID !== 'string') {
|
|
// Some calls to this aren't typesafe.
|
|
// This helps surface mistakes in tests.
|
|
throw new Error(
|
|
'removeChildFromContainer() first argument is not a container.',
|
|
);
|
|
}
|
|
removeChildFromContainerOrInstance(parentInstance, child);
|
|
}
|
|
|
|
function removeChild(
|
|
parentInstance: Instance,
|
|
child: Instance | TextInstance,
|
|
): void {
|
|
if (typeof (parentInstance: any).rootID === 'string') {
|
|
// Some calls to this aren't typesafe.
|
|
// This helps surface mistakes in tests.
|
|
throw new Error('removeChild() first argument is not an instance.');
|
|
}
|
|
removeChildFromContainerOrInstance(parentInstance, child);
|
|
}
|
|
|
|
function cloneInstance(
|
|
instance: Instance,
|
|
updatePayload: null | Object,
|
|
type: string,
|
|
oldProps: Props,
|
|
newProps: Props,
|
|
internalInstanceHandle: Object,
|
|
keepChildren: boolean,
|
|
recyclableInstance: null | Instance,
|
|
): Instance {
|
|
const clone = {
|
|
id: instance.id,
|
|
type: type,
|
|
parent: instance.parent,
|
|
children: keepChildren ? instance.children : [],
|
|
text: shouldSetTextContent(type, newProps)
|
|
? computeText((newProps.children: any) + '', instance.context)
|
|
: null,
|
|
prop: newProps.prop,
|
|
hidden: !!newProps.hidden,
|
|
context: instance.context,
|
|
};
|
|
Object.defineProperty(clone, 'id', {
|
|
value: clone.id,
|
|
enumerable: false,
|
|
});
|
|
Object.defineProperty(clone, 'parent', {
|
|
value: clone.parent,
|
|
enumerable: false,
|
|
});
|
|
Object.defineProperty(clone, 'text', {
|
|
value: clone.text,
|
|
enumerable: false,
|
|
});
|
|
Object.defineProperty(clone, 'context', {
|
|
value: clone.context,
|
|
enumerable: false,
|
|
});
|
|
hostCloneCounter++;
|
|
return clone;
|
|
}
|
|
|
|
function shouldSetTextContent(type: string, props: Props): boolean {
|
|
if (type === 'errorInBeginPhase') {
|
|
throw new Error('Error in host config.');
|
|
}
|
|
return (
|
|
typeof props.children === 'string' || typeof props.children === 'number'
|
|
);
|
|
}
|
|
|
|
function computeText(rawText, hostContext) {
|
|
return hostContext === UPPERCASE_CONTEXT ? rawText.toUpperCase() : rawText;
|
|
}
|
|
|
|
const sharedHostConfig = {
|
|
getRootHostContext() {
|
|
return NO_CONTEXT;
|
|
},
|
|
|
|
getChildHostContext(
|
|
parentHostContext: HostContext,
|
|
type: string,
|
|
rootcontainerInstance: Container,
|
|
) {
|
|
if (type === 'offscreen') {
|
|
return parentHostContext;
|
|
}
|
|
if (type === 'uppercase') {
|
|
return UPPERCASE_CONTEXT;
|
|
}
|
|
return NO_CONTEXT;
|
|
},
|
|
|
|
getPublicInstance(instance) {
|
|
return instance;
|
|
},
|
|
|
|
createInstance(
|
|
type: string,
|
|
props: Props,
|
|
rootContainerInstance: Container,
|
|
hostContext: HostContext,
|
|
internalInstanceHandle: Object,
|
|
): Instance {
|
|
if (type === 'errorInCompletePhase') {
|
|
throw new Error('Error in host config.');
|
|
}
|
|
const inst = {
|
|
id: instanceCounter++,
|
|
type: type,
|
|
children: [],
|
|
parent: -1,
|
|
text: shouldSetTextContent(type, props)
|
|
? computeText((props.children: any) + '', hostContext)
|
|
: null,
|
|
prop: props.prop,
|
|
hidden: !!props.hidden,
|
|
context: hostContext,
|
|
};
|
|
// Hide from unit tests
|
|
Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false});
|
|
Object.defineProperty(inst, 'parent', {
|
|
value: inst.parent,
|
|
enumerable: false,
|
|
});
|
|
Object.defineProperty(inst, 'text', {
|
|
value: inst.text,
|
|
enumerable: false,
|
|
});
|
|
Object.defineProperty(inst, 'context', {
|
|
value: inst.context,
|
|
enumerable: false,
|
|
});
|
|
Object.defineProperty(inst, 'fiber', {
|
|
value: internalInstanceHandle,
|
|
enumerable: false,
|
|
});
|
|
return inst;
|
|
},
|
|
|
|
appendInitialChild(
|
|
parentInstance: Instance,
|
|
child: Instance | TextInstance,
|
|
): void {
|
|
const prevParent = child.parent;
|
|
if (prevParent !== -1 && prevParent !== parentInstance.id) {
|
|
throw new Error('Reparenting is not allowed');
|
|
}
|
|
child.parent = parentInstance.id;
|
|
parentInstance.children.push(child);
|
|
},
|
|
|
|
finalizeInitialChildren(
|
|
domElement: Instance,
|
|
type: string,
|
|
props: Props,
|
|
): boolean {
|
|
return false;
|
|
},
|
|
|
|
prepareUpdate(
|
|
instance: Instance,
|
|
type: string,
|
|
oldProps: Props,
|
|
newProps: Props,
|
|
): null | {...} {
|
|
if (type === 'errorInCompletePhase') {
|
|
throw new Error('Error in host config.');
|
|
}
|
|
if (oldProps === null) {
|
|
throw new Error('Should have old props');
|
|
}
|
|
if (newProps === null) {
|
|
throw new Error('Should have new props');
|
|
}
|
|
hostDiffCounter++;
|
|
return UPDATE_SIGNAL;
|
|
},
|
|
|
|
shouldSetTextContent,
|
|
|
|
createTextInstance(
|
|
text: string,
|
|
rootContainerInstance: Container,
|
|
hostContext: Object,
|
|
internalInstanceHandle: Object,
|
|
): TextInstance {
|
|
if (hostContext === UPPERCASE_CONTEXT) {
|
|
text = text.toUpperCase();
|
|
}
|
|
const inst = {
|
|
text: text,
|
|
id: instanceCounter++,
|
|
parent: -1,
|
|
hidden: false,
|
|
context: hostContext,
|
|
};
|
|
// Hide from unit tests
|
|
Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false});
|
|
Object.defineProperty(inst, 'parent', {
|
|
value: inst.parent,
|
|
enumerable: false,
|
|
});
|
|
Object.defineProperty(inst, 'context', {
|
|
value: inst.context,
|
|
enumerable: false,
|
|
});
|
|
return inst;
|
|
},
|
|
|
|
scheduleTimeout: setTimeout,
|
|
cancelTimeout: clearTimeout,
|
|
noTimeout: -1,
|
|
|
|
supportsMicrotasks: true,
|
|
scheduleMicrotask:
|
|
typeof queueMicrotask === 'function'
|
|
? queueMicrotask
|
|
: typeof Promise !== 'undefined'
|
|
? callback =>
|
|
Promise.resolve(null)
|
|
.then(callback)
|
|
.catch(error => {
|
|
setTimeout(() => {
|
|
throw error;
|
|
});
|
|
})
|
|
: setTimeout,
|
|
|
|
prepareForCommit(): null | Object {
|
|
return null;
|
|
},
|
|
|
|
resetAfterCommit(): void {},
|
|
|
|
getCurrentEventPriority() {
|
|
return currentEventPriority;
|
|
},
|
|
|
|
now: Scheduler.unstable_now,
|
|
|
|
isPrimaryRenderer: true,
|
|
warnsIfNotActing: true,
|
|
supportsHydration: false,
|
|
|
|
getInstanceFromNode() {
|
|
throw new Error('Not yet implemented.');
|
|
},
|
|
|
|
beforeActiveInstanceBlur() {
|
|
// NO-OP
|
|
},
|
|
|
|
afterActiveInstanceBlur() {
|
|
// NO-OP
|
|
},
|
|
|
|
preparePortalMount() {
|
|
// NO-OP
|
|
},
|
|
|
|
prepareScopeUpdate() {},
|
|
|
|
getInstanceFromScope() {
|
|
throw new Error('Not yet implemented.');
|
|
},
|
|
|
|
detachDeletedInstance() {},
|
|
};
|
|
|
|
const hostConfig = useMutation
|
|
? {
|
|
...sharedHostConfig,
|
|
|
|
supportsMutation: true,
|
|
supportsPersistence: false,
|
|
|
|
commitMount(instance: Instance, type: string, newProps: Props): void {
|
|
// Noop
|
|
},
|
|
|
|
commitUpdate(
|
|
instance: Instance,
|
|
updatePayload: Object,
|
|
type: string,
|
|
oldProps: Props,
|
|
newProps: Props,
|
|
): void {
|
|
if (oldProps === null) {
|
|
throw new Error('Should have old props');
|
|
}
|
|
hostUpdateCounter++;
|
|
instance.prop = newProps.prop;
|
|
instance.hidden = !!newProps.hidden;
|
|
if (shouldSetTextContent(type, newProps)) {
|
|
instance.text = computeText(
|
|
(newProps.children: any) + '',
|
|
instance.context,
|
|
);
|
|
}
|
|
},
|
|
|
|
commitTextUpdate(
|
|
textInstance: TextInstance,
|
|
oldText: string,
|
|
newText: string,
|
|
): void {
|
|
hostUpdateCounter++;
|
|
textInstance.text = computeText(newText, textInstance.context);
|
|
},
|
|
|
|
appendChild,
|
|
appendChildToContainer,
|
|
insertBefore,
|
|
insertInContainerBefore,
|
|
removeChild,
|
|
removeChildFromContainer,
|
|
clearContainer,
|
|
|
|
hideInstance(instance: Instance): void {
|
|
instance.hidden = true;
|
|
},
|
|
|
|
hideTextInstance(textInstance: TextInstance): void {
|
|
textInstance.hidden = true;
|
|
},
|
|
|
|
unhideInstance(instance: Instance, props: Props): void {
|
|
if (!props.hidden) {
|
|
instance.hidden = false;
|
|
}
|
|
},
|
|
|
|
unhideTextInstance(textInstance: TextInstance, text: string): void {
|
|
textInstance.hidden = false;
|
|
},
|
|
|
|
resetTextContent(instance: Instance): void {
|
|
instance.text = null;
|
|
},
|
|
}
|
|
: {
|
|
...sharedHostConfig,
|
|
supportsMutation: false,
|
|
supportsPersistence: true,
|
|
|
|
cloneInstance,
|
|
clearContainer,
|
|
|
|
createContainerChildSet(
|
|
container: Container,
|
|
): Array<Instance | TextInstance> {
|
|
return [];
|
|
},
|
|
|
|
appendChildToContainerChildSet(
|
|
childSet: Array<Instance | TextInstance>,
|
|
child: Instance | TextInstance,
|
|
): void {
|
|
childSet.push(child);
|
|
},
|
|
|
|
finalizeContainerChildren(
|
|
container: Container,
|
|
newChildren: Array<Instance | TextInstance>,
|
|
): void {
|
|
container.pendingChildren = newChildren;
|
|
if (
|
|
newChildren.length === 1 &&
|
|
newChildren[0].text === 'Error when completing root'
|
|
) {
|
|
// Trigger an error for testing purposes
|
|
throw Error('Error when completing root');
|
|
}
|
|
},
|
|
|
|
replaceContainerChildren(
|
|
container: Container,
|
|
newChildren: Array<Instance | TextInstance>,
|
|
): void {
|
|
container.children = newChildren;
|
|
},
|
|
|
|
getOffscreenContainerType(): string {
|
|
return 'offscreen';
|
|
},
|
|
|
|
getOffscreenContainerProps(
|
|
mode: OffscreenMode,
|
|
children: ReactNodeList,
|
|
): Props {
|
|
return {
|
|
hidden: mode === 'hidden',
|
|
children,
|
|
};
|
|
},
|
|
|
|
cloneHiddenInstance(
|
|
instance: Instance,
|
|
type: string,
|
|
props: Props,
|
|
internalInstanceHandle: Object,
|
|
): Instance {
|
|
const clone = cloneInstance(
|
|
instance,
|
|
null,
|
|
type,
|
|
props,
|
|
props,
|
|
internalInstanceHandle,
|
|
true,
|
|
null,
|
|
);
|
|
clone.hidden = true;
|
|
return clone;
|
|
},
|
|
|
|
cloneHiddenTextInstance(
|
|
instance: TextInstance,
|
|
text: string,
|
|
internalInstanceHandle: Object,
|
|
): TextInstance {
|
|
const clone = {
|
|
text: instance.text,
|
|
id: instance.id,
|
|
parent: instance.parent,
|
|
hidden: true,
|
|
context: instance.context,
|
|
};
|
|
// Hide from unit tests
|
|
Object.defineProperty(clone, 'id', {
|
|
value: clone.id,
|
|
enumerable: false,
|
|
});
|
|
Object.defineProperty(clone, 'parent', {
|
|
value: clone.parent,
|
|
enumerable: false,
|
|
});
|
|
Object.defineProperty(clone, 'context', {
|
|
value: clone.context,
|
|
enumerable: false,
|
|
});
|
|
return clone;
|
|
},
|
|
};
|
|
|
|
const NoopRenderer = reconciler(hostConfig);
|
|
|
|
const rootContainers = new Map();
|
|
const roots = new Map();
|
|
const DEFAULT_ROOT_ID = '<default>';
|
|
|
|
let currentEventPriority = DefaultEventPriority;
|
|
|
|
function childToJSX(child, text) {
|
|
if (text !== null) {
|
|
return text;
|
|
}
|
|
if (child === null) {
|
|
return null;
|
|
}
|
|
if (typeof child === 'string') {
|
|
return child;
|
|
}
|
|
if (isArray(child)) {
|
|
if (child.length === 0) {
|
|
return null;
|
|
}
|
|
if (child.length === 1) {
|
|
return childToJSX(child[0], null);
|
|
}
|
|
// $FlowFixMe
|
|
const children = child.map(c => childToJSX(c, null));
|
|
if (children.every(c => typeof c === 'string' || typeof c === 'number')) {
|
|
return children.join('');
|
|
}
|
|
return children;
|
|
}
|
|
if (isArray(child.children)) {
|
|
// This is an instance.
|
|
const instance: Instance = (child: any);
|
|
const children = childToJSX(instance.children, instance.text);
|
|
const props = ({prop: instance.prop}: any);
|
|
if (instance.hidden) {
|
|
props.hidden = true;
|
|
}
|
|
if (children !== null) {
|
|
props.children = children;
|
|
}
|
|
return {
|
|
$$typeof: REACT_ELEMENT_TYPE,
|
|
type: instance.type,
|
|
key: null,
|
|
ref: null,
|
|
props: props,
|
|
_owner: null,
|
|
_store: __DEV__ ? {} : undefined,
|
|
};
|
|
}
|
|
// This is a text instance
|
|
const textInstance: TextInstance = (child: any);
|
|
if (textInstance.hidden) {
|
|
return '';
|
|
}
|
|
return textInstance.text;
|
|
}
|
|
|
|
function getChildren(root) {
|
|
if (root) {
|
|
return useMutation
|
|
? root.children
|
|
: removeOffscreenContainersFromChildren(root.children, false);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getPendingChildren(root) {
|
|
if (root) {
|
|
return useMutation
|
|
? root.children
|
|
: removeOffscreenContainersFromChildren(root.pendingChildren, false);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function removeOffscreenContainersFromChildren(children, hideNearestNode) {
|
|
// Mutation mode and persistent mode have different outputs for Offscreen
|
|
// and Suspense trees. Persistent mode adds an additional host node wrapper,
|
|
// whereas mutation mode does not.
|
|
//
|
|
// This function removes the offscreen host wrappers so that the output is
|
|
// consistent. If the offscreen node is hidden, it transfers the hiddenness
|
|
// to the child nodes, to mimic how it works in mutation mode. That way our
|
|
// tests don't have to fork tree assertions.
|
|
//
|
|
// So, it takes a tree that looks like this:
|
|
//
|
|
// <offscreen hidden={true}>
|
|
// <span>A</span>
|
|
// <span>B</span>
|
|
// </offscren>
|
|
//
|
|
// And turns it into this:
|
|
//
|
|
// <span hidden={true}>A</span>
|
|
// <span hidden={true}>B</span>
|
|
//
|
|
// We don't mutate the original tree, but instead return a copy.
|
|
//
|
|
// This function is only used by our test assertions, via the `getChildren`
|
|
// and `getChildrenAsJSX` methods.
|
|
let didClone = false;
|
|
const newChildren = [];
|
|
for (let i = 0; i < children.length; i++) {
|
|
const child = children[i];
|
|
const innerChildren = child.children;
|
|
if (innerChildren !== undefined) {
|
|
// This is a host instance instance
|
|
const instance: Instance = (child: any);
|
|
if (instance.type === 'offscreen') {
|
|
// This is an offscreen wrapper instance. Remove it from the tree
|
|
// and recursively return its children, as if it were a fragment.
|
|
didClone = true;
|
|
if (instance.text !== null) {
|
|
// If this offscreen tree contains only text, we replace it with
|
|
// a text child. Related to `shouldReplaceTextContent` feature.
|
|
const offscreenTextInstance: TextInstance = {
|
|
text: instance.text,
|
|
id: instanceCounter++,
|
|
parent: instance.parent,
|
|
hidden: hideNearestNode || instance.hidden,
|
|
context: instance.context,
|
|
};
|
|
// Hide from unit tests
|
|
Object.defineProperty(offscreenTextInstance, 'id', {
|
|
value: offscreenTextInstance.id,
|
|
enumerable: false,
|
|
});
|
|
Object.defineProperty(offscreenTextInstance, 'parent', {
|
|
value: offscreenTextInstance.parent,
|
|
enumerable: false,
|
|
});
|
|
Object.defineProperty(offscreenTextInstance, 'context', {
|
|
value: offscreenTextInstance.context,
|
|
enumerable: false,
|
|
});
|
|
newChildren.push(offscreenTextInstance);
|
|
} else {
|
|
// Skip the offscreen node and replace it with its children
|
|
const offscreenChildren = removeOffscreenContainersFromChildren(
|
|
innerChildren,
|
|
hideNearestNode || instance.hidden,
|
|
);
|
|
newChildren.push.apply(newChildren, offscreenChildren);
|
|
}
|
|
} else {
|
|
// This is a regular (non-offscreen) instance. If the nearest
|
|
// offscreen boundary is hidden, hide this node.
|
|
const hidden = hideNearestNode ? true : instance.hidden;
|
|
const clonedChildren = removeOffscreenContainersFromChildren(
|
|
instance.children,
|
|
// We never need to hide the children of this node, since if we're
|
|
// inside a hidden tree, then the hidden style will be applied to
|
|
// this node.
|
|
false,
|
|
);
|
|
if (
|
|
clonedChildren === instance.children &&
|
|
hidden === instance.hidden
|
|
) {
|
|
// No changes. Reuse the original instance without cloning.
|
|
newChildren.push(instance);
|
|
} else {
|
|
didClone = true;
|
|
const clone: Instance = {
|
|
id: instance.id,
|
|
type: instance.type,
|
|
parent: instance.parent,
|
|
children: clonedChildren,
|
|
text: instance.text,
|
|
prop: instance.prop,
|
|
hidden: hideNearestNode ? true : instance.hidden,
|
|
context: instance.context,
|
|
};
|
|
Object.defineProperty(clone, 'id', {
|
|
value: clone.id,
|
|
enumerable: false,
|
|
});
|
|
Object.defineProperty(clone, 'parent', {
|
|
value: clone.parent,
|
|
enumerable: false,
|
|
});
|
|
Object.defineProperty(clone, 'text', {
|
|
value: clone.text,
|
|
enumerable: false,
|
|
});
|
|
Object.defineProperty(clone, 'context', {
|
|
value: clone.context,
|
|
enumerable: false,
|
|
});
|
|
newChildren.push(clone);
|
|
}
|
|
}
|
|
} else {
|
|
// This is a text instance
|
|
const textInstance: TextInstance = (child: any);
|
|
if (hideNearestNode) {
|
|
didClone = true;
|
|
const clone = {
|
|
text: textInstance.text,
|
|
id: textInstance.id,
|
|
parent: textInstance.parent,
|
|
hidden: textInstance.hidden || hideNearestNode,
|
|
context: textInstance.context,
|
|
};
|
|
Object.defineProperty(clone, 'id', {
|
|
value: clone.id,
|
|
enumerable: false,
|
|
});
|
|
Object.defineProperty(clone, 'parent', {
|
|
value: clone.parent,
|
|
enumerable: false,
|
|
});
|
|
Object.defineProperty(clone, 'context', {
|
|
value: clone.context,
|
|
enumerable: false,
|
|
});
|
|
|
|
newChildren.push(clone);
|
|
} else {
|
|
newChildren.push(textInstance);
|
|
}
|
|
}
|
|
}
|
|
// There are some tests that assume reference equality, so preserve it
|
|
// when possible. Alternatively, we could update the tests to compare the
|
|
// ids instead.
|
|
return didClone ? newChildren : children;
|
|
}
|
|
|
|
function getChildrenAsJSX(root) {
|
|
const children = childToJSX(getChildren(root), null);
|
|
if (children === null) {
|
|
return null;
|
|
}
|
|
if (isArray(children)) {
|
|
return {
|
|
$$typeof: REACT_ELEMENT_TYPE,
|
|
type: REACT_FRAGMENT_TYPE,
|
|
key: null,
|
|
ref: null,
|
|
props: {children},
|
|
_owner: null,
|
|
_store: __DEV__ ? {} : undefined,
|
|
};
|
|
}
|
|
return children;
|
|
}
|
|
|
|
function getPendingChildrenAsJSX(root) {
|
|
const children = childToJSX(getChildren(root), null);
|
|
if (children === null) {
|
|
return null;
|
|
}
|
|
if (isArray(children)) {
|
|
return {
|
|
$$typeof: REACT_ELEMENT_TYPE,
|
|
type: REACT_FRAGMENT_TYPE,
|
|
key: null,
|
|
ref: null,
|
|
props: {children},
|
|
_owner: null,
|
|
_store: __DEV__ ? {} : undefined,
|
|
};
|
|
}
|
|
return children;
|
|
}
|
|
|
|
let idCounter = 0;
|
|
|
|
const ReactNoop = {
|
|
_Scheduler: Scheduler,
|
|
|
|
getChildren(rootID: string = DEFAULT_ROOT_ID) {
|
|
const container = rootContainers.get(rootID);
|
|
return getChildren(container);
|
|
},
|
|
|
|
getPendingChildren(rootID: string = DEFAULT_ROOT_ID) {
|
|
const container = rootContainers.get(rootID);
|
|
return getPendingChildren(container);
|
|
},
|
|
|
|
getOrCreateRootContainer(rootID: string = DEFAULT_ROOT_ID, tag: RootTag) {
|
|
let root = roots.get(rootID);
|
|
if (!root) {
|
|
const container = {rootID: rootID, pendingChildren: [], children: []};
|
|
rootContainers.set(rootID, container);
|
|
root = NoopRenderer.createContainer(container, tag, false, null, null);
|
|
roots.set(rootID, root);
|
|
}
|
|
return root.current.stateNode.containerInfo;
|
|
},
|
|
|
|
// TODO: Replace ReactNoop.render with createRoot + root.render
|
|
createRoot() {
|
|
const container = {
|
|
rootID: '' + idCounter++,
|
|
pendingChildren: [],
|
|
children: [],
|
|
};
|
|
const fiberRoot = NoopRenderer.createContainer(
|
|
container,
|
|
ConcurrentRoot,
|
|
false,
|
|
null,
|
|
null,
|
|
);
|
|
return {
|
|
_Scheduler: Scheduler,
|
|
render(children: ReactNodeList) {
|
|
NoopRenderer.updateContainer(children, fiberRoot, null, null);
|
|
},
|
|
getChildren() {
|
|
return getChildren(container);
|
|
},
|
|
getChildrenAsJSX() {
|
|
return getChildrenAsJSX(container);
|
|
},
|
|
};
|
|
},
|
|
|
|
createLegacyRoot() {
|
|
const container = {
|
|
rootID: '' + idCounter++,
|
|
pendingChildren: [],
|
|
children: [],
|
|
};
|
|
const fiberRoot = NoopRenderer.createContainer(
|
|
container,
|
|
LegacyRoot,
|
|
false,
|
|
null,
|
|
null,
|
|
);
|
|
return {
|
|
_Scheduler: Scheduler,
|
|
render(children: ReactNodeList) {
|
|
NoopRenderer.updateContainer(children, fiberRoot, null, null);
|
|
},
|
|
getChildren() {
|
|
return getChildren(container);
|
|
},
|
|
getChildrenAsJSX() {
|
|
return getChildrenAsJSX(container);
|
|
},
|
|
};
|
|
},
|
|
|
|
getChildrenAsJSX(rootID: string = DEFAULT_ROOT_ID) {
|
|
const container = rootContainers.get(rootID);
|
|
return getChildrenAsJSX(container);
|
|
},
|
|
|
|
getPendingChildrenAsJSX(rootID: string = DEFAULT_ROOT_ID) {
|
|
const container = rootContainers.get(rootID);
|
|
return getPendingChildrenAsJSX(container);
|
|
},
|
|
|
|
createPortal(
|
|
children: ReactNodeList,
|
|
container: Container,
|
|
key: ?string = null,
|
|
) {
|
|
return NoopRenderer.createPortal(children, container, null, key);
|
|
},
|
|
|
|
// Shortcut for testing a single root
|
|
render(element: React$Element<any>, callback: ?Function) {
|
|
ReactNoop.renderToRootWithID(element, DEFAULT_ROOT_ID, callback);
|
|
},
|
|
|
|
renderLegacySyncRoot(element: React$Element<any>, callback: ?Function) {
|
|
const rootID = DEFAULT_ROOT_ID;
|
|
const container = ReactNoop.getOrCreateRootContainer(rootID, LegacyRoot);
|
|
const root = roots.get(container.rootID);
|
|
NoopRenderer.updateContainer(element, root, null, callback);
|
|
},
|
|
|
|
renderToRootWithID(
|
|
element: React$Element<any>,
|
|
rootID: string,
|
|
callback: ?Function,
|
|
) {
|
|
const container = ReactNoop.getOrCreateRootContainer(
|
|
rootID,
|
|
ConcurrentRoot,
|
|
);
|
|
const root = roots.get(container.rootID);
|
|
NoopRenderer.updateContainer(element, root, null, callback);
|
|
},
|
|
|
|
unmountRootWithID(rootID: string) {
|
|
const root = roots.get(rootID);
|
|
if (root) {
|
|
NoopRenderer.updateContainer(null, root, null, () => {
|
|
roots.delete(rootID);
|
|
rootContainers.delete(rootID);
|
|
});
|
|
}
|
|
},
|
|
|
|
findInstance(
|
|
componentOrElement: Element | ?React$Component<any, any>,
|
|
): null | Instance | TextInstance {
|
|
if (componentOrElement == null) {
|
|
return null;
|
|
}
|
|
// Unsound duck typing.
|
|
const component = (componentOrElement: any);
|
|
if (typeof component.id === 'number') {
|
|
return component;
|
|
}
|
|
if (__DEV__) {
|
|
return NoopRenderer.findHostInstanceWithWarning(
|
|
component,
|
|
'findInstance',
|
|
);
|
|
}
|
|
return NoopRenderer.findHostInstance(component);
|
|
},
|
|
|
|
flushNextYield(): Array<mixed> {
|
|
Scheduler.unstable_flushNumberOfYields(1);
|
|
return Scheduler.unstable_clearYields();
|
|
},
|
|
|
|
flushWithHostCounters(
|
|
fn: () => void,
|
|
):
|
|
| {|
|
|
hostDiffCounter: number,
|
|
hostUpdateCounter: number,
|
|
|}
|
|
| {|
|
|
hostDiffCounter: number,
|
|
hostCloneCounter: number,
|
|
|} {
|
|
hostDiffCounter = 0;
|
|
hostUpdateCounter = 0;
|
|
hostCloneCounter = 0;
|
|
try {
|
|
Scheduler.unstable_flushAll();
|
|
return useMutation
|
|
? {
|
|
hostDiffCounter,
|
|
hostUpdateCounter,
|
|
}
|
|
: {
|
|
hostDiffCounter,
|
|
hostCloneCounter,
|
|
};
|
|
} finally {
|
|
hostDiffCounter = 0;
|
|
hostUpdateCounter = 0;
|
|
hostCloneCounter = 0;
|
|
}
|
|
},
|
|
|
|
expire: Scheduler.unstable_advanceTime,
|
|
|
|
flushExpired(): Array<mixed> {
|
|
return Scheduler.unstable_flushExpired();
|
|
},
|
|
|
|
unstable_runWithPriority: NoopRenderer.runWithPriority,
|
|
|
|
batchedUpdates: NoopRenderer.batchedUpdates,
|
|
|
|
deferredUpdates: NoopRenderer.deferredUpdates,
|
|
|
|
discreteUpdates: NoopRenderer.discreteUpdates,
|
|
|
|
idleUpdates<T>(fn: () => T): T {
|
|
const prevEventPriority = currentEventPriority;
|
|
currentEventPriority = IdleEventPriority;
|
|
try {
|
|
fn();
|
|
} finally {
|
|
currentEventPriority = prevEventPriority;
|
|
}
|
|
},
|
|
|
|
flushSync: NoopRenderer.flushSync,
|
|
flushPassiveEffects: NoopRenderer.flushPassiveEffects,
|
|
|
|
// Logs the current state of the tree.
|
|
dumpTree(rootID: string = DEFAULT_ROOT_ID) {
|
|
const root = roots.get(rootID);
|
|
const rootContainer = rootContainers.get(rootID);
|
|
if (!root || !rootContainer) {
|
|
// eslint-disable-next-line react-internal/no-production-logging
|
|
console.log('Nothing rendered yet.');
|
|
return;
|
|
}
|
|
|
|
const bufferedLog = [];
|
|
function log(...args) {
|
|
bufferedLog.push(...args, '\n');
|
|
}
|
|
|
|
function logHostInstances(
|
|
children: Array<Instance | TextInstance>,
|
|
depth,
|
|
) {
|
|
for (let i = 0; i < children.length; i++) {
|
|
const child = children[i];
|
|
const indent = ' '.repeat(depth);
|
|
if (typeof child.text === 'string') {
|
|
log(indent + '- ' + child.text);
|
|
} else {
|
|
// $FlowFixMe - The child should've been refined now.
|
|
log(indent + '- ' + child.type + '#' + child.id);
|
|
// $FlowFixMe - The child should've been refined now.
|
|
logHostInstances(child.children, depth + 1);
|
|
}
|
|
}
|
|
}
|
|
function logContainer(container: Container, depth) {
|
|
log(' '.repeat(depth) + '- [root#' + container.rootID + ']');
|
|
logHostInstances(container.children, depth + 1);
|
|
}
|
|
|
|
function logUpdateQueue(updateQueue: UpdateQueue<mixed>, depth) {
|
|
log(' '.repeat(depth + 1) + 'QUEUED UPDATES');
|
|
const first = updateQueue.firstBaseUpdate;
|
|
const update = first;
|
|
if (update !== null) {
|
|
do {
|
|
log(
|
|
' '.repeat(depth + 1) + '~',
|
|
'[' + update.expirationTime + ']',
|
|
);
|
|
} while (update !== null);
|
|
}
|
|
|
|
const lastPending = updateQueue.shared.pending;
|
|
if (lastPending !== null) {
|
|
const firstPending = lastPending.next;
|
|
const pendingUpdate = firstPending;
|
|
if (pendingUpdate !== null) {
|
|
do {
|
|
log(
|
|
' '.repeat(depth + 1) + '~',
|
|
'[' + pendingUpdate.expirationTime + ']',
|
|
);
|
|
} while (pendingUpdate !== null && pendingUpdate !== firstPending);
|
|
}
|
|
}
|
|
}
|
|
|
|
function logFiber(fiber: Fiber, depth) {
|
|
log(
|
|
' '.repeat(depth) +
|
|
'- ' +
|
|
// need to explicitly coerce Symbol to a string
|
|
(fiber.type ? fiber.type.name || fiber.type.toString() : '[root]'),
|
|
'[' +
|
|
fiber.childExpirationTime +
|
|
(fiber.pendingProps ? '*' : '') +
|
|
']',
|
|
);
|
|
if (fiber.updateQueue) {
|
|
logUpdateQueue(fiber.updateQueue, depth);
|
|
}
|
|
// const childInProgress = fiber.progressedChild;
|
|
// if (childInProgress && childInProgress !== fiber.child) {
|
|
// log(
|
|
// ' '.repeat(depth + 1) + 'IN PROGRESS: ' + fiber.pendingWorkPriority,
|
|
// );
|
|
// logFiber(childInProgress, depth + 1);
|
|
// if (fiber.child) {
|
|
// log(' '.repeat(depth + 1) + 'CURRENT');
|
|
// }
|
|
// } else if (fiber.child && fiber.updateQueue) {
|
|
// log(' '.repeat(depth + 1) + 'CHILDREN');
|
|
// }
|
|
if (fiber.child) {
|
|
logFiber(fiber.child, depth + 1);
|
|
}
|
|
if (fiber.sibling) {
|
|
logFiber(fiber.sibling, depth);
|
|
}
|
|
}
|
|
|
|
log('HOST INSTANCES:');
|
|
logContainer(rootContainer, 0);
|
|
log('FIBERS:');
|
|
logFiber(root.current, 0);
|
|
|
|
// eslint-disable-next-line react-internal/no-production-logging
|
|
console.log(...bufferedLog);
|
|
},
|
|
|
|
getRoot(rootID: string = DEFAULT_ROOT_ID) {
|
|
return roots.get(rootID);
|
|
},
|
|
};
|
|
|
|
return ReactNoop;
|
|
}
|
|
|
|
export default createReactNoop;
|