diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js index f1b6859ab7ea2..cbb85abe7c2dd 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js @@ -12,7 +12,7 @@ import type { TouchedViewDataAtPoint, ViewConfig, } from './ReactNativeTypes'; -import {create, diff} from './ReactNativeAttributePayload'; +import {create, diff} from './ReactNativeAttributePayloadFabric'; import {dispatchEvent} from './ReactFabricEventEmitter'; import { NoEventPriority, diff --git a/packages/react-native-renderer/src/ReactNativeAttributePayloadFabric.js b/packages/react-native-renderer/src/ReactNativeAttributePayloadFabric.js new file mode 100644 index 0000000000000..0b316ff1ab875 --- /dev/null +++ b/packages/react-native-renderer/src/ReactNativeAttributePayloadFabric.js @@ -0,0 +1,499 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +// Modules provided by RN: +import { + deepDiffer, + flattenStyle, +} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; +import isArray from 'shared/isArray'; + +import {enableEarlyReturnForPropDiffing} from 'shared/ReactFeatureFlags'; + +import type {AttributeConfiguration} from './ReactNativeTypes'; + +const emptyObject = {}; + +/** + * Create a payload that contains all the updates between two sets of props. + * + * These helpers are all encapsulated into a single module, because they use + * mutation as a performance optimization which leads to subtle shared + * dependencies between the code paths. To avoid this mutable state leaking + * across modules, I've kept them isolated to this module. + */ + +type NestedNode = Array | Object; + +// Tracks removed keys +let removedKeys: {[string]: boolean} | null = null; +let removedKeyCount = 0; + +const deepDifferOptions = { + unsafelyIgnoreFunctions: true, +}; + +function defaultDiffer(prevProp: mixed, nextProp: mixed): boolean { + if (typeof nextProp !== 'object' || nextProp === null) { + // Scalars have already been checked for equality + return true; + } else { + // For objects and arrays, the default diffing algorithm is a deep compare + return deepDiffer(prevProp, nextProp, deepDifferOptions); + } +} + +function restoreDeletedValuesInNestedArray( + updatePayload: Object, + node: NestedNode, + validAttributes: AttributeConfiguration, +) { + if (isArray(node)) { + let i = node.length; + while (i-- && removedKeyCount > 0) { + restoreDeletedValuesInNestedArray( + updatePayload, + node[i], + validAttributes, + ); + } + } else if (node && removedKeyCount > 0) { + const obj = node; + for (const propKey in removedKeys) { + // $FlowFixMe[incompatible-use] found when upgrading Flow + if (!removedKeys[propKey]) { + continue; + } + let nextProp = obj[propKey]; + if (nextProp === undefined) { + continue; + } + + const attributeConfig = validAttributes[propKey]; + if (!attributeConfig) { + continue; // not a valid native prop + } + + if (typeof nextProp === 'function') { + // $FlowFixMe[incompatible-type] found when upgrading Flow + nextProp = true; + } + if (typeof nextProp === 'undefined') { + // $FlowFixMe[incompatible-type] found when upgrading Flow + nextProp = null; + } + + if (typeof attributeConfig !== 'object') { + // case: !Object is the default case + updatePayload[propKey] = nextProp; + } else if ( + typeof attributeConfig.diff === 'function' || + typeof attributeConfig.process === 'function' + ) { + // case: CustomAttributeConfiguration + const nextValue = + typeof attributeConfig.process === 'function' + ? attributeConfig.process(nextProp) + : nextProp; + updatePayload[propKey] = nextValue; + } + // $FlowFixMe[incompatible-use] found when upgrading Flow + removedKeys[propKey] = false; + removedKeyCount--; + } + } +} + +function diffNestedArrayProperty( + updatePayload: null | Object, + prevArray: Array, + nextArray: Array, + validAttributes: AttributeConfiguration, +): null | Object { + const minLength = + prevArray.length < nextArray.length ? prevArray.length : nextArray.length; + let i; + for (i = 0; i < minLength; i++) { + // Diff any items in the array in the forward direction. Repeated keys + // will be overwritten by later values. + updatePayload = diffNestedProperty( + updatePayload, + prevArray[i], + nextArray[i], + validAttributes, + ); + } + for (; i < prevArray.length; i++) { + // Clear out all remaining properties. + updatePayload = clearNestedProperty( + updatePayload, + prevArray[i], + validAttributes, + ); + } + for (; i < nextArray.length; i++) { + // Add all remaining properties. + updatePayload = addNestedProperty( + updatePayload, + nextArray[i], + validAttributes, + ); + } + return updatePayload; +} + +function diffNestedProperty( + updatePayload: null | Object, + prevProp: NestedNode, + nextProp: NestedNode, + validAttributes: AttributeConfiguration, +): null | Object { + if (!updatePayload && prevProp === nextProp) { + // If no properties have been added, then we can bail out quickly on object + // equality. + return updatePayload; + } + + if (!prevProp || !nextProp) { + if (nextProp) { + return addNestedProperty(updatePayload, nextProp, validAttributes); + } + if (prevProp) { + return clearNestedProperty(updatePayload, prevProp, validAttributes); + } + return updatePayload; + } + + if (!isArray(prevProp) && !isArray(nextProp)) { + // Both are leaves, we can diff the leaves. + return diffProperties(updatePayload, prevProp, nextProp, validAttributes); + } + + if (isArray(prevProp) && isArray(nextProp)) { + // Both are arrays, we can diff the arrays. + return diffNestedArrayProperty( + updatePayload, + prevProp, + nextProp, + validAttributes, + ); + } + + if (isArray(prevProp)) { + return diffProperties( + updatePayload, + flattenStyle(prevProp), + nextProp, + validAttributes, + ); + } + + return diffProperties( + updatePayload, + prevProp, + flattenStyle(nextProp), + validAttributes, + ); +} + +/** + * addNestedProperty takes a single set of props and valid attribute + * attribute configurations. It processes each prop and adds it to the + * updatePayload. + */ +function addNestedProperty( + updatePayload: null | Object, + nextProp: NestedNode, + validAttributes: AttributeConfiguration, +): $FlowFixMe { + if (!nextProp) { + return updatePayload; + } + + if (!isArray(nextProp)) { + // Add each property of the leaf. + return addProperties(updatePayload, nextProp, validAttributes); + } + + for (let i = 0; i < nextProp.length; i++) { + // Add all the properties of the array. + updatePayload = addNestedProperty( + updatePayload, + nextProp[i], + validAttributes, + ); + } + + return updatePayload; +} + +/** + * clearNestedProperty takes a single set of props and valid attributes. It + * adds a null sentinel to the updatePayload, for each prop key. + */ +function clearNestedProperty( + updatePayload: null | Object, + prevProp: NestedNode, + validAttributes: AttributeConfiguration, +): null | Object { + if (!prevProp) { + return updatePayload; + } + + if (!isArray(prevProp)) { + // Add each property of the leaf. + return clearProperties(updatePayload, prevProp, validAttributes); + } + + for (let i = 0; i < prevProp.length; i++) { + // Add all the properties of the array. + updatePayload = clearNestedProperty( + updatePayload, + prevProp[i], + validAttributes, + ); + } + return updatePayload; +} + +/** + * diffProperties takes two sets of props and a set of valid attributes + * and write to updatePayload the values that changed or were deleted. + * If no updatePayload is provided, a new one is created and returned if + * anything changed. + */ +function diffProperties( + updatePayload: null | Object, + prevProps: Object, + nextProps: Object, + validAttributes: AttributeConfiguration, +): null | Object { + let attributeConfig; + let nextProp; + let prevProp; + + for (const propKey in nextProps) { + attributeConfig = validAttributes[propKey]; + if (!attributeConfig) { + continue; // not a valid native prop + } + + prevProp = prevProps[propKey]; + nextProp = nextProps[propKey]; + + // functions are converted to booleans as markers that the associated + // events should be sent from native. + if (typeof nextProp === 'function') { + nextProp = (true: any); + // If nextProp is not a function, then don't bother changing prevProp + // since nextProp will win and go into the updatePayload regardless. + if (typeof prevProp === 'function') { + prevProp = (true: any); + } + } + + // An explicit value of undefined is treated as a null because it overrides + // any other preceding value. + if (typeof nextProp === 'undefined') { + nextProp = (null: any); + if (typeof prevProp === 'undefined') { + prevProp = (null: any); + } + } + + if (removedKeys) { + removedKeys[propKey] = false; + } + + if (updatePayload && updatePayload[propKey] !== undefined) { + // Something else already triggered an update to this key because another + // value diffed. Since we're now later in the nested arrays our value is + // more important so we need to calculate it and override the existing + // value. It doesn't matter if nothing changed, we'll set it anyway. + + // Pattern match on: attributeConfig + if (typeof attributeConfig !== 'object') { + // case: !Object is the default case + updatePayload[propKey] = nextProp; + } else if ( + typeof attributeConfig.diff === 'function' || + typeof attributeConfig.process === 'function' + ) { + // case: CustomAttributeConfiguration + const nextValue = + typeof attributeConfig.process === 'function' + ? attributeConfig.process(nextProp) + : nextProp; + updatePayload[propKey] = nextValue; + } + continue; + } + + if (prevProp === nextProp) { + continue; // nothing changed + } + + // Pattern match on: attributeConfig + if (typeof attributeConfig !== 'object') { + // case: !Object is the default case + if (defaultDiffer(prevProp, nextProp)) { + // a normal leaf has changed + (updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[ + propKey + ] = nextProp; + } + } else if ( + typeof attributeConfig.diff === 'function' || + typeof attributeConfig.process === 'function' + ) { + // case: CustomAttributeConfiguration + const shouldUpdate = + prevProp === undefined || + (typeof attributeConfig.diff === 'function' + ? attributeConfig.diff(prevProp, nextProp) + : defaultDiffer(prevProp, nextProp)); + if (shouldUpdate) { + const nextValue = + typeof attributeConfig.process === 'function' + ? // $FlowFixMe[incompatible-use] found when upgrading Flow + attributeConfig.process(nextProp) + : nextProp; + (updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[ + propKey + ] = nextValue; + } + } else { + // default: fallthrough case when nested properties are defined + removedKeys = null; + removedKeyCount = 0; + // We think that attributeConfig is not CustomAttributeConfiguration at + // this point so we assume it must be AttributeConfiguration. + updatePayload = diffNestedProperty( + updatePayload, + prevProp, + nextProp, + ((attributeConfig: any): AttributeConfiguration), + ); + if (removedKeyCount > 0 && updatePayload) { + restoreDeletedValuesInNestedArray( + updatePayload, + nextProp, + ((attributeConfig: any): AttributeConfiguration), + ); + removedKeys = null; + } + } + } + + // Also iterate through all the previous props to catch any that have been + // removed and make sure native gets the signal so it can reset them to the + // default. + for (const propKey in prevProps) { + if (nextProps[propKey] !== undefined) { + continue; // we've already covered this key in the previous pass + } + attributeConfig = validAttributes[propKey]; + if (!attributeConfig) { + continue; // not a valid native prop + } + + if (updatePayload && updatePayload[propKey] !== undefined) { + // This was already updated to a diff result earlier. + continue; + } + + prevProp = prevProps[propKey]; + if (prevProp === undefined) { + continue; // was already empty anyway + } + // Pattern match on: attributeConfig + if ( + typeof attributeConfig !== 'object' || + typeof attributeConfig.diff === 'function' || + typeof attributeConfig.process === 'function' + ) { + // case: CustomAttributeConfiguration | !Object + // Flag the leaf property for removal by sending a sentinel. + (updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[ + propKey + ] = null; + if (!removedKeys) { + removedKeys = ({}: {[string]: boolean}); + } + if (!removedKeys[propKey]) { + removedKeys[propKey] = true; + removedKeyCount++; + } + } else { + // default: + // This is a nested attribute configuration where all the properties + // were removed so we need to go through and clear out all of them. + updatePayload = clearNestedProperty( + updatePayload, + prevProp, + ((attributeConfig: any): AttributeConfiguration), + ); + } + } + return updatePayload; +} + +/** + * addProperties adds all the valid props to the payload after being processed. + */ +function addProperties( + updatePayload: null | Object, + props: Object, + validAttributes: AttributeConfiguration, +): null | Object { + // TODO: Fast path + return diffProperties(updatePayload, emptyObject, props, validAttributes); +} + +/** + * clearProperties clears all the previous props by adding a null sentinel + * to the payload for each valid key. + */ +function clearProperties( + updatePayload: null | Object, + prevProps: Object, + validAttributes: AttributeConfiguration, +): null | Object { + // TODO: Fast path + return diffProperties(updatePayload, prevProps, emptyObject, validAttributes); +} + +export function create( + props: Object, + validAttributes: AttributeConfiguration, +): null | Object { + return addProperties( + null, // updatePayload + props, + validAttributes, + ); +} + +export function diff( + prevProps: Object, + nextProps: Object, + validAttributes: AttributeConfiguration, +): null | Object { + if (enableEarlyReturnForPropDiffing) { + if (prevProps === nextProps) { + return null; // no change + } + } + return diffProperties( + null, // updatePayload + prevProps, + nextProps, + validAttributes, + ); +}