From 64cf51adc607e2e3b6e7ac73e2341d976e7f94d2 Mon Sep 17 00:00:00 2001 From: Dan Date: Sat, 12 May 2018 21:44:22 +0100 Subject: [PATCH 1/5] Separate test renderer host config --- .../src/ReactTestHostConfig.js | 218 +++++++++++++ .../src/ReactTestRenderer.js | 299 +----------------- .../src/ReactTestRendererScheduling.js | 100 ++++++ 3 files changed, 331 insertions(+), 286 deletions(-) create mode 100644 packages/react-test-renderer/src/ReactTestHostConfig.js create mode 100644 packages/react-test-renderer/src/ReactTestRendererScheduling.js diff --git a/packages/react-test-renderer/src/ReactTestHostConfig.js b/packages/react-test-renderer/src/ReactTestHostConfig.js new file mode 100644 index 0000000000000..ef7c6c927c4b8 --- /dev/null +++ b/packages/react-test-renderer/src/ReactTestHostConfig.js @@ -0,0 +1,218 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import emptyObject from 'fbjs/lib/emptyObject'; + +import * as TestRendererScheduling from './ReactTestRendererScheduling'; + +export type Instance = {| + type: string, + props: Object, + children: Array, + rootContainerInstance: Container, + tag: 'INSTANCE', +|}; + +export type TextInstance = {| + text: string, + tag: 'TEXT', +|}; + +type Container = {| + children: Array, + createNodeMock: Function, + tag: 'CONTAINER', +|}; + +type Props = Object; + +const UPDATE_SIGNAL = {}; + +function getPublicInstance(inst: Instance | TextInstance): * { + switch (inst.tag) { + case 'INSTANCE': + const createNodeMock = inst.rootContainerInstance.createNodeMock; + return createNodeMock({ + type: inst.type, + props: inst.props, + }); + default: + return inst; + } +} + +function appendChild( + parentInstance: Instance | Container, + child: Instance | TextInstance, +): void { + const index = parentInstance.children.indexOf(child); + if (index !== -1) { + parentInstance.children.splice(index, 1); + } + parentInstance.children.push(child); +} + +function insertBefore( + parentInstance: Instance | Container, + 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); + parentInstance.children.splice(beforeIndex, 0, child); +} + +function removeChild( + parentInstance: Instance | Container, + child: Instance | TextInstance, +): void { + const index = parentInstance.children.indexOf(child); + parentInstance.children.splice(index, 1); +} + +const ReactTestHostConfig = { + getRootHostContext() { + return emptyObject; + }, + + getChildHostContext() { + return emptyObject; + }, + + prepareForCommit(): void { + // noop + }, + + resetAfterCommit(): void { + // noop + }, + + createInstance( + type: string, + props: Props, + rootContainerInstance: Container, + hostContext: Object, + internalInstanceHandle: Object, + ): Instance { + return { + type, + props, + children: [], + rootContainerInstance, + tag: 'INSTANCE', + }; + }, + + appendInitialChild( + parentInstance: Instance, + child: Instance | TextInstance, + ): void { + const index = parentInstance.children.indexOf(child); + if (index !== -1) { + parentInstance.children.splice(index, 1); + } + parentInstance.children.push(child); + }, + + finalizeInitialChildren( + testElement: Instance, + type: string, + props: Props, + rootContainerInstance: Container, + ): boolean { + return false; + }, + + prepareUpdate( + testElement: Instance, + type: string, + oldProps: Props, + newProps: Props, + rootContainerInstance: Container, + hostContext: Object, + ): null | {} { + return UPDATE_SIGNAL; + }, + + shouldSetTextContent(type: string, props: Props): boolean { + return false; + }, + + shouldDeprioritizeSubtree(type: string, props: Props): boolean { + return false; + }, + + createTextInstance( + text: string, + rootContainerInstance: Container, + hostContext: Object, + internalInstanceHandle: Object, + ): TextInstance { + return { + text, + tag: 'TEXT', + }; + }, + + getPublicInstance, + + scheduleDeferredCallback: TestRendererScheduling.scheduleDeferredCallback, + cancelDeferredCallback: TestRendererScheduling.cancelDeferredCallback, + // This approach enables `now` to be mocked by tests, + // Even after the reconciler has initialized and read host config values. + now: () => TestRendererScheduling.nowImplementation(), + + isPrimaryRenderer: true, + + mutation: { + commitUpdate( + instance: Instance, + updatePayload: {}, + type: string, + oldProps: Props, + newProps: Props, + internalInstanceHandle: Object, + ): void { + instance.type = type; + instance.props = newProps; + }, + + commitMount( + instance: Instance, + type: string, + newProps: Props, + internalInstanceHandle: Object, + ): void { + // noop + }, + + commitTextUpdate( + textInstance: TextInstance, + oldText: string, + newText: string, + ): void { + textInstance.text = newText; + }, + resetTextContent(testElement: Instance): void { + // noop + }, + + appendChild: appendChild, + appendChildToContainer: appendChild, + insertBefore: insertBefore, + insertInContainerBefore: insertBefore, + removeChild: removeChild, + removeChildFromContainer: removeChild, + }, +}; + +export default ReactTestHostConfig; diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index 7fa54534d3103..5fab9af606fbe 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -9,12 +9,11 @@ import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot'; -import type {Deadline} from 'react-reconciler/src/ReactFiberReconciler'; +import type {Instance, TextInstance} from './ReactTestHostConfig'; import ReactFiberReconciler from 'react-reconciler'; import {batchedUpdates} from 'events/ReactGenericBatching'; import {findCurrentFiberUsingSlowPath} from 'react-reconciler/reflection'; -import emptyObject from 'fbjs/lib/emptyObject'; import { Fragment, FunctionalComponent, @@ -31,6 +30,9 @@ import { } from 'shared/ReactTypeOfWork'; import invariant from 'fbjs/lib/invariant'; +import ReactTestHostConfig from './ReactTestHostConfig'; +import * as TestRendererScheduling from './ReactTestRendererScheduling'; + type TestRendererOptions = { createNodeMock: (element: React$Element) => any, unstable_isAsync: boolean, @@ -44,26 +46,6 @@ type ReactTestRendererJSON = {| |}; type ReactTestRendererNode = ReactTestRendererJSON | string; -type Container = {| - children: Array, - createNodeMock: Function, - tag: 'CONTAINER', -|}; - -type Props = Object; -type Instance = {| - type: string, - props: Object, - children: Array, - rootContainerInstance: Container, - tag: 'INSTANCE', -|}; - -type TextInstance = {| - text: string, - tag: 'TEXT', -|}; - type FindOptions = $Shape<{ // performs a "greedy" search: if a matching node is found, will continue // to search within the matching node's children. (default: true) @@ -72,203 +54,7 @@ type FindOptions = $Shape<{ export type Predicate = (node: ReactTestInstance) => ?boolean; -const UPDATE_SIGNAL = {}; - -function getPublicInstance(inst: Instance | TextInstance): * { - switch (inst.tag) { - case 'INSTANCE': - const createNodeMock = inst.rootContainerInstance.createNodeMock; - return createNodeMock({ - type: inst.type, - props: inst.props, - }); - default: - return inst; - } -} - -function appendChild( - parentInstance: Instance | Container, - child: Instance | TextInstance, -): void { - const index = parentInstance.children.indexOf(child); - if (index !== -1) { - parentInstance.children.splice(index, 1); - } - parentInstance.children.push(child); -} - -function insertBefore( - parentInstance: Instance | Container, - 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); - parentInstance.children.splice(beforeIndex, 0, child); -} - -function removeChild( - parentInstance: Instance | Container, - child: Instance | TextInstance, -): void { - const index = parentInstance.children.indexOf(child); - parentInstance.children.splice(index, 1); -} - -// Current virtual time -let nowImplementation = () => 0; -let scheduledCallback: ((deadline: Deadline) => mixed) | null = null; -let yieldedValues: Array | null = null; - -const TestRenderer = ReactFiberReconciler({ - getRootHostContext() { - return emptyObject; - }, - - getChildHostContext() { - return emptyObject; - }, - - prepareForCommit(): void { - // noop - }, - - resetAfterCommit(): void { - // noop - }, - - createInstance( - type: string, - props: Props, - rootContainerInstance: Container, - hostContext: Object, - internalInstanceHandle: Object, - ): Instance { - return { - type, - props, - children: [], - rootContainerInstance, - tag: 'INSTANCE', - }; - }, - - appendInitialChild( - parentInstance: Instance, - child: Instance | TextInstance, - ): void { - const index = parentInstance.children.indexOf(child); - if (index !== -1) { - parentInstance.children.splice(index, 1); - } - parentInstance.children.push(child); - }, - - finalizeInitialChildren( - testElement: Instance, - type: string, - props: Props, - rootContainerInstance: Container, - ): boolean { - return false; - }, - - prepareUpdate( - testElement: Instance, - type: string, - oldProps: Props, - newProps: Props, - rootContainerInstance: Container, - hostContext: Object, - ): null | {} { - return UPDATE_SIGNAL; - }, - - shouldSetTextContent(type: string, props: Props): boolean { - return false; - }, - - shouldDeprioritizeSubtree(type: string, props: Props): boolean { - return false; - }, - - createTextInstance( - text: string, - rootContainerInstance: Container, - hostContext: Object, - internalInstanceHandle: Object, - ): TextInstance { - return { - text, - tag: 'TEXT', - }; - }, - - scheduleDeferredCallback( - callback: (deadline: Deadline) => mixed, - options?: {timeout: number}, - ): number { - scheduledCallback = callback; - return 0; - }, - - cancelDeferredCallback(timeoutID: number): void { - scheduledCallback = null; - }, - - getPublicInstance, - - // This approach enables `now` to be mocked by tests, - // Even after the reconciler has initialized and read host config values. - now: () => nowImplementation(), - - isPrimaryRenderer: true, - - mutation: { - commitUpdate( - instance: Instance, - updatePayload: {}, - type: string, - oldProps: Props, - newProps: Props, - internalInstanceHandle: Object, - ): void { - instance.type = type; - instance.props = newProps; - }, - - commitMount( - instance: Instance, - type: string, - newProps: Props, - internalInstanceHandle: Object, - ): void { - // noop - }, - - commitTextUpdate( - textInstance: TextInstance, - oldText: string, - newText: string, - ): void { - textInstance.text = newText; - }, - resetTextContent(testElement: Instance): void { - // noop - }, - - appendChild: appendChild, - appendChildToContainer: appendChild, - insertBefore: insertBefore, - insertInContainerBefore: insertBefore, - removeChild: removeChild, - removeChildFromContainer: removeChild, - }, -}); +const TestRenderer = ReactFiberReconciler(ReactTestHostConfig); const defaultTestOptions = { createNodeMock: function() { @@ -444,7 +230,7 @@ class ReactTestInstance { get instance() { if (this._fiber.tag === HostComponent) { - return getPublicInstance(this._fiber.stateNode); + return ReactTestHostConfig.getPublicInstance(this._fiber.stateNode); } else { return this._fiber.stateNode; } @@ -676,77 +462,20 @@ const ReactTestRendererFiber = { container = null; root = null; }, - unstable_flushAll(): Array { - yieldedValues = null; - while (scheduledCallback !== null) { - const cb = scheduledCallback; - scheduledCallback = null; - cb({ - timeRemaining() { - // Keep rendering until there's no more work - return 999; - }, - // React's scheduler has its own way of keeping track of expired - // work and doesn't read this, so don't bother setting it to the - // correct value. - didTimeout: false, - }); - } - if (yieldedValues === null) { - // Always return an array. - return []; - } - return yieldedValues; - }, - unstable_flushThrough(expectedValues: Array): Array { - let didStop = false; - yieldedValues = null; - while (scheduledCallback !== null && !didStop) { - const cb = scheduledCallback; - scheduledCallback = null; - cb({ - timeRemaining() { - if ( - yieldedValues !== null && - yieldedValues.length >= expectedValues.length - ) { - // We at least as many values as expected. Stop rendering. - didStop = true; - return 0; - } - // Keep rendering. - return 999; - }, - // React's scheduler has its own way of keeping track of expired - // work and doesn't read this, so don't bother setting it to the - // correct value. - didTimeout: false, - }); - } - if (yieldedValues === null) { - // Always return an array. - return []; - } - return yieldedValues; - }, - unstable_yield(value: mixed): void { - if (yieldedValues === null) { - yieldedValues = [value]; - } else { - yieldedValues.push(value); - } - }, getInstance() { if (root == null || root.current == null) { return null; } return TestRenderer.getPublicRootInstance(root); }, + unstable_flushAll: TestRendererScheduling.flushAll, unstable_flushSync(fn: Function) { - yieldedValues = []; - TestRenderer.flushSync(fn); - return yieldedValues; + return TestRendererScheduling.withCleanYields(() => { + TestRenderer.flushSync(fn); + }); }, + unstable_flushThrough: TestRendererScheduling.flushThrough, + unstable_yield: TestRendererScheduling.yieldValue, }; Object.defineProperty( @@ -771,9 +500,7 @@ const ReactTestRendererFiber = { unstable_batchedUpdates: batchedUpdates, /* eslint-enable camelcase */ - unstable_setNowImplementation(implementation: () => number): void { - nowImplementation = implementation; - }, + unstable_setNowImplementation: TestRendererScheduling.setNowImplementation, }; export default ReactTestRendererFiber; diff --git a/packages/react-test-renderer/src/ReactTestRendererScheduling.js b/packages/react-test-renderer/src/ReactTestRendererScheduling.js new file mode 100644 index 0000000000000..dfa7616a3b366 --- /dev/null +++ b/packages/react-test-renderer/src/ReactTestRendererScheduling.js @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Deadline} from 'react-reconciler/src/ReactFiberReconciler'; + +// Current virtual time +export let nowImplementation = () => 0; +export let scheduledCallback: ((deadline: Deadline) => mixed) | null = null; +export let yieldedValues: Array | null = null; + +export function scheduleDeferredCallback( + callback: (deadline: Deadline) => mixed, + options?: {timeout: number}, +): number { + scheduledCallback = callback; + return 0; +} + +export function cancelDeferredCallback(timeoutID: number): void { + scheduledCallback = null; +} + +export function setNowImplementation(implementation: () => number): void { + nowImplementation = implementation; +} + +export function flushAll(): Array { + yieldedValues = null; + while (scheduledCallback !== null) { + const cb = scheduledCallback; + scheduledCallback = null; + cb({ + timeRemaining() { + // Keep rendering until there's no more work + return 999; + }, + // React's scheduler has its own way of keeping track of expired + // work and doesn't read this, so don't bother setting it to the + // correct value. + didTimeout: false, + }); + } + if (yieldedValues === null) { + // Always return an array. + return []; + } + return yieldedValues; +} + +export function flushThrough(expectedValues: Array): Array { + let didStop = false; + yieldedValues = null; + while (scheduledCallback !== null && !didStop) { + const cb = scheduledCallback; + scheduledCallback = null; + cb({ + timeRemaining() { + if ( + yieldedValues !== null && + yieldedValues.length >= expectedValues.length + ) { + // We at least as many values as expected. Stop rendering. + didStop = true; + return 0; + } + // Keep rendering. + return 999; + }, + // React's scheduler has its own way of keeping track of expired + // work and doesn't read this, so don't bother setting it to the + // correct value. + didTimeout: false, + }); + } + if (yieldedValues === null) { + // Always return an array. + return []; + } + return yieldedValues; +} + +export function yieldValue(value: mixed): void { + if (yieldedValues === null) { + yieldedValues = [value]; + } else { + yieldedValues.push(value); + } +} + +export function withCleanYields(fn: Function) { + yieldedValues = []; + fn(); + return yieldedValues; +} From 22c26cbfc737094f7b838bd83e36b9cc65f666b3 Mon Sep 17 00:00:00 2001 From: Dan Date: Sat, 12 May 2018 22:11:08 +0100 Subject: [PATCH 2/5] Separate ART renderer host config --- packages/react-art/src/ReactART.js | 410 +------------------ packages/react-art/src/ReactARTHostConfig.js | 390 ++++++++++++++++++ packages/react-art/src/ReactARTInternals.js | 34 ++ 3 files changed, 429 insertions(+), 405 deletions(-) create mode 100644 packages/react-art/src/ReactARTHostConfig.js create mode 100644 packages/react-art/src/ReactARTInternals.js diff --git a/packages/react-art/src/ReactART.js b/packages/react-art/src/ReactART.js index a7f9a3064ce1a..d24395c462268 100644 --- a/packages/react-art/src/ReactART.js +++ b/packages/react-art/src/ReactART.js @@ -7,267 +7,18 @@ import React from 'react'; import ReactFiberReconciler from 'react-reconciler'; -import * as ReactScheduler from 'react-scheduler'; +import Transform from 'art/core/transform'; import Mode from 'art/modes/current'; import FastNoSideEffects from 'art/modes/fast-noSideEffects'; -import Transform from 'art/core/transform'; -import invariant from 'fbjs/lib/invariant'; -import emptyObject from 'fbjs/lib/emptyObject'; + +import ReactARTHostConfig from './ReactARTHostConfig'; +import {TYPES, childrenAsString} from './ReactARTInternals'; Mode.setCurrent( // Change to 'art/modes/dom' for easier debugging via SVG FastNoSideEffects, ); -const pooledTransform = new Transform(); - -const EVENT_TYPES = { - onClick: 'click', - onMouseMove: 'mousemove', - onMouseOver: 'mouseover', - onMouseOut: 'mouseout', - onMouseUp: 'mouseup', - onMouseDown: 'mousedown', -}; - -const TYPES = { - CLIPPING_RECTANGLE: 'ClippingRectangle', - GROUP: 'Group', - SHAPE: 'Shape', - TEXT: 'Text', -}; - -const 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 childrenAsString(children) { - if (!children) { - return ''; - } else if (typeof children === 'string') { - return children; - } else if (children.length) { - return children.join(''); - } else { - return ''; - } -} - -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 (let 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 (let 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; - } -} - /** Declarative fill-type objects; API design not finalized */ const slice = Array.prototype.slice; @@ -383,158 +134,7 @@ class Text extends React.Component { /** ART Renderer */ -const ARTRenderer = ReactFiberReconciler({ - appendInitialChild(parentInstance, child) { - if (typeof child === 'string') { - // Noop for string children of Text (eg {'foo'}{'bar'}) - invariant(false, 'Text children should already be flattened.'); - return; - } - - child.inject(parentInstance); - }, - - 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; - } - - invariant(instance, 'ReactART does not support the type "%s"', type); - - instance._applyProps(instance, props); - - return instance; - }, - - createTextInstance(text, rootContainerInstance, internalInstanceHandle) { - return text; - }, - - finalizeInitialChildren(domElement, type, props) { - return false; - }, - - getPublicInstance(instance) { - return instance; - }, - - prepareForCommit() { - // Noop - }, - - prepareUpdate(domElement, type, oldProps, newProps) { - return UPDATE_SIGNAL; - }, - - resetAfterCommit() { - // Noop - }, - - resetTextContent(domElement) { - // Noop - }, - - shouldDeprioritizeSubtree(type, props) { - return false; - }, - - getRootHostContext() { - return emptyObject; - }, - - getChildHostContext() { - return emptyObject; - }, - - scheduleDeferredCallback: ReactScheduler.rIC, - - shouldSetTextContent(type, props) { - return ( - typeof props.children === 'string' || typeof props.children === 'number' - ); - }, - - now: ReactScheduler.now, - - // The ART renderer is secondary to the React DOM renderer. - isPrimaryRenderer: false, - - mutation: { - appendChild(parentInstance, child) { - if (child.parentNode === parentInstance) { - child.eject(); - } - child.inject(parentInstance); - }, - - appendChildToContainer(parentInstance, child) { - if (child.parentNode === parentInstance) { - child.eject(); - } - child.inject(parentInstance); - }, - - insertBefore(parentInstance, child, beforeChild) { - invariant( - child !== beforeChild, - 'ReactART: Can not insert node before itself', - ); - child.injectBefore(beforeChild); - }, - - insertInContainerBefore(parentInstance, child, beforeChild) { - invariant( - child !== beforeChild, - 'ReactART: Can not insert node before itself', - ); - child.injectBefore(beforeChild); - }, - - removeChild(parentInstance, child) { - destroyEventListeners(child); - child.eject(); - }, - - removeChildFromContainer(parentInstance, child) { - destroyEventListeners(child); - child.eject(); - }, - - commitTextUpdate(textInstance, oldText, newText) { - // Noop - }, - - commitMount(instance, type, newProps) { - // Noop - }, - - commitUpdate(instance, updatePayload, type, oldProps, newProps) { - instance._applyProps(instance, newProps, oldProps); - }, - }, -}); +const ARTRenderer = ReactFiberReconciler(ReactARTHostConfig); /** API */ diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js new file mode 100644 index 0000000000000..cd059537cccb7 --- /dev/null +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -0,0 +1,390 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as ReactScheduler from 'react-scheduler'; +import Transform from 'art/core/transform'; +import Mode from 'art/modes/current'; +import invariant from 'fbjs/lib/invariant'; +import emptyObject from 'fbjs/lib/emptyObject'; + +import {TYPES, EVENT_TYPES, childrenAsString} from './ReactARTInternals'; + +const pooledTransform = new Transform(); + +const 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 (let 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 (let 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; + } +} + +const ReactARTHostConfig = { + appendInitialChild(parentInstance, child) { + if (typeof child === 'string') { + // Noop for string children of Text (eg {'foo'}{'bar'}) + invariant(false, 'Text children should already be flattened.'); + return; + } + + child.inject(parentInstance); + }, + + 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; + } + + invariant(instance, 'ReactART does not support the type "%s"', type); + + instance._applyProps(instance, props); + + return instance; + }, + + createTextInstance(text, rootContainerInstance, internalInstanceHandle) { + return text; + }, + + finalizeInitialChildren(domElement, type, props) { + return false; + }, + + getPublicInstance(instance) { + return instance; + }, + + prepareForCommit() { + // Noop + }, + + prepareUpdate(domElement, type, oldProps, newProps) { + return UPDATE_SIGNAL; + }, + + resetAfterCommit() { + // Noop + }, + + resetTextContent(domElement) { + // Noop + }, + + shouldDeprioritizeSubtree(type, props) { + return false; + }, + + getRootHostContext() { + return emptyObject; + }, + + getChildHostContext() { + return emptyObject; + }, + + scheduleDeferredCallback: ReactScheduler.rIC, + + shouldSetTextContent(type, props) { + return ( + typeof props.children === 'string' || typeof props.children === 'number' + ); + }, + + now: ReactScheduler.now, + + // The ART renderer is secondary to the React DOM renderer. + isPrimaryRenderer: false, + + mutation: { + appendChild(parentInstance, child) { + if (child.parentNode === parentInstance) { + child.eject(); + } + child.inject(parentInstance); + }, + + appendChildToContainer(parentInstance, child) { + if (child.parentNode === parentInstance) { + child.eject(); + } + child.inject(parentInstance); + }, + + insertBefore(parentInstance, child, beforeChild) { + invariant( + child !== beforeChild, + 'ReactART: Can not insert node before itself', + ); + child.injectBefore(beforeChild); + }, + + insertInContainerBefore(parentInstance, child, beforeChild) { + invariant( + child !== beforeChild, + 'ReactART: Can not insert node before itself', + ); + child.injectBefore(beforeChild); + }, + + removeChild(parentInstance, child) { + destroyEventListeners(child); + child.eject(); + }, + + removeChildFromContainer(parentInstance, child) { + destroyEventListeners(child); + child.eject(); + }, + + commitTextUpdate(textInstance, oldText, newText) { + // Noop + }, + + commitMount(instance, type, newProps) { + // Noop + }, + + commitUpdate(instance, updatePayload, type, oldProps, newProps) { + instance._applyProps(instance, newProps, oldProps); + }, + }, +}; + +export default ReactARTHostConfig; diff --git a/packages/react-art/src/ReactARTInternals.js b/packages/react-art/src/ReactARTInternals.js new file mode 100644 index 0000000000000..b31c9c896496f --- /dev/null +++ b/packages/react-art/src/ReactARTInternals.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export const TYPES = { + CLIPPING_RECTANGLE: 'ClippingRectangle', + GROUP: 'Group', + SHAPE: 'Shape', + TEXT: 'Text', +}; + +export const EVENT_TYPES = { + onClick: 'click', + onMouseMove: 'mousemove', + onMouseOver: 'mouseover', + onMouseOut: 'mouseout', + onMouseUp: 'mouseup', + onMouseDown: 'mousedown', +}; + +export function childrenAsString(children) { + if (!children) { + return ''; + } else if (typeof children === 'string') { + return children; + } else if (children.length) { + return children.join(''); + } else { + return ''; + } +} From 11cba36b4f574b0c1272a7d521fb276a65161415 Mon Sep 17 00:00:00 2001 From: Dan Date: Sat, 12 May 2018 22:27:16 +0100 Subject: [PATCH 3/5] Separate ReactDOM host config --- packages/react-dom/src/client/ReactDOM.js | 549 +---------------- .../src/client/ReactDOMHostConfig.js | 568 ++++++++++++++++++ 2 files changed, 572 insertions(+), 545 deletions(-) create mode 100644 packages/react-dom/src/client/ReactDOMHostConfig.js diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index 81683c47748e3..40450e5f5eef4 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -14,12 +14,12 @@ import type { FiberRoot, Batch as FiberRootBatch, } from 'react-reconciler/src/ReactFiberRoot'; +import type {Container} from './ReactDOMHostConfig'; import '../shared/checkReact'; import './ReactDOMClientInjection'; import ReactFiberReconciler from 'react-reconciler'; -// TODO: direct imports like some-package/src/* are bad. Fix me. import * as ReactPortal from 'shared/ReactPortal'; import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'; import * as ReactGenericBatching from 'events/ReactGenericBatching'; @@ -29,53 +29,29 @@ import * as EventPluginRegistry from 'events/EventPluginRegistry'; import * as EventPropagators from 'events/EventPropagators'; import * as ReactInstanceMap from 'shared/ReactInstanceMap'; import ReactVersion from 'shared/ReactVersion'; -import * as ReactScheduler from 'react-scheduler'; import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState'; import getComponentName from 'shared/getComponentName'; import invariant from 'fbjs/lib/invariant'; import lowPriorityWarning from 'shared/lowPriorityWarning'; import warning from 'fbjs/lib/warning'; +import ReactDOMHostConfig from './ReactDOMHostConfig'; import * as ReactDOMComponentTree from './ReactDOMComponentTree'; import * as ReactDOMFiberComponent from './ReactDOMFiberComponent'; -import * as ReactInputSelection from './ReactInputSelection'; -import setTextContent from './setTextContent'; -import validateDOMNesting from './validateDOMNesting'; -import * as ReactBrowserEventEmitter from '../events/ReactBrowserEventEmitter'; import * as ReactDOMEventListener from '../events/ReactDOMEventListener'; -import {getChildNamespace} from '../shared/DOMNamespaces'; import { ELEMENT_NODE, - TEXT_NODE, COMMENT_NODE, DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE, } from '../shared/HTMLNodeType'; import {ROOT_ATTRIBUTE_NAME} from '../shared/DOMProperty'; -const { - createElement, - createTextNode, - setInitialProperties, - diffProperties, - updateProperties, - diffHydratedProperties, - diffHydratedText, - warnForUnmatchedText, - warnForDeletedHydratableElement, - warnForDeletedHydratableText, - warnForInsertedHydratedElement, - warnForInsertedHydratedText, -} = ReactDOMFiberComponent; -const {updatedAncestorInfo} = validateDOMNesting; -const {precacheFiberNode, updateFiberProps} = ReactDOMComponentTree; - -let SUPPRESS_HYDRATION_WARNING; + let topLevelUpdateWarnings; let warnOnInvalidCallback; let didWarnAboutUnstableCreatePortal = false; if (__DEV__) { - SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning'; if ( typeof Map !== 'function' || Map.prototype == null || @@ -157,26 +133,6 @@ type DOMContainer = _reactRootContainer: ?Root, }); -type Container = Element | Document; -type Props = { - autoFocus?: boolean, - children?: mixed, - hidden?: boolean, - suppressHydrationWarning?: boolean, -}; -type Instance = Element; -type TextInstance = Text; - -type HostContextDev = { - namespace: string, - ancestorInfo: mixed, -}; -type HostContextProd = string; -type HostContext = HostContextDev | HostContextProd; - -let eventsEnabled: ?boolean = null; -let selectionInformation: ?mixed = null; - type Batch = FiberRootBatch & { render(children: ReactNodeList): Work, then(onComplete: () => mixed): void, @@ -491,504 +447,7 @@ function shouldHydrateDueToLegacyHeuristic(container) { ); } -function shouldAutoFocusHostComponent(type: string, props: Props): boolean { - switch (type) { - case 'button': - case 'input': - case 'select': - case 'textarea': - return !!props.autoFocus; - } - return false; -} - -const DOMRenderer = ReactFiberReconciler({ - getRootHostContext(rootContainerInstance: Container): HostContext { - let type; - let namespace; - const nodeType = rootContainerInstance.nodeType; - switch (nodeType) { - case DOCUMENT_NODE: - case DOCUMENT_FRAGMENT_NODE: { - type = nodeType === DOCUMENT_NODE ? '#document' : '#fragment'; - let root = (rootContainerInstance: any).documentElement; - namespace = root ? root.namespaceURI : getChildNamespace(null, ''); - break; - } - default: { - const container: any = - nodeType === COMMENT_NODE - ? rootContainerInstance.parentNode - : rootContainerInstance; - const ownNamespace = container.namespaceURI || null; - type = container.tagName; - namespace = getChildNamespace(ownNamespace, type); - break; - } - } - if (__DEV__) { - const validatedTag = type.toLowerCase(); - const ancestorInfo = updatedAncestorInfo(null, validatedTag, null); - return {namespace, ancestorInfo}; - } - return namespace; - }, - - getChildHostContext( - parentHostContext: HostContext, - type: string, - ): HostContext { - if (__DEV__) { - const parentHostContextDev = ((parentHostContext: any): HostContextDev); - const namespace = getChildNamespace(parentHostContextDev.namespace, type); - const ancestorInfo = updatedAncestorInfo( - parentHostContextDev.ancestorInfo, - type, - null, - ); - return {namespace, ancestorInfo}; - } - const parentNamespace = ((parentHostContext: any): HostContextProd); - return getChildNamespace(parentNamespace, type); - }, - - getPublicInstance(instance) { - return instance; - }, - - prepareForCommit(): void { - eventsEnabled = ReactBrowserEventEmitter.isEnabled(); - selectionInformation = ReactInputSelection.getSelectionInformation(); - ReactBrowserEventEmitter.setEnabled(false); - }, - - resetAfterCommit(): void { - ReactInputSelection.restoreSelection(selectionInformation); - selectionInformation = null; - ReactBrowserEventEmitter.setEnabled(eventsEnabled); - eventsEnabled = null; - }, - - createInstance( - type: string, - props: Props, - rootContainerInstance: Container, - hostContext: HostContext, - internalInstanceHandle: Object, - ): Instance { - let parentNamespace: string; - if (__DEV__) { - // TODO: take namespace into account when validating. - const hostContextDev = ((hostContext: any): HostContextDev); - validateDOMNesting(type, null, hostContextDev.ancestorInfo); - if ( - typeof props.children === 'string' || - typeof props.children === 'number' - ) { - const string = '' + props.children; - const ownAncestorInfo = updatedAncestorInfo( - hostContextDev.ancestorInfo, - type, - null, - ); - validateDOMNesting(null, string, ownAncestorInfo); - } - parentNamespace = hostContextDev.namespace; - } else { - parentNamespace = ((hostContext: any): HostContextProd); - } - const domElement: Instance = createElement( - type, - props, - rootContainerInstance, - parentNamespace, - ); - precacheFiberNode(internalInstanceHandle, domElement); - updateFiberProps(domElement, props); - return domElement; - }, - - appendInitialChild( - parentInstance: Instance, - child: Instance | TextInstance, - ): void { - parentInstance.appendChild(child); - }, - - finalizeInitialChildren( - domElement: Instance, - type: string, - props: Props, - rootContainerInstance: Container, - ): boolean { - setInitialProperties(domElement, type, props, rootContainerInstance); - return shouldAutoFocusHostComponent(type, props); - }, - - prepareUpdate( - domElement: Instance, - type: string, - oldProps: Props, - newProps: Props, - rootContainerInstance: Container, - hostContext: HostContext, - ): null | Array { - if (__DEV__) { - const hostContextDev = ((hostContext: any): HostContextDev); - if ( - typeof newProps.children !== typeof oldProps.children && - (typeof newProps.children === 'string' || - typeof newProps.children === 'number') - ) { - const string = '' + newProps.children; - const ownAncestorInfo = updatedAncestorInfo( - hostContextDev.ancestorInfo, - type, - null, - ); - validateDOMNesting(null, string, ownAncestorInfo); - } - } - return diffProperties( - domElement, - type, - oldProps, - newProps, - rootContainerInstance, - ); - }, - - shouldSetTextContent(type: string, props: Props): boolean { - return ( - type === 'textarea' || - typeof props.children === 'string' || - typeof props.children === 'number' || - (typeof props.dangerouslySetInnerHTML === 'object' && - props.dangerouslySetInnerHTML !== null && - typeof props.dangerouslySetInnerHTML.__html === 'string') - ); - }, - - shouldDeprioritizeSubtree(type: string, props: Props): boolean { - return !!props.hidden; - }, - - createTextInstance( - text: string, - rootContainerInstance: Container, - hostContext: HostContext, - internalInstanceHandle: Object, - ): TextInstance { - if (__DEV__) { - const hostContextDev = ((hostContext: any): HostContextDev); - validateDOMNesting(null, text, hostContextDev.ancestorInfo); - } - const textNode: TextInstance = createTextNode(text, rootContainerInstance); - precacheFiberNode(internalInstanceHandle, textNode); - return textNode; - }, - - now: ReactScheduler.now, - - isPrimaryRenderer: true, - - mutation: { - commitMount( - domElement: Instance, - type: string, - newProps: Props, - internalInstanceHandle: Object, - ): void { - // Despite the naming that might imply otherwise, this method only - // fires if there is an `Update` effect scheduled during mounting. - // This happens if `finalizeInitialChildren` returns `true` (which it - // does to implement the `autoFocus` attribute on the client). But - // there are also other cases when this might happen (such as patching - // up text content during hydration mismatch). So we'll check this again. - if (shouldAutoFocusHostComponent(type, newProps)) { - ((domElement: any): - | HTMLButtonElement - | HTMLInputElement - | HTMLSelectElement - | HTMLTextAreaElement).focus(); - } - }, - - commitUpdate( - domElement: Instance, - updatePayload: Array, - type: string, - oldProps: Props, - newProps: Props, - internalInstanceHandle: Object, - ): void { - // Update the props handle so that we know which props are the ones with - // with current event handlers. - updateFiberProps(domElement, newProps); - // Apply the diff to the DOM node. - updateProperties(domElement, updatePayload, type, oldProps, newProps); - }, - - resetTextContent(domElement: Instance): void { - setTextContent(domElement, ''); - }, - - commitTextUpdate( - textInstance: TextInstance, - oldText: string, - newText: string, - ): void { - textInstance.nodeValue = newText; - }, - - appendChild( - parentInstance: Instance, - child: Instance | TextInstance, - ): void { - parentInstance.appendChild(child); - }, - - appendChildToContainer( - container: Container, - child: Instance | TextInstance, - ): void { - if (container.nodeType === COMMENT_NODE) { - (container.parentNode: any).insertBefore(child, container); - } else { - container.appendChild(child); - } - }, - - insertBefore( - parentInstance: Instance, - child: Instance | TextInstance, - beforeChild: Instance | TextInstance, - ): void { - parentInstance.insertBefore(child, beforeChild); - }, - - insertInContainerBefore( - container: Container, - child: Instance | TextInstance, - beforeChild: Instance | TextInstance, - ): void { - if (container.nodeType === COMMENT_NODE) { - (container.parentNode: any).insertBefore(child, beforeChild); - } else { - container.insertBefore(child, beforeChild); - } - }, - - removeChild( - parentInstance: Instance, - child: Instance | TextInstance, - ): void { - parentInstance.removeChild(child); - }, - - removeChildFromContainer( - container: Container, - child: Instance | TextInstance, - ): void { - if (container.nodeType === COMMENT_NODE) { - (container.parentNode: any).removeChild(child); - } else { - container.removeChild(child); - } - }, - }, - - hydration: { - canHydrateInstance( - instance: Instance | TextInstance, - type: string, - props: Props, - ): null | Instance { - if ( - instance.nodeType !== ELEMENT_NODE || - type.toLowerCase() !== instance.nodeName.toLowerCase() - ) { - return null; - } - // This has now been refined to an element node. - return ((instance: any): Instance); - }, - - canHydrateTextInstance( - instance: Instance | TextInstance, - text: string, - ): null | TextInstance { - if (text === '' || instance.nodeType !== TEXT_NODE) { - // Empty strings are not parsed by HTML so there won't be a correct match here. - return null; - } - // This has now been refined to a text node. - return ((instance: any): TextInstance); - }, - - getNextHydratableSibling( - instance: Instance | TextInstance, - ): null | Instance | TextInstance { - let node = instance.nextSibling; - // Skip non-hydratable nodes. - while ( - node && - node.nodeType !== ELEMENT_NODE && - node.nodeType !== TEXT_NODE - ) { - node = node.nextSibling; - } - return (node: any); - }, - - getFirstHydratableChild( - parentInstance: Container | Instance, - ): null | Instance | TextInstance { - let next = parentInstance.firstChild; - // Skip non-hydratable nodes. - while ( - next && - next.nodeType !== ELEMENT_NODE && - next.nodeType !== TEXT_NODE - ) { - next = next.nextSibling; - } - return (next: any); - }, - - hydrateInstance( - instance: Instance, - type: string, - props: Props, - rootContainerInstance: Container, - hostContext: HostContext, - internalInstanceHandle: Object, - ): null | Array { - precacheFiberNode(internalInstanceHandle, instance); - // TODO: Possibly defer this until the commit phase where all the events - // get attached. - updateFiberProps(instance, props); - let parentNamespace: string; - if (__DEV__) { - const hostContextDev = ((hostContext: any): HostContextDev); - parentNamespace = hostContextDev.namespace; - } else { - parentNamespace = ((hostContext: any): HostContextProd); - } - return diffHydratedProperties( - instance, - type, - props, - parentNamespace, - rootContainerInstance, - ); - }, - - hydrateTextInstance( - textInstance: TextInstance, - text: string, - internalInstanceHandle: Object, - ): boolean { - precacheFiberNode(internalInstanceHandle, textInstance); - return diffHydratedText(textInstance, text); - }, - - didNotMatchHydratedContainerTextInstance( - parentContainer: Container, - textInstance: TextInstance, - text: string, - ) { - if (__DEV__) { - warnForUnmatchedText(textInstance, text); - } - }, - - didNotMatchHydratedTextInstance( - parentType: string, - parentProps: Props, - parentInstance: Instance, - textInstance: TextInstance, - text: string, - ) { - if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { - warnForUnmatchedText(textInstance, text); - } - }, - - didNotHydrateContainerInstance( - parentContainer: Container, - instance: Instance | TextInstance, - ) { - if (__DEV__) { - if (instance.nodeType === 1) { - warnForDeletedHydratableElement(parentContainer, (instance: any)); - } else { - warnForDeletedHydratableText(parentContainer, (instance: any)); - } - } - }, - - didNotHydrateInstance( - parentType: string, - parentProps: Props, - parentInstance: Instance, - instance: Instance | TextInstance, - ) { - if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { - if (instance.nodeType === 1) { - warnForDeletedHydratableElement(parentInstance, (instance: any)); - } else { - warnForDeletedHydratableText(parentInstance, (instance: any)); - } - } - }, - - didNotFindHydratableContainerInstance( - parentContainer: Container, - type: string, - props: Props, - ) { - if (__DEV__) { - warnForInsertedHydratedElement(parentContainer, type, props); - } - }, - - didNotFindHydratableContainerTextInstance( - parentContainer: Container, - text: string, - ) { - if (__DEV__) { - warnForInsertedHydratedText(parentContainer, text); - } - }, - - didNotFindHydratableInstance( - parentType: string, - parentProps: Props, - parentInstance: Instance, - type: string, - props: Props, - ) { - if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { - warnForInsertedHydratedElement(parentInstance, type, props); - } - }, - - didNotFindHydratableTextInstance( - parentType: string, - parentProps: Props, - parentInstance: Instance, - text: string, - ) { - if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { - warnForInsertedHydratedText(parentInstance, text); - } - }, - }, - - scheduleDeferredCallback: ReactScheduler.rIC, - cancelDeferredCallback: ReactScheduler.cIC, -}); +const DOMRenderer = ReactFiberReconciler(ReactDOMHostConfig); ReactGenericBatching.injection.injectRenderer(DOMRenderer); diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js new file mode 100644 index 0000000000000..95471f2279da1 --- /dev/null +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -0,0 +1,568 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import * as ReactScheduler from 'react-scheduler'; + +import * as ReactDOMComponentTree from './ReactDOMComponentTree'; +import * as ReactDOMFiberComponent from './ReactDOMFiberComponent'; +import * as ReactInputSelection from './ReactInputSelection'; +import setTextContent from './setTextContent'; +import validateDOMNesting from './validateDOMNesting'; +import * as ReactBrowserEventEmitter from '../events/ReactBrowserEventEmitter'; +import {getChildNamespace} from '../shared/DOMNamespaces'; +import { + ELEMENT_NODE, + TEXT_NODE, + COMMENT_NODE, + DOCUMENT_NODE, + DOCUMENT_FRAGMENT_NODE, +} from '../shared/HTMLNodeType'; + +export type Container = Element | Document; +type Props = { + autoFocus?: boolean, + children?: mixed, + hidden?: boolean, + suppressHydrationWarning?: boolean, +}; +type Instance = Element; +type TextInstance = Text; + +type HostContextDev = { + namespace: string, + ancestorInfo: mixed, +}; +type HostContextProd = string; +type HostContext = HostContextDev | HostContextProd; + +const { + createElement, + createTextNode, + setInitialProperties, + diffProperties, + updateProperties, + diffHydratedProperties, + diffHydratedText, + warnForUnmatchedText, + warnForDeletedHydratableElement, + warnForDeletedHydratableText, + warnForInsertedHydratedElement, + warnForInsertedHydratedText, +} = ReactDOMFiberComponent; +const {updatedAncestorInfo} = validateDOMNesting; +const {precacheFiberNode, updateFiberProps} = ReactDOMComponentTree; + +let SUPPRESS_HYDRATION_WARNING; +if (__DEV__) { + SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning'; +} + +let eventsEnabled: ?boolean = null; +let selectionInformation: ?mixed = null; + +function shouldAutoFocusHostComponent(type: string, props: Props): boolean { + switch (type) { + case 'button': + case 'input': + case 'select': + case 'textarea': + return !!props.autoFocus; + } + return false; +} + +const ReactDOMHostConfig = { + getRootHostContext(rootContainerInstance: Container): HostContext { + let type; + let namespace; + const nodeType = rootContainerInstance.nodeType; + switch (nodeType) { + case DOCUMENT_NODE: + case DOCUMENT_FRAGMENT_NODE: { + type = nodeType === DOCUMENT_NODE ? '#document' : '#fragment'; + let root = (rootContainerInstance: any).documentElement; + namespace = root ? root.namespaceURI : getChildNamespace(null, ''); + break; + } + default: { + const container: any = + nodeType === COMMENT_NODE + ? rootContainerInstance.parentNode + : rootContainerInstance; + const ownNamespace = container.namespaceURI || null; + type = container.tagName; + namespace = getChildNamespace(ownNamespace, type); + break; + } + } + if (__DEV__) { + const validatedTag = type.toLowerCase(); + const ancestorInfo = updatedAncestorInfo(null, validatedTag, null); + return {namespace, ancestorInfo}; + } + return namespace; + }, + + getChildHostContext( + parentHostContext: HostContext, + type: string, + ): HostContext { + if (__DEV__) { + const parentHostContextDev = ((parentHostContext: any): HostContextDev); + const namespace = getChildNamespace(parentHostContextDev.namespace, type); + const ancestorInfo = updatedAncestorInfo( + parentHostContextDev.ancestorInfo, + type, + null, + ); + return {namespace, ancestorInfo}; + } + const parentNamespace = ((parentHostContext: any): HostContextProd); + return getChildNamespace(parentNamespace, type); + }, + + getPublicInstance(instance: Instance | TextInstance): * { + return instance; + }, + + prepareForCommit(): void { + eventsEnabled = ReactBrowserEventEmitter.isEnabled(); + selectionInformation = ReactInputSelection.getSelectionInformation(); + ReactBrowserEventEmitter.setEnabled(false); + }, + + resetAfterCommit(): void { + ReactInputSelection.restoreSelection(selectionInformation); + selectionInformation = null; + ReactBrowserEventEmitter.setEnabled(eventsEnabled); + eventsEnabled = null; + }, + + createInstance( + type: string, + props: Props, + rootContainerInstance: Container, + hostContext: HostContext, + internalInstanceHandle: Object, + ): Instance { + let parentNamespace: string; + if (__DEV__) { + // TODO: take namespace into account when validating. + const hostContextDev = ((hostContext: any): HostContextDev); + validateDOMNesting(type, null, hostContextDev.ancestorInfo); + if ( + typeof props.children === 'string' || + typeof props.children === 'number' + ) { + const string = '' + props.children; + const ownAncestorInfo = updatedAncestorInfo( + hostContextDev.ancestorInfo, + type, + null, + ); + validateDOMNesting(null, string, ownAncestorInfo); + } + parentNamespace = hostContextDev.namespace; + } else { + parentNamespace = ((hostContext: any): HostContextProd); + } + const domElement: Instance = createElement( + type, + props, + rootContainerInstance, + parentNamespace, + ); + precacheFiberNode(internalInstanceHandle, domElement); + updateFiberProps(domElement, props); + return domElement; + }, + + appendInitialChild( + parentInstance: Instance, + child: Instance | TextInstance, + ): void { + parentInstance.appendChild(child); + }, + + finalizeInitialChildren( + domElement: Instance, + type: string, + props: Props, + rootContainerInstance: Container, + ): boolean { + setInitialProperties(domElement, type, props, rootContainerInstance); + return shouldAutoFocusHostComponent(type, props); + }, + + prepareUpdate( + domElement: Instance, + type: string, + oldProps: Props, + newProps: Props, + rootContainerInstance: Container, + hostContext: HostContext, + ): null | Array { + if (__DEV__) { + const hostContextDev = ((hostContext: any): HostContextDev); + if ( + typeof newProps.children !== typeof oldProps.children && + (typeof newProps.children === 'string' || + typeof newProps.children === 'number') + ) { + const string = '' + newProps.children; + const ownAncestorInfo = updatedAncestorInfo( + hostContextDev.ancestorInfo, + type, + null, + ); + validateDOMNesting(null, string, ownAncestorInfo); + } + } + return diffProperties( + domElement, + type, + oldProps, + newProps, + rootContainerInstance, + ); + }, + + shouldSetTextContent(type: string, props: Props): boolean { + return ( + type === 'textarea' || + typeof props.children === 'string' || + typeof props.children === 'number' || + (typeof props.dangerouslySetInnerHTML === 'object' && + props.dangerouslySetInnerHTML !== null && + typeof props.dangerouslySetInnerHTML.__html === 'string') + ); + }, + + shouldDeprioritizeSubtree(type: string, props: Props): boolean { + return !!props.hidden; + }, + + createTextInstance( + text: string, + rootContainerInstance: Container, + hostContext: HostContext, + internalInstanceHandle: Object, + ): TextInstance { + if (__DEV__) { + const hostContextDev = ((hostContext: any): HostContextDev); + validateDOMNesting(null, text, hostContextDev.ancestorInfo); + } + const textNode: TextInstance = createTextNode(text, rootContainerInstance); + precacheFiberNode(internalInstanceHandle, textNode); + return textNode; + }, + + now: ReactScheduler.now, + + isPrimaryRenderer: true, + + mutation: { + commitMount( + domElement: Instance, + type: string, + newProps: Props, + internalInstanceHandle: Object, + ): void { + // Despite the naming that might imply otherwise, this method only + // fires if there is an `Update` effect scheduled during mounting. + // This happens if `finalizeInitialChildren` returns `true` (which it + // does to implement the `autoFocus` attribute on the client). But + // there are also other cases when this might happen (such as patching + // up text content during hydration mismatch). So we'll check this again. + if (shouldAutoFocusHostComponent(type, newProps)) { + ((domElement: any): + | HTMLButtonElement + | HTMLInputElement + | HTMLSelectElement + | HTMLTextAreaElement).focus(); + } + }, + + commitUpdate( + domElement: Instance, + updatePayload: Array, + type: string, + oldProps: Props, + newProps: Props, + internalInstanceHandle: Object, + ): void { + // Update the props handle so that we know which props are the ones with + // with current event handlers. + updateFiberProps(domElement, newProps); + // Apply the diff to the DOM node. + updateProperties(domElement, updatePayload, type, oldProps, newProps); + }, + + resetTextContent(domElement: Instance): void { + setTextContent(domElement, ''); + }, + + commitTextUpdate( + textInstance: TextInstance, + oldText: string, + newText: string, + ): void { + textInstance.nodeValue = newText; + }, + + appendChild( + parentInstance: Instance, + child: Instance | TextInstance, + ): void { + parentInstance.appendChild(child); + }, + + appendChildToContainer( + container: Container, + child: Instance | TextInstance, + ): void { + if (container.nodeType === COMMENT_NODE) { + (container.parentNode: any).insertBefore(child, container); + } else { + container.appendChild(child); + } + }, + + insertBefore( + parentInstance: Instance, + child: Instance | TextInstance, + beforeChild: Instance | TextInstance, + ): void { + parentInstance.insertBefore(child, beforeChild); + }, + + insertInContainerBefore( + container: Container, + child: Instance | TextInstance, + beforeChild: Instance | TextInstance, + ): void { + if (container.nodeType === COMMENT_NODE) { + (container.parentNode: any).insertBefore(child, beforeChild); + } else { + container.insertBefore(child, beforeChild); + } + }, + + removeChild( + parentInstance: Instance, + child: Instance | TextInstance, + ): void { + parentInstance.removeChild(child); + }, + + removeChildFromContainer( + container: Container, + child: Instance | TextInstance, + ): void { + if (container.nodeType === COMMENT_NODE) { + (container.parentNode: any).removeChild(child); + } else { + container.removeChild(child); + } + }, + }, + + hydration: { + canHydrateInstance( + instance: Instance | TextInstance, + type: string, + props: Props, + ): null | Instance { + if ( + instance.nodeType !== ELEMENT_NODE || + type.toLowerCase() !== instance.nodeName.toLowerCase() + ) { + return null; + } + // This has now been refined to an element node. + return ((instance: any): Instance); + }, + + canHydrateTextInstance( + instance: Instance | TextInstance, + text: string, + ): null | TextInstance { + if (text === '' || instance.nodeType !== TEXT_NODE) { + // Empty strings are not parsed by HTML so there won't be a correct match here. + return null; + } + // This has now been refined to a text node. + return ((instance: any): TextInstance); + }, + + getNextHydratableSibling( + instance: Instance | TextInstance, + ): null | Instance | TextInstance { + let node = instance.nextSibling; + // Skip non-hydratable nodes. + while ( + node && + node.nodeType !== ELEMENT_NODE && + node.nodeType !== TEXT_NODE + ) { + node = node.nextSibling; + } + return (node: any); + }, + + getFirstHydratableChild( + parentInstance: Container | Instance, + ): null | Instance | TextInstance { + let next = parentInstance.firstChild; + // Skip non-hydratable nodes. + while ( + next && + next.nodeType !== ELEMENT_NODE && + next.nodeType !== TEXT_NODE + ) { + next = next.nextSibling; + } + return (next: any); + }, + + hydrateInstance( + instance: Instance, + type: string, + props: Props, + rootContainerInstance: Container, + hostContext: HostContext, + internalInstanceHandle: Object, + ): null | Array { + precacheFiberNode(internalInstanceHandle, instance); + // TODO: Possibly defer this until the commit phase where all the events + // get attached. + updateFiberProps(instance, props); + let parentNamespace: string; + if (__DEV__) { + const hostContextDev = ((hostContext: any): HostContextDev); + parentNamespace = hostContextDev.namespace; + } else { + parentNamespace = ((hostContext: any): HostContextProd); + } + return diffHydratedProperties( + instance, + type, + props, + parentNamespace, + rootContainerInstance, + ); + }, + + hydrateTextInstance( + textInstance: TextInstance, + text: string, + internalInstanceHandle: Object, + ): boolean { + precacheFiberNode(internalInstanceHandle, textInstance); + return diffHydratedText(textInstance, text); + }, + + didNotMatchHydratedContainerTextInstance( + parentContainer: Container, + textInstance: TextInstance, + text: string, + ) { + if (__DEV__) { + warnForUnmatchedText(textInstance, text); + } + }, + + didNotMatchHydratedTextInstance( + parentType: string, + parentProps: Props, + parentInstance: Instance, + textInstance: TextInstance, + text: string, + ) { + if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { + warnForUnmatchedText(textInstance, text); + } + }, + + didNotHydrateContainerInstance( + parentContainer: Container, + instance: Instance | TextInstance, + ) { + if (__DEV__) { + if (instance.nodeType === 1) { + warnForDeletedHydratableElement(parentContainer, (instance: any)); + } else { + warnForDeletedHydratableText(parentContainer, (instance: any)); + } + } + }, + + didNotHydrateInstance( + parentType: string, + parentProps: Props, + parentInstance: Instance, + instance: Instance | TextInstance, + ) { + if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { + if (instance.nodeType === 1) { + warnForDeletedHydratableElement(parentInstance, (instance: any)); + } else { + warnForDeletedHydratableText(parentInstance, (instance: any)); + } + } + }, + + didNotFindHydratableContainerInstance( + parentContainer: Container, + type: string, + props: Props, + ) { + if (__DEV__) { + warnForInsertedHydratedElement(parentContainer, type, props); + } + }, + + didNotFindHydratableContainerTextInstance( + parentContainer: Container, + text: string, + ) { + if (__DEV__) { + warnForInsertedHydratedText(parentContainer, text); + } + }, + + didNotFindHydratableInstance( + parentType: string, + parentProps: Props, + parentInstance: Instance, + type: string, + props: Props, + ) { + if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { + warnForInsertedHydratedElement(parentInstance, type, props); + } + }, + + didNotFindHydratableTextInstance( + parentType: string, + parentProps: Props, + parentInstance: Instance, + text: string, + ) { + if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { + warnForInsertedHydratedText(parentInstance, text); + } + }, + }, + + scheduleDeferredCallback: ReactScheduler.rIC, + cancelDeferredCallback: ReactScheduler.cIC, +}; + +export default ReactDOMHostConfig; From 57828ce5c0a5380ec9c1b29f6cfe67d4d0bf8e21 Mon Sep 17 00:00:00 2001 From: Dan Date: Sat, 12 May 2018 22:39:13 +0100 Subject: [PATCH 4/5] Extract RN Fabric host config --- .../src/ReactFabricHostConfig.js | 355 ++++++++++++++++++ .../src/ReactFabricRenderer.js | 345 +---------------- 2 files changed, 357 insertions(+), 343 deletions(-) create mode 100644 packages/react-native-renderer/src/ReactFabricHostConfig.js diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js new file mode 100644 index 0000000000000..903a6f266516d --- /dev/null +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -0,0 +1,355 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type { + MeasureInWindowOnSuccessCallback, + MeasureLayoutOnSuccessCallback, + MeasureOnSuccessCallback, + NativeMethodsMixinType, + ReactNativeBaseComponentViewConfig, +} from './ReactNativeTypes'; + +import {mountSafeCallback, warnForStyleProps} from './NativeMethodsMixinUtils'; +import * as ReactNativeAttributePayload from './ReactNativeAttributePayload'; +import * as ReactNativeFrameScheduling from './ReactNativeFrameScheduling'; +import * as ReactNativeViewConfigRegistry from 'ReactNativeViewConfigRegistry'; + +import deepFreezeAndThrowOnMutationInDev from 'deepFreezeAndThrowOnMutationInDev'; +import invariant from 'fbjs/lib/invariant'; + +// Modules provided by RN: +import TextInputState from 'TextInputState'; +import FabricUIManager from 'FabricUIManager'; +import UIManager from 'UIManager'; + +// Counter for uniquely identifying views. +// % 10 === 1 means it is a rootTag. +// % 2 === 0 means it is a Fabric tag. +// This means that they never overlap. +let nextReactTag = 2; + +type HostContext = $ReadOnly<{| + isInAParentText: boolean, +|}>; + +/** + * This is used for refs on host components. + */ +class ReactFabricHostComponent { + _nativeTag: number; + viewConfig: ReactNativeBaseComponentViewConfig; + currentProps: Props; + + constructor( + tag: number, + viewConfig: ReactNativeBaseComponentViewConfig, + props: Props, + ) { + this._nativeTag = tag; + this.viewConfig = viewConfig; + this.currentProps = props; + } + + blur() { + TextInputState.blurTextInput(this._nativeTag); + } + + focus() { + TextInputState.focusTextInput(this._nativeTag); + } + + measure(callback: MeasureOnSuccessCallback) { + UIManager.measure(this._nativeTag, mountSafeCallback(this, callback)); + } + + measureInWindow(callback: MeasureInWindowOnSuccessCallback) { + UIManager.measureInWindow( + this._nativeTag, + mountSafeCallback(this, callback), + ); + } + + measureLayout( + relativeToNativeNode: number, + onSuccess: MeasureLayoutOnSuccessCallback, + onFail: () => void /* currently unused */, + ) { + UIManager.measureLayout( + this._nativeTag, + relativeToNativeNode, + mountSafeCallback(this, onFail), + mountSafeCallback(this, onSuccess), + ); + } + + setNativeProps(nativeProps: Object) { + if (__DEV__) { + warnForStyleProps(nativeProps, this.viewConfig.validAttributes); + } + + const updatePayload = ReactNativeAttributePayload.create( + nativeProps, + this.viewConfig.validAttributes, + ); + + // Avoid the overhead of bridge calls if there's no update. + // This is an expensive no-op for Android, and causes an unnecessary + // view invalidation for certain components (eg RCTTextInput) on iOS. + if (updatePayload != null) { + UIManager.updateView( + this._nativeTag, + this.viewConfig.uiViewClassName, + updatePayload, + ); + } + } +} + +// eslint-disable-next-line no-unused-expressions +(ReactFabricHostComponent.prototype: NativeMethodsMixinType); + +type Node = Object; +type ChildSet = Object; +type Container = number; +type Instance = { + node: Node, + canonical: ReactFabricHostComponent, +}; +type Props = Object; +type TextInstance = { + node: Node, +}; + +const ReacFabricHostConfig = { + appendInitialChild( + parentInstance: Instance, + child: Instance | TextInstance, + ): void { + FabricUIManager.appendChild(parentInstance.node, child.node); + }, + + createInstance( + type: string, + props: Props, + rootContainerInstance: Container, + hostContext: HostContext, + internalInstanceHandle: Object, + ): Instance { + const tag = nextReactTag; + nextReactTag += 2; + + const viewConfig = ReactNativeViewConfigRegistry.get(type); + + if (__DEV__) { + for (const key in viewConfig.validAttributes) { + if (props.hasOwnProperty(key)) { + deepFreezeAndThrowOnMutationInDev(props[key]); + } + } + } + + invariant( + type !== 'RCTView' || !hostContext.isInAParentText, + 'Nesting of within is not currently supported.', + ); + + const updatePayload = ReactNativeAttributePayload.create( + props, + viewConfig.validAttributes, + ); + + const node = FabricUIManager.createNode( + tag, // reactTag + viewConfig.uiViewClassName, // viewName + rootContainerInstance, // rootTag + updatePayload, // props + internalInstanceHandle, // internalInstanceHandle + ); + + const component = new ReactFabricHostComponent(tag, viewConfig, props); + + return { + node: node, + canonical: component, + }; + }, + + createTextInstance( + text: string, + rootContainerInstance: Container, + hostContext: HostContext, + internalInstanceHandle: Object, + ): TextInstance { + invariant( + hostContext.isInAParentText, + 'Text strings must be rendered within a component.', + ); + + const tag = nextReactTag; + nextReactTag += 2; + + const node = FabricUIManager.createNode( + tag, // reactTag + 'RCTRawText', // viewName + rootContainerInstance, // rootTag + {text: text}, // props + internalInstanceHandle, // instance handle + ); + + return { + node: node, + }; + }, + + finalizeInitialChildren( + parentInstance: Instance, + type: string, + props: Props, + rootContainerInstance: Container, + ): boolean { + return false; + }, + + getRootHostContext(rootContainerInstance: Container): HostContext { + return {isInAParentText: false}; + }, + + getChildHostContext( + parentHostContext: HostContext, + type: string, + ): HostContext { + const prevIsInAParentText = parentHostContext.isInAParentText; + const isInAParentText = + type === 'AndroidTextInput' || // Android + type === 'RCTMultilineTextInputView' || // iOS + type === 'RCTSinglelineTextInputView' || // iOS + type === 'RCTText' || + type === 'RCTVirtualText'; + + if (prevIsInAParentText !== isInAParentText) { + return {isInAParentText}; + } else { + return parentHostContext; + } + }, + + getPublicInstance(instance) { + return instance.canonical; + }, + + now: ReactNativeFrameScheduling.now, + + // The Fabric renderer is secondary to the existing React Native renderer. + isPrimaryRenderer: false, + + prepareForCommit(): void { + // Noop + }, + + prepareUpdate( + instance: Instance, + type: string, + oldProps: Props, + newProps: Props, + rootContainerInstance: Container, + hostContext: HostContext, + ): null | Object { + const viewConfig = instance.canonical.viewConfig; + const updatePayload = ReactNativeAttributePayload.diff( + oldProps, + newProps, + viewConfig.validAttributes, + ); + // TODO: If the event handlers have changed, we need to update the current props + // in the commit phase but there is no host config hook to do it yet. + return updatePayload; + }, + + resetAfterCommit(): void { + // Noop + }, + + scheduleDeferredCallback: ReactNativeFrameScheduling.scheduleDeferredCallback, + cancelDeferredCallback: ReactNativeFrameScheduling.cancelDeferredCallback, + + shouldDeprioritizeSubtree(type: string, props: Props): boolean { + return false; + }, + + shouldSetTextContent(type: string, props: Props): boolean { + // TODO (bvaughn) Revisit this decision. + // Always returning false simplifies the createInstance() implementation, + // But creates an additional child Fiber for raw text children. + // No additional native views are created though. + // It's not clear to me which is better so I'm deferring for now. + // More context @ github.com/facebook/react/pull/8560#discussion_r92111303 + return false; + }, + + persistence: { + cloneInstance( + instance: Instance, + updatePayload: null | Object, + type: string, + oldProps: Props, + newProps: Props, + internalInstanceHandle: Object, + keepChildren: boolean, + recyclableInstance: null | Instance, + ): Instance { + const node = instance.node; + let clone; + if (keepChildren) { + if (updatePayload !== null) { + clone = FabricUIManager.cloneNodeWithNewProps(node, updatePayload); + } else { + clone = FabricUIManager.cloneNode(node); + } + } else { + if (updatePayload !== null) { + clone = FabricUIManager.cloneNodeWithNewChildrenAndProps( + node, + updatePayload, + ); + } else { + clone = FabricUIManager.cloneNodeWithNewChildren(node); + } + } + return { + node: clone, + canonical: instance.canonical, + }; + }, + + createContainerChildSet(container: Container): ChildSet { + return FabricUIManager.createChildSet(container); + }, + + appendChildToContainerChildSet( + childSet: ChildSet, + child: Instance | TextInstance, + ): void { + FabricUIManager.appendChildToSet(childSet, child.node); + }, + + finalizeContainerChildren( + container: Container, + newChildren: ChildSet, + ): void { + FabricUIManager.completeRoot(container, newChildren); + }, + + replaceContainerChildren( + container: Container, + newChildren: ChildSet, + ): void {}, + }, +}; + +export default ReacFabricHostConfig; diff --git a/packages/react-native-renderer/src/ReactFabricRenderer.js b/packages/react-native-renderer/src/ReactFabricRenderer.js index 9c597c2e53201..8f77d283c9a2e 100644 --- a/packages/react-native-renderer/src/ReactFabricRenderer.js +++ b/packages/react-native-renderer/src/ReactFabricRenderer.js @@ -7,350 +7,9 @@ * @flow */ -import type { - MeasureInWindowOnSuccessCallback, - MeasureLayoutOnSuccessCallback, - MeasureOnSuccessCallback, - NativeMethodsMixinType, - ReactNativeBaseComponentViewConfig, -} from './ReactNativeTypes'; - -import {mountSafeCallback, warnForStyleProps} from './NativeMethodsMixinUtils'; -import * as ReactNativeAttributePayload from './ReactNativeAttributePayload'; -import * as ReactNativeFrameScheduling from './ReactNativeFrameScheduling'; -import * as ReactNativeViewConfigRegistry from 'ReactNativeViewConfigRegistry'; import ReactFiberReconciler from 'react-reconciler'; +import ReactFabricHostConfig from './ReactFabricHostConfig'; -import deepFreezeAndThrowOnMutationInDev from 'deepFreezeAndThrowOnMutationInDev'; -import invariant from 'fbjs/lib/invariant'; - -// Modules provided by RN: -import TextInputState from 'TextInputState'; -import FabricUIManager from 'FabricUIManager'; -import UIManager from 'UIManager'; - -// Counter for uniquely identifying views. -// % 10 === 1 means it is a rootTag. -// % 2 === 0 means it is a Fabric tag. -// This means that they never overlap. -let nextReactTag = 2; - -type HostContext = $ReadOnly<{| - isInAParentText: boolean, -|}>; - -/** - * This is used for refs on host components. - */ -class ReactFabricHostComponent { - _nativeTag: number; - viewConfig: ReactNativeBaseComponentViewConfig; - currentProps: Props; - - constructor( - tag: number, - viewConfig: ReactNativeBaseComponentViewConfig, - props: Props, - ) { - this._nativeTag = tag; - this.viewConfig = viewConfig; - this.currentProps = props; - } - - blur() { - TextInputState.blurTextInput(this._nativeTag); - } - - focus() { - TextInputState.focusTextInput(this._nativeTag); - } - - measure(callback: MeasureOnSuccessCallback) { - UIManager.measure(this._nativeTag, mountSafeCallback(this, callback)); - } - - measureInWindow(callback: MeasureInWindowOnSuccessCallback) { - UIManager.measureInWindow( - this._nativeTag, - mountSafeCallback(this, callback), - ); - } - - measureLayout( - relativeToNativeNode: number, - onSuccess: MeasureLayoutOnSuccessCallback, - onFail: () => void /* currently unused */, - ) { - UIManager.measureLayout( - this._nativeTag, - relativeToNativeNode, - mountSafeCallback(this, onFail), - mountSafeCallback(this, onSuccess), - ); - } - - setNativeProps(nativeProps: Object) { - if (__DEV__) { - warnForStyleProps(nativeProps, this.viewConfig.validAttributes); - } - - const updatePayload = ReactNativeAttributePayload.create( - nativeProps, - this.viewConfig.validAttributes, - ); - - // Avoid the overhead of bridge calls if there's no update. - // This is an expensive no-op for Android, and causes an unnecessary - // view invalidation for certain components (eg RCTTextInput) on iOS. - if (updatePayload != null) { - UIManager.updateView( - this._nativeTag, - this.viewConfig.uiViewClassName, - updatePayload, - ); - } - } -} - -// eslint-disable-next-line no-unused-expressions -(ReactFabricHostComponent.prototype: NativeMethodsMixinType); - -type Node = Object; -type ChildSet = Object; -type Container = number; -type Instance = { - node: Node, - canonical: ReactFabricHostComponent, -}; -type Props = Object; -type TextInstance = { - node: Node, -}; - -const ReactFabricRenderer = ReactFiberReconciler({ - appendInitialChild( - parentInstance: Instance, - child: Instance | TextInstance, - ): void { - FabricUIManager.appendChild(parentInstance.node, child.node); - }, - - createInstance( - type: string, - props: Props, - rootContainerInstance: Container, - hostContext: HostContext, - internalInstanceHandle: Object, - ): Instance { - const tag = nextReactTag; - nextReactTag += 2; - - const viewConfig = ReactNativeViewConfigRegistry.get(type); - - if (__DEV__) { - for (const key in viewConfig.validAttributes) { - if (props.hasOwnProperty(key)) { - deepFreezeAndThrowOnMutationInDev(props[key]); - } - } - } - - invariant( - type !== 'RCTView' || !hostContext.isInAParentText, - 'Nesting of within is not currently supported.', - ); - - const updatePayload = ReactNativeAttributePayload.create( - props, - viewConfig.validAttributes, - ); - - const node = FabricUIManager.createNode( - tag, // reactTag - viewConfig.uiViewClassName, // viewName - rootContainerInstance, // rootTag - updatePayload, // props - internalInstanceHandle, // internalInstanceHandle - ); - - const component = new ReactFabricHostComponent(tag, viewConfig, props); - - return { - node: node, - canonical: component, - }; - }, - - createTextInstance( - text: string, - rootContainerInstance: Container, - hostContext: HostContext, - internalInstanceHandle: Object, - ): TextInstance { - invariant( - hostContext.isInAParentText, - 'Text strings must be rendered within a component.', - ); - - const tag = nextReactTag; - nextReactTag += 2; - - const node = FabricUIManager.createNode( - tag, // reactTag - 'RCTRawText', // viewName - rootContainerInstance, // rootTag - {text: text}, // props - internalInstanceHandle, // instance handle - ); - - return { - node: node, - }; - }, - - finalizeInitialChildren( - parentInstance: Instance, - type: string, - props: Props, - rootContainerInstance: Container, - ): boolean { - return false; - }, - - getRootHostContext(rootContainerInstance: Container): HostContext { - return {isInAParentText: false}; - }, - - getChildHostContext( - parentHostContext: HostContext, - type: string, - ): HostContext { - const prevIsInAParentText = parentHostContext.isInAParentText; - const isInAParentText = - type === 'AndroidTextInput' || // Android - type === 'RCTMultilineTextInputView' || // iOS - type === 'RCTSinglelineTextInputView' || // iOS - type === 'RCTText' || - type === 'RCTVirtualText'; - - if (prevIsInAParentText !== isInAParentText) { - return {isInAParentText}; - } else { - return parentHostContext; - } - }, - - getPublicInstance(instance) { - return instance.canonical; - }, - - now: ReactNativeFrameScheduling.now, - - // The Fabric renderer is secondary to the existing React Native renderer. - isPrimaryRenderer: false, - - prepareForCommit(): void { - // Noop - }, - - prepareUpdate( - instance: Instance, - type: string, - oldProps: Props, - newProps: Props, - rootContainerInstance: Container, - hostContext: HostContext, - ): null | Object { - const viewConfig = instance.canonical.viewConfig; - const updatePayload = ReactNativeAttributePayload.diff( - oldProps, - newProps, - viewConfig.validAttributes, - ); - // TODO: If the event handlers have changed, we need to update the current props - // in the commit phase but there is no host config hook to do it yet. - return updatePayload; - }, - - resetAfterCommit(): void { - // Noop - }, - - scheduleDeferredCallback: ReactNativeFrameScheduling.scheduleDeferredCallback, - cancelDeferredCallback: ReactNativeFrameScheduling.cancelDeferredCallback, - - shouldDeprioritizeSubtree(type: string, props: Props): boolean { - return false; - }, - - shouldSetTextContent(type: string, props: Props): boolean { - // TODO (bvaughn) Revisit this decision. - // Always returning false simplifies the createInstance() implementation, - // But creates an additional child Fiber for raw text children. - // No additional native views are created though. - // It's not clear to me which is better so I'm deferring for now. - // More context @ github.com/facebook/react/pull/8560#discussion_r92111303 - return false; - }, - - persistence: { - cloneInstance( - instance: Instance, - updatePayload: null | Object, - type: string, - oldProps: Props, - newProps: Props, - internalInstanceHandle: Object, - keepChildren: boolean, - recyclableInstance: null | Instance, - ): Instance { - const node = instance.node; - let clone; - if (keepChildren) { - if (updatePayload !== null) { - clone = FabricUIManager.cloneNodeWithNewProps(node, updatePayload); - } else { - clone = FabricUIManager.cloneNode(node); - } - } else { - if (updatePayload !== null) { - clone = FabricUIManager.cloneNodeWithNewChildrenAndProps( - node, - updatePayload, - ); - } else { - clone = FabricUIManager.cloneNodeWithNewChildren(node); - } - } - return { - node: clone, - canonical: instance.canonical, - }; - }, - - createContainerChildSet(container: Container): ChildSet { - return FabricUIManager.createChildSet(container); - }, - - appendChildToContainerChildSet( - childSet: ChildSet, - child: Instance | TextInstance, - ): void { - FabricUIManager.appendChildToSet(childSet, child.node); - }, - - finalizeContainerChildren( - container: Container, - newChildren: ChildSet, - ): void { - FabricUIManager.completeRoot(container, newChildren); - }, - - replaceContainerChildren( - container: Container, - newChildren: ChildSet, - ): void {}, - }, -}); +const ReactFabricRenderer = ReactFiberReconciler(ReactFabricHostConfig); export default ReactFabricRenderer; From 53b03e4ab8df24364d68e9ea0c5a5c437c1b052a Mon Sep 17 00:00:00 2001 From: Dan Date: Sat, 12 May 2018 22:42:57 +0100 Subject: [PATCH 5/5] Extract RN host config --- .../react-native-renderer/src/ReactFabricHostConfig.js | 2 +- .../src/ReactNativeFiberHostComponent.js | 2 +- ...ctNativeFiberRenderer.js => ReactNativeHostConfig.js} | 9 ++++----- .../react-native-renderer/src/ReactNativeRenderer.js | 5 ++++- 4 files changed, 10 insertions(+), 8 deletions(-) rename packages/react-native-renderer/src/{ReactNativeFiberRenderer.js => ReactNativeHostConfig.js} (98%) diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index 903a6f266516d..921067e5164f6 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -239,7 +239,7 @@ const ReacFabricHostConfig = { } }, - getPublicInstance(instance) { + getPublicInstance(instance: Instance): * { return instance.canonical; }, diff --git a/packages/react-native-renderer/src/ReactNativeFiberHostComponent.js b/packages/react-native-renderer/src/ReactNativeFiberHostComponent.js index 86c0d3ad707d3..3d5bf5bbc993c 100644 --- a/packages/react-native-renderer/src/ReactNativeFiberHostComponent.js +++ b/packages/react-native-renderer/src/ReactNativeFiberHostComponent.js @@ -14,7 +14,7 @@ import type { NativeMethodsMixinType, ReactNativeBaseComponentViewConfig, } from './ReactNativeTypes'; -import type {Instance} from './ReactNativeFiberRenderer'; +import type {Instance} from './ReactNativeHostConfig'; // Modules provided by RN: import TextInputState from 'TextInputState'; diff --git a/packages/react-native-renderer/src/ReactNativeFiberRenderer.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js similarity index 98% rename from packages/react-native-renderer/src/ReactNativeFiberRenderer.js rename to packages/react-native-renderer/src/ReactNativeHostConfig.js index 01fe4816b4bab..b766d1e6554f7 100644 --- a/packages/react-native-renderer/src/ReactNativeFiberRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js @@ -9,7 +9,6 @@ import type {ReactNativeBaseComponentViewConfig} from './ReactNativeTypes'; -import ReactFiberReconciler from 'react-reconciler'; import emptyObject from 'fbjs/lib/emptyObject'; import invariant from 'fbjs/lib/invariant'; @@ -64,7 +63,7 @@ function recursivelyUncacheFiberNode(node: Instance | TextInstance) { } } -const NativeRenderer = ReactFiberReconciler({ +const ReactNativeHostConfig = { appendInitialChild( parentInstance: Instance, child: Instance | TextInstance, @@ -193,7 +192,7 @@ const NativeRenderer = ReactFiberReconciler({ } }, - getPublicInstance(instance) { + getPublicInstance(instance: Instance): * { return instance; }, @@ -427,6 +426,6 @@ const NativeRenderer = ReactFiberReconciler({ // Noop }, }, -}); +}; -export default NativeRenderer; +export default ReactNativeHostConfig; diff --git a/packages/react-native-renderer/src/ReactNativeRenderer.js b/packages/react-native-renderer/src/ReactNativeRenderer.js index f8e984d0e63fd..ce55def9e679a 100644 --- a/packages/react-native-renderer/src/ReactNativeRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeRenderer.js @@ -12,6 +12,7 @@ import type {ReactNodeList} from 'shared/ReactTypes'; import './ReactNativeInjection'; +import ReactFiberReconciler from 'react-reconciler'; import * as ReactPortal from 'shared/ReactPortal'; import * as ReactGenericBatching from 'events/ReactGenericBatching'; import ReactVersion from 'shared/ReactVersion'; @@ -20,16 +21,18 @@ import UIManager from 'UIManager'; import {getStackAddendumByWorkInProgressFiber} from 'shared/ReactFiberComponentTreeHook'; +import ReactNativeHostConfig from './ReactNativeHostConfig'; import NativeMethodsMixin from './NativeMethodsMixin'; import ReactNativeComponent from './ReactNativeComponent'; import * as ReactNativeComponentTree from './ReactNativeComponentTree'; -import ReactNativeFiberRenderer from './ReactNativeFiberRenderer'; import {getInspectorDataForViewTag} from './ReactNativeFiberInspector'; import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState'; import getComponentName from 'shared/getComponentName'; import warning from 'fbjs/lib/warning'; +const ReactNativeFiberRenderer = ReactFiberReconciler(ReactNativeHostConfig); + const findHostInstance = ReactNativeFiberRenderer.findHostInstance; function findNodeHandle(componentOrHandle: any): ?number {