Files
react/packages/react-art/src/ReactARTHostConfig.js
T
Andrew Clark 0131d0cff4 Check if suspensey instance resolves in immediate task (#26427)
When rendering a suspensey resource that we haven't seen before, it may
have loaded in the background while we were rendering. We should yield
to the main thread to see if the load event fires in an immediate task.

For example, if the resource for a link element has already loaded, its
load event will fire in a task right after React yields to the main
thread. Because the continuation task is not scheduled until right
before React yields, the load event will ping React before it resumes.

If this happens, we can resume rendering without showing a fallback.

I don't think this matters much for images, because the `completed`
property tells us whether the image has loaded, and during a non-urgent
render, we never block the main thread for more than 5ms at a time (for
now — we might increase this in the future). It matters more for
stylesheets because the only way to check if it has loaded is by
listening for the load event.

This is essentially the same trick that `use` does for userspace
promises, but a bit simpler because we don't need to replay the host
component's begin phase; the work-in-progress fiber already completed,
so we can just continue onto the next sibling without any additional
work.

As part of this change, I split the `shouldSuspendCommit` host config
method into separate `maySuspendCommit` and `preloadInstance` methods.
Previously `shouldSuspendCommit` was used for both.

This raised a question of whether we should preload resources during a
synchronous render. My initial instinct was that we shouldn't, because
we're going to synchronously block the main thread until the resource is
inserted into the DOM, anyway. But I wonder if the browser is able to
initiate the preload even while the main thread is blocked. It's
probably a micro-optimization either way because most resources will be
loaded during transitions, not urgent renders.
2023-03-20 12:35:10 -04:00

486 lines
11 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import Transform from 'art/core/transform';
import Mode from 'art/modes/current';
import {TYPES, EVENT_TYPES, childrenAsString} from './ReactARTInternals';
import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities';
const pooledTransform = new Transform();
const NO_CONTEXT = {};
const UPDATE_SIGNAL = {};
if (__DEV__) {
Object.freeze(NO_CONTEXT);
Object.freeze(UPDATE_SIGNAL);
}
/** Helper Methods */
function addEventListeners(instance, type, listener) {
// We need to explicitly unregister before unmount.
// For this reason we need to track subscriptions.
if (!instance._listeners) {
instance._listeners = {};
instance._subscriptions = {};
}
instance._listeners[type] = listener;
if (listener) {
if (!instance._subscriptions[type]) {
instance._subscriptions[type] = instance.subscribe(
type,
createEventHandler(instance),
instance,
);
}
} else {
if (instance._subscriptions[type]) {
instance._subscriptions[type]();
delete instance._subscriptions[type];
}
}
}
function createEventHandler(instance) {
return function handleEvent(event) {
const listener = instance._listeners[event.type];
if (!listener) {
// Noop
} else if (typeof listener === 'function') {
listener.call(instance, event);
} else if (listener.handleEvent) {
listener.handleEvent(event);
}
};
}
function destroyEventListeners(instance) {
if (instance._subscriptions) {
for (const type in instance._subscriptions) {
instance._subscriptions[type]();
}
}
instance._subscriptions = null;
instance._listeners = null;
}
function getScaleX(props) {
if (props.scaleX != null) {
return props.scaleX;
} else if (props.scale != null) {
return props.scale;
} else {
return 1;
}
}
function getScaleY(props) {
if (props.scaleY != null) {
return props.scaleY;
} else if (props.scale != null) {
return props.scale;
} else {
return 1;
}
}
function isSameFont(oldFont, newFont) {
if (oldFont === newFont) {
return true;
} else if (typeof newFont === 'string' || typeof oldFont === 'string') {
return false;
} else {
return (
newFont.fontSize === oldFont.fontSize &&
newFont.fontStyle === oldFont.fontStyle &&
newFont.fontVariant === oldFont.fontVariant &&
newFont.fontWeight === oldFont.fontWeight &&
newFont.fontFamily === oldFont.fontFamily
);
}
}
/** Render Methods */
function applyClippingRectangleProps(instance, props, prevProps = {}) {
applyNodeProps(instance, props, prevProps);
instance.width = props.width;
instance.height = props.height;
}
function applyGroupProps(instance, props, prevProps = {}) {
applyNodeProps(instance, props, prevProps);
instance.width = props.width;
instance.height = props.height;
}
function applyNodeProps(instance, props, prevProps = {}) {
const scaleX = getScaleX(props);
const scaleY = getScaleY(props);
pooledTransform
.transformTo(1, 0, 0, 1, 0, 0)
.move(props.x || 0, props.y || 0)
.rotate(props.rotation || 0, props.originX, props.originY)
.scale(scaleX, scaleY, props.originX, props.originY);
if (props.transform != null) {
pooledTransform.transform(props.transform);
}
if (
instance.xx !== pooledTransform.xx ||
instance.yx !== pooledTransform.yx ||
instance.xy !== pooledTransform.xy ||
instance.yy !== pooledTransform.yy ||
instance.x !== pooledTransform.x ||
instance.y !== pooledTransform.y
) {
instance.transformTo(pooledTransform);
}
if (props.cursor !== prevProps.cursor || props.title !== prevProps.title) {
instance.indicate(props.cursor, props.title);
}
if (instance.blend && props.opacity !== prevProps.opacity) {
instance.blend(props.opacity == null ? 1 : props.opacity);
}
if (props.visible !== prevProps.visible) {
if (props.visible == null || props.visible) {
instance.show();
} else {
instance.hide();
}
}
for (const type in EVENT_TYPES) {
addEventListeners(instance, EVENT_TYPES[type], props[type]);
}
}
function applyRenderableNodeProps(instance, props, prevProps = {}) {
applyNodeProps(instance, props, prevProps);
if (prevProps.fill !== props.fill) {
if (props.fill && props.fill.applyFill) {
props.fill.applyFill(instance);
} else {
instance.fill(props.fill);
}
}
if (
prevProps.stroke !== props.stroke ||
prevProps.strokeWidth !== props.strokeWidth ||
prevProps.strokeCap !== props.strokeCap ||
prevProps.strokeJoin !== props.strokeJoin ||
// TODO: Consider deep check of stokeDash; may benefit VML in IE.
prevProps.strokeDash !== props.strokeDash
) {
instance.stroke(
props.stroke,
props.strokeWidth,
props.strokeCap,
props.strokeJoin,
props.strokeDash,
);
}
}
function applyShapeProps(instance, props, prevProps = {}) {
applyRenderableNodeProps(instance, props, prevProps);
const path = props.d || childrenAsString(props.children);
const prevDelta = instance._prevDelta;
const prevPath = instance._prevPath;
if (
path !== prevPath ||
path.delta !== prevDelta ||
prevProps.height !== props.height ||
prevProps.width !== props.width
) {
instance.draw(path, props.width, props.height);
instance._prevDelta = path.delta;
instance._prevPath = path;
}
}
function applyTextProps(instance, props, prevProps = {}) {
applyRenderableNodeProps(instance, props, prevProps);
const string = props.children;
if (
instance._currentString !== string ||
!isSameFont(props.font, prevProps.font) ||
props.alignment !== prevProps.alignment ||
props.path !== prevProps.path
) {
instance.draw(string, props.font, props.alignment, props.path);
instance._currentString = string;
}
}
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoPersistence';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoScopes';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoSingletons';
export function appendInitialChild(parentInstance, child) {
if (typeof child === 'string') {
// Noop for string children of Text (eg <Text>{'foo'}{'bar'}</Text>)
throw new Error('Text children should already be flattened.');
}
child.inject(parentInstance);
}
export function createInstance(type, props, internalInstanceHandle) {
let instance;
switch (type) {
case TYPES.CLIPPING_RECTANGLE:
instance = Mode.ClippingRectangle();
instance._applyProps = applyClippingRectangleProps;
break;
case TYPES.GROUP:
instance = Mode.Group();
instance._applyProps = applyGroupProps;
break;
case TYPES.SHAPE:
instance = Mode.Shape();
instance._applyProps = applyShapeProps;
break;
case TYPES.TEXT:
instance = Mode.Text(
props.children,
props.font,
props.alignment,
props.path,
);
instance._applyProps = applyTextProps;
break;
}
if (!instance) {
throw new Error(`ReactART does not support the type "${type}"`);
}
instance._applyProps(instance, props);
return instance;
}
export function createTextInstance(
text,
rootContainerInstance,
internalInstanceHandle,
) {
return text;
}
export function finalizeInitialChildren(domElement, type, props) {
return false;
}
export function getPublicInstance(instance) {
return instance;
}
export function prepareForCommit() {
// Noop
return null;
}
export function prepareUpdate(domElement, type, oldProps, newProps) {
return UPDATE_SIGNAL;
}
export function resetAfterCommit() {
// Noop
}
export function resetTextContent(domElement) {
// Noop
}
export function getRootHostContext() {
return NO_CONTEXT;
}
export function getChildHostContext() {
return NO_CONTEXT;
}
export const scheduleTimeout = setTimeout;
export const cancelTimeout = clearTimeout;
export const noTimeout = -1;
export function shouldSetTextContent(type, props) {
return (
typeof props.children === 'string' || typeof props.children === 'number'
);
}
export function getCurrentEventPriority() {
return DefaultEventPriority;
}
// The ART renderer is secondary to the React DOM renderer.
export const isPrimaryRenderer = false;
// The ART renderer shouldn't trigger missing act() warnings
export const warnsIfNotActing = false;
export const supportsMutation = true;
export function appendChild(parentInstance, child) {
if (child.parentNode === parentInstance) {
child.eject();
}
child.inject(parentInstance);
}
export function appendChildToContainer(parentInstance, child) {
if (child.parentNode === parentInstance) {
child.eject();
}
child.inject(parentInstance);
}
export function insertBefore(parentInstance, child, beforeChild) {
if (child === beforeChild) {
throw new Error('ReactART: Can not insert node before itself');
}
child.injectBefore(beforeChild);
}
export function insertInContainerBefore(parentInstance, child, beforeChild) {
if (child === beforeChild) {
throw new Error('ReactART: Can not insert node before itself');
}
child.injectBefore(beforeChild);
}
export function removeChild(parentInstance, child) {
destroyEventListeners(child);
child.eject();
}
export function removeChildFromContainer(parentInstance, child) {
destroyEventListeners(child);
child.eject();
}
export function commitTextUpdate(textInstance, oldText, newText) {
// Noop
}
export function commitMount(instance, type, newProps) {
// Noop
}
export function commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
) {
instance._applyProps(instance, newProps, oldProps);
}
export function hideInstance(instance) {
instance.hide();
}
export function hideTextInstance(textInstance) {
// Noop
}
export function unhideInstance(instance, props) {
if (props.visible == null || props.visible) {
instance.show();
}
}
export function unhideTextInstance(textInstance, text): void {
// Noop
}
export function clearContainer(container) {
// TODO Implement this
}
export function getInstanceFromNode(node) {
throw new Error('Not implemented.');
}
export function beforeActiveInstanceBlur(internalInstanceHandle: Object) {
// noop
}
export function afterActiveInstanceBlur() {
// noop
}
export function preparePortalMount(portalInstance: any): void {
// noop
}
// eslint-disable-next-line no-undef
export function detachDeletedInstance(node: Instance): void {
// noop
}
export function requestPostPaintCallback(callback: (time: number) => void) {
// noop
}
export function maySuspendCommit(type, props) {
return false;
}
export function preloadInstance(type, props) {
// Return true to indicate it's already loaded
return true;
}
export function startSuspendingCommit() {}
export function suspendInstance(type, props) {}
export function waitForCommitToBeReady() {
return null;
}
// eslint-disable-next-line no-undef
export function prepareRendererToRender(container: Container): void {
// noop
}
export function resetRendererAfterRender(): void {
// noop
}