[Fiber] Compute the Host Diff During Reconciliation (#8607)

* Allow renderers to return an update payload in prepareUpdate

This then gets stored on updateQueue so that the renderer doesn't need to
think about how to store this.

It then gets passed into commitUpdate during the commit phase.

This allows renderers to do the diffing during the time sliced path,
allocate a queue for changes and do only the absolute minimal work to
apply those changes in the commit phase.

If refs update we still schedule and update.

* Hack around the listener problem

* Diff ReactDOMFiber properties in prepareUpdate

We now take advantage of the new capability to diff properties early.
We do this by generating an update payload in the form of an array with
each property name and value that we're about to update.

* Add todo for handling wasCustomComponentTag

* Always force an update to wrapper components

Wrapper components have custom logic that gets applied at the commit phase
so we always need to ensure that we schedule an update for them.

* Remove rootContainerInstance from commitMount

No use case yet and I removed it from commitUpdate earlier.

* Use update signal object in test renderer

* Incorporate 8652 into new algorithm

* Fix comment

* Add failing test for flipping event handlers

This illustrates the problem that happens if we store a pointer to the
Fiber and then choose not to update that pointer when no properties change.
That causes an old Fiber to be retained on the DOM node. Then that Fiber
can be reused by the pooling mechanism which then will mutate that Fiber
with new event handlers, which makes them active before commit.

* Store current props in the RN instance cache and on the DOM node

This represents the current set of event listeners. By not relying on the
Fiber, it allows us to avoid doing any effects in the commit phase when
nothing changes.

This is a bit ugly. Not super happy how this all came together.
This commit is contained in:
Sebastian Markbåge
2017-01-19 21:56:15 -08:00
committed by GitHub
parent c6a7dc7f24
commit b354db22a9
20 changed files with 412 additions and 173 deletions
+3
View File
@@ -37,6 +37,9 @@ src/renderers/dom/shared/__tests__/ReactRenderDocument-test.js
* should throw on full document render w/ no markup * should throw on full document render w/ no markup
* supports findDOMNode on full-page components * supports findDOMNode on full-page components
src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js
* should control a value in reentrant events
src/renderers/shared/__tests__/ReactPerf-test.js src/renderers/shared/__tests__/ReactPerf-test.js
* should count no-op update as waste * should count no-op update as waste
* should count no-op update in child as waste * should count no-op update in child as waste
+1 -1
View File
@@ -546,6 +546,7 @@ src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
* findDOMNode should find dom element after expanding a fragment * findDOMNode should find dom element after expanding a fragment
* should bubble events from the portal to the parent * should bubble events from the portal to the parent
* should not onMouseLeave when staying in the portal * should not onMouseLeave when staying in the portal
* should not update event handlers until commit
* should not crash encountering low-priority tree * should not crash encountering low-priority tree
* throws if non-element passed to top-level render * throws if non-element passed to top-level render
* throws if something other than false, null, or an element is returned from render * throws if something other than false, null, or an element is returned from render
@@ -959,7 +960,6 @@ src/renderers/dom/shared/wrappers/__tests__/ReactDOMIframe-test.js
src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js
* should properly control a value even if no event listener exists * should properly control a value even if no event listener exists
* should control a value in reentrant events
* should control values in reentrant events with different targets * should control values in reentrant events with different targets
* should display `defaultValue` of number 0 * should display `defaultValue` of number 0
* only assigns defaultValue if it changes * only assigns defaultValue if it changes
+4 -2
View File
@@ -42,6 +42,8 @@ const TYPES = {
TEXT: 'Text', TEXT: 'Text',
}; };
const UPDATE_SIGNAL = {};
/** Helper Methods */ /** Helper Methods */
function addEventListeners(instance, type, listener) { function addEventListeners(instance, type, listener) {
@@ -418,7 +420,7 @@ const ARTRenderer = ReactFiberReconciler({
// Noop // Noop
}, },
commitUpdate(instance, type, oldProps, newProps) { commitUpdate(instance, updatePayload, type, oldProps, newProps) {
instance._applyProps(instance, newProps, oldProps); instance._applyProps(instance, newProps, oldProps);
}, },
@@ -482,7 +484,7 @@ const ARTRenderer = ReactFiberReconciler({
}, },
prepareUpdate(domElement, type, oldProps, newProps) { prepareUpdate(domElement, type, oldProps, newProps) {
return true; return UPDATE_SIGNAL;
}, },
removeChild(parentInstance, child) { removeChild(parentInstance, child) {
+15 -9
View File
@@ -35,9 +35,13 @@ var {
createElement, createElement,
getChildNamespace, getChildNamespace,
setInitialProperties, setInitialProperties,
diffProperties,
updateProperties, updateProperties,
} = ReactDOMFiberComponent; } = ReactDOMFiberComponent;
var { precacheFiberNode } = ReactDOMComponentTree; var {
precacheFiberNode,
updateFiberEventHandlers,
} = ReactDOMComponentTree;
if (__DEV__) { if (__DEV__) {
var validateDOMNesting = require('validateDOMNesting'); var validateDOMNesting = require('validateDOMNesting');
@@ -184,6 +188,7 @@ var DOMRenderer = ReactFiberReconciler({
} }
const domElement : Instance = createElement(type, props, rootContainerInstance, parentNamespace); const domElement : Instance = createElement(type, props, rootContainerInstance, parentNamespace);
precacheFiberNode(internalInstanceHandle, domElement); precacheFiberNode(internalInstanceHandle, domElement);
updateFiberEventHandlers(domElement, props);
return domElement; return domElement;
}, },
@@ -206,8 +211,9 @@ var DOMRenderer = ReactFiberReconciler({
type : string, type : string,
oldProps : Props, oldProps : Props,
newProps : Props, newProps : Props,
rootContainerInstance : Container,
hostContext : HostContext, hostContext : HostContext,
) : boolean { ) : null | Array<mixed> {
if (__DEV__) { if (__DEV__) {
const hostContextDev = ((hostContext : any) : HostContextDev); const hostContextDev = ((hostContext : any) : HostContextDev);
if (typeof newProps.children !== typeof oldProps.children && ( if (typeof newProps.children !== typeof oldProps.children && (
@@ -218,14 +224,13 @@ var DOMRenderer = ReactFiberReconciler({
validateDOMNesting(null, String(newProps.children), null, ownAncestorInfo); validateDOMNesting(null, String(newProps.children), null, ownAncestorInfo);
} }
} }
return true; return diffProperties(domElement, type, oldProps, newProps, rootContainerInstance);
}, },
commitMount( commitMount(
domElement : Instance, domElement : Instance,
type : string, type : string,
newProps : Props, newProps : Props,
rootContainerInstance : Container,
internalInstanceHandle : Object, internalInstanceHandle : Object,
) : void { ) : void {
((domElement : any) : HTMLButtonElement | HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement).focus(); ((domElement : any) : HTMLButtonElement | HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement).focus();
@@ -233,16 +238,17 @@ var DOMRenderer = ReactFiberReconciler({
commitUpdate( commitUpdate(
domElement : Instance, domElement : Instance,
updatePayload : Array<mixed>,
type : string, type : string,
oldProps : Props, oldProps : Props,
newProps : Props, newProps : Props,
rootContainerInstance : Container,
internalInstanceHandle : Object, internalInstanceHandle : Object,
) : void { ) : void {
// Update the internal instance handle so that we know which props are // Update the props handle so that we know which props are the ones with
// the current ones. // with current event handlers.
precacheFiberNode(internalInstanceHandle, domElement); updateFiberEventHandlers(domElement, newProps);
updateProperties(domElement, type, oldProps, newProps, rootContainerInstance); // Apply the diff to the DOM node.
updateProperties(domElement, updatePayload, type, oldProps, newProps);
}, },
shouldSetTextContent(props : Props) : boolean { shouldSetTextContent(props : Props) : boolean {
+208 -103
View File
@@ -301,69 +301,15 @@ function isCustomComponent(tagName, props) {
return tagName.indexOf('-') >= 0 || props.is != null; return tagName.indexOf('-') >= 0 || props.is != null;
} }
/** function setInitialDOMProperties(
* Reconciles the properties by detecting differences in property values and
* updating the DOM as necessary. This function is probably the single most
* critical path for performance optimization.
*
* TODO: Benchmark whether checking for changed values in memory actually
* improves performance (especially statically positioned elements).
* TODO: Benchmark the effects of putting this at the top since 99% of props
* do not change for a given reconciliation.
* TODO: Benchmark areas that can be improved with caching.
*/
function updateDOMProperties(
domElement : Element, domElement : Element,
rootContainerElement : Element, rootContainerElement : Element,
lastProps : null | Object,
nextProps : Object, nextProps : Object,
wasCustomComponentTag : boolean,
isCustomComponentTag : boolean, isCustomComponentTag : boolean,
) : void { ) : void {
var propKey; for (var propKey in nextProps) {
var styleName;
var styleUpdates;
for (propKey in lastProps) {
if (nextProps.hasOwnProperty(propKey) ||
!lastProps.hasOwnProperty(propKey) ||
lastProps[propKey] == null) {
continue;
}
if (propKey === STYLE) {
var lastStyle = lastProps[propKey];
for (styleName in lastStyle) {
if (lastStyle.hasOwnProperty(styleName)) {
styleUpdates = styleUpdates || {};
styleUpdates[styleName] = '';
}
}
} else if (propKey === DANGEROUSLY_SET_INNER_HTML ||
propKey === CHILDREN) {
// TODO: Clear innerHTML. This is currently broken in Fiber because we are
// too late to clear everything at this point because new children have
// already been inserted.
} else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING) {
// Noop
} else if (registrationNameModules.hasOwnProperty(propKey)) {
// Do nothing for deleted listeners.
} else if (wasCustomComponentTag) {
DOMPropertyOperations.deleteValueForAttribute(
domElement,
propKey
);
} else if (
DOMProperty.properties[propKey] ||
DOMProperty.isCustomAttribute(propKey)) {
DOMPropertyOperations.deleteValueForProperty(domElement, propKey);
}
}
for (propKey in nextProps) {
var nextProp = nextProps[propKey]; var nextProp = nextProps[propKey];
var lastProp = if (!nextProps.hasOwnProperty(propKey)) {
lastProps != null ? lastProps[propKey] : undefined;
if (!nextProps.hasOwnProperty(propKey) ||
nextProp === lastProp ||
nextProp == null && lastProp == null) {
continue; continue;
} }
if (propKey === STYLE) { if (propKey === STYLE) {
@@ -374,38 +320,16 @@ function updateDOMProperties(
Object.freeze(nextProp); Object.freeze(nextProp);
} }
} }
if (lastProp) { // Relies on `updateStylesByID` not mutating `styleUpdates`.
// Unset styles on `lastProp` but not on `nextProp`. // TODO: call ReactInstrumentation.debugTool.onHostOperation in DEV.
for (styleName in lastProp) { CSSPropertyOperations.setValueForStyles(
if (lastProp.hasOwnProperty(styleName) && domElement,
(!nextProp || !nextProp.hasOwnProperty(styleName))) { nextProp,
styleUpdates = styleUpdates || {}; );
styleUpdates[styleName] = '';
}
}
// Update styles that changed since `lastProp`.
for (styleName in nextProp) {
if (nextProp.hasOwnProperty(styleName) &&
lastProp[styleName] !== nextProp[styleName]) {
styleUpdates = styleUpdates || {};
styleUpdates[styleName] = nextProp[styleName];
}
}
} else {
// Relies on `updateStylesByID` not mutating `styleUpdates`.
styleUpdates = nextProp;
}
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) { } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
var nextHtml = nextProp ? nextProp[HTML] : undefined; var nextHtml = nextProp ? nextProp[HTML] : undefined;
var lastHtml = lastProp ? lastProp[HTML] : undefined;
// Intentional use of != to avoid catching zero/false.
if (nextHtml != null) { if (nextHtml != null) {
if (lastHtml !== nextHtml) { setInnerHTML(domElement, nextHtml);
setInnerHTML(domElement, '' + nextHtml);
}
} else {
// TODO: It might be too late to clear this if we have children
// inserted already.
} }
} else if (propKey === CHILDREN) { } else if (propKey === CHILDREN) {
if (typeof nextProp === 'string') { if (typeof nextProp === 'string') {
@@ -433,18 +357,57 @@ function updateDOMProperties(
// brings us in line with the same behavior we have on initial render. // brings us in line with the same behavior we have on initial render.
if (nextProp != null) { if (nextProp != null) {
DOMPropertyOperations.setValueForProperty(domElement, propKey, nextProp); DOMPropertyOperations.setValueForProperty(domElement, propKey, nextProp);
}
}
}
}
function updateDOMProperties(
domElement : Element,
updatePayload : Array<any>,
wasCustomComponentTag : boolean,
isCustomComponentTag : boolean,
) : void {
// TODO: Handle wasCustomComponentTag
for (var i = 0; i < updatePayload.length; i+=2) {
var propKey = updatePayload[i];
var propValue = updatePayload[i + 1];
if (propKey === STYLE) {
// TODO: call ReactInstrumentation.debugTool.onHostOperation in DEV.
CSSPropertyOperations.setValueForStyles(
domElement,
propValue,
);
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
setInnerHTML(domElement, propValue);
} else if (propKey === CHILDREN) {
setTextContent(domElement, propValue);
} else if (isCustomComponentTag) {
if (propValue != null) {
DOMPropertyOperations.setValueForAttribute(
domElement,
propKey,
propValue
);
} else {
DOMPropertyOperations.deleteValueForAttribute(
domElement,
propKey
);
}
} else if (
DOMProperty.properties[propKey] ||
DOMProperty.isCustomAttribute(propKey)) {
// If we're updating to null or undefined, we should remove the property
// from the DOM node instead of inadvertently setting to a string. This
// brings us in line with the same behavior we have on initial render.
if (propValue != null) {
DOMPropertyOperations.setValueForProperty(domElement, propKey, propValue);
} else { } else {
DOMPropertyOperations.deleteValueForProperty(domElement, propKey); DOMPropertyOperations.deleteValueForProperty(domElement, propKey);
} }
} }
} }
if (styleUpdates) {
// TODO: call ReactInstrumentation.debugTool.onHostOperation in DEV.
CSSPropertyOperations.setValueForStyles(
domElement,
styleUpdates,
);
}
} }
// Assumes there is no parent namespace. // Assumes there is no parent namespace.
@@ -592,12 +555,10 @@ var ReactDOMFiberComponent = {
assertValidProps(tag, props); assertValidProps(tag, props);
updateDOMProperties( setInitialDOMProperties(
domElement, domElement,
rootContainerElement, rootContainerElement,
null,
props, props,
false,
isCustomComponentTag isCustomComponentTag
); );
@@ -626,35 +587,42 @@ var ReactDOMFiberComponent = {
} }
}, },
updateProperties( // Calculate the diff between the two objects.
diffProperties(
domElement : Element, domElement : Element,
tag : string, tag : string,
lastRawProps : Object, lastRawProps : Object,
nextRawProps : Object, nextRawProps : Object,
rootContainerElement : Element, rootContainerElement : Element,
) : void { ) : null | Array<mixed> {
if (__DEV__) { if (__DEV__) {
validatePropertiesInDevelopment(tag, nextRawProps); validatePropertiesInDevelopment(tag, nextRawProps);
} }
var updatePayload : null | Array<any> = null;
var lastProps : Object; var lastProps : Object;
var nextProps : Object; var nextProps : Object;
switch (tag) { switch (tag) {
case 'input': case 'input':
lastProps = ReactDOMFiberInput.getHostProps(domElement, lastRawProps); lastProps = ReactDOMFiberInput.getHostProps(domElement, lastRawProps);
nextProps = ReactDOMFiberInput.getHostProps(domElement, nextRawProps); nextProps = ReactDOMFiberInput.getHostProps(domElement, nextRawProps);
updatePayload = [];
break; break;
case 'option': case 'option':
lastProps = ReactDOMFiberOption.getHostProps(domElement, lastRawProps); lastProps = ReactDOMFiberOption.getHostProps(domElement, lastRawProps);
nextProps = ReactDOMFiberOption.getHostProps(domElement, nextRawProps); nextProps = ReactDOMFiberOption.getHostProps(domElement, nextRawProps);
updatePayload = [];
break; break;
case 'select': case 'select':
lastProps = ReactDOMFiberSelect.getHostProps(domElement, lastRawProps); lastProps = ReactDOMFiberSelect.getHostProps(domElement, lastRawProps);
nextProps = ReactDOMFiberSelect.getHostProps(domElement, nextRawProps); nextProps = ReactDOMFiberSelect.getHostProps(domElement, nextRawProps);
updatePayload = [];
break; break;
case 'textarea': case 'textarea':
lastProps = ReactDOMFiberTextarea.getHostProps(domElement, lastRawProps); lastProps = ReactDOMFiberTextarea.getHostProps(domElement, lastRawProps);
nextProps = ReactDOMFiberTextarea.getHostProps(domElement, nextRawProps); nextProps = ReactDOMFiberTextarea.getHostProps(domElement, nextRawProps);
updatePayload = [];
break; break;
default: default:
lastProps = lastRawProps; lastProps = lastRawProps;
@@ -668,17 +636,154 @@ var ReactDOMFiberComponent = {
} }
assertValidProps(tag, nextProps); assertValidProps(tag, nextProps);
var wasCustomComponentTag = isCustomComponent(tag, lastProps);
var isCustomComponentTag = isCustomComponent(tag, nextProps); var propKey;
var styleName;
var styleUpdates = null;
for (propKey in lastProps) {
if (nextProps.hasOwnProperty(propKey) ||
!lastProps.hasOwnProperty(propKey) ||
lastProps[propKey] == null) {
continue;
}
if (propKey === STYLE) {
var lastStyle = lastProps[propKey];
for (styleName in lastStyle) {
if (lastStyle.hasOwnProperty(styleName)) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = '';
}
}
} else if (propKey === DANGEROUSLY_SET_INNER_HTML ||
propKey === CHILDREN) {
// Noop. This is handled by the clear text mechanism.
} else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING) {
// Noop
} else if (registrationNameModules.hasOwnProperty(propKey)) {
// This is a special case. If any listener updates we need to ensure
// that the "current" fiber pointer gets updated so we need a commit
// to update this element.
if (!updatePayload) {
updatePayload = [];
}
} else {
// For all other deleted properties we add it to the queue. We use
// the whitelist in the commit phase instead.
(updatePayload = updatePayload || []).push(propKey, null);
}
}
for (propKey in nextProps) {
var nextProp = nextProps[propKey];
var lastProp =
lastProps != null ? lastProps[propKey] : undefined;
if (!nextProps.hasOwnProperty(propKey) ||
nextProp === lastProp ||
nextProp == null && lastProp == null) {
continue;
}
if (propKey === STYLE) {
if (__DEV__) {
if (nextProp) {
// Freeze the next style object so that we can assume it won't be
// mutated. We have already warned for this in the past.
Object.freeze(nextProp);
}
}
if (lastProp) {
// Unset styles on `lastProp` but not on `nextProp`.
for (styleName in lastProp) {
if (lastProp.hasOwnProperty(styleName) &&
(!nextProp || !nextProp.hasOwnProperty(styleName))) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = '';
}
}
// Update styles that changed since `lastProp`.
for (styleName in nextProp) {
if (nextProp.hasOwnProperty(styleName) &&
lastProp[styleName] !== nextProp[styleName]) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = nextProp[styleName];
}
}
} else {
// Relies on `updateStylesByID` not mutating `styleUpdates`.
if (!styleUpdates) {
if (!updatePayload) {
updatePayload = [];
}
updatePayload.push(propKey, styleUpdates);
}
styleUpdates = nextProp;
}
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
var nextHtml = nextProp ? nextProp[HTML] : undefined;
var lastHtml = lastProp ? lastProp[HTML] : undefined;
if (nextHtml != null) {
if (lastHtml !== nextHtml) {
(updatePayload = updatePayload || []).push(propKey, '' + nextHtml);
}
} else {
// TODO: It might be too late to clear this if we have children
// inserted already.
}
} else if (propKey === CHILDREN) {
if (lastProp !== nextProp && (
typeof nextProp === 'string' || typeof nextProp === 'number'
)) {
(updatePayload = updatePayload || []).push(propKey, '' + nextProp);
}
} else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING) {
// Noop
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp) {
// We eagerly listen to this even though we haven't committed yet.
ensureListeningTo(rootContainerElement, propKey);
}
if (!updatePayload && lastProp !== nextProp) {
// This is a special case. If any listener updates we need to ensure
// that the "current" props pointer gets updated so we need a commit
// to update this element.
updatePayload = [];
}
} else {
// For any other property we always add it to the queue and then we
// filter it out using the whitelist during the commit.
(updatePayload = updatePayload || []).push(propKey, nextProp);
}
}
if (styleUpdates) {
(updatePayload = updatePayload || []).push(STYLE, styleUpdates);
}
return updatePayload;
},
// Apply the diff.
updateProperties(
domElement : Element,
updatePayload : Array<any>,
tag : string,
lastRawProps : Object,
nextRawProps : Object
) : void {
var wasCustomComponentTag = isCustomComponent(tag, lastRawProps);
var isCustomComponentTag = isCustomComponent(tag, nextRawProps);
// Apply the diff.
updateDOMProperties( updateDOMProperties(
domElement, domElement,
rootContainerElement, updatePayload,
lastProps,
nextProps,
wasCustomComponentTag, wasCustomComponentTag,
isCustomComponentTag isCustomComponentTag
); );
// TODO: Ensure that an update gets scheduled if any of the special props
// changed.
switch (tag) { switch (tag) {
case 'input': case 'input':
// Update the wrapper around inputs *after* updating props. This has to // Update the wrapper around inputs *after* updating props. This has to
@@ -994,6 +994,89 @@ describe('ReactDOMFiber', () => {
]); ]);
}); });
it('should not update event handlers until commit', () => {
let ops = [];
const handlerA = () => ops.push('A');
const handlerB = () => ops.push('B');
class Example extends React.Component {
state = { flip: false, count: 0 };
flip() {
this.setState({ flip: true, count: this.state.count + 1 });
}
tick() {
this.setState({ count: this.state.count + 1 });
}
render() {
const useB = !this.props.forceA && this.state.flip;
return (
<div onClick={useB ? handlerB : handlerA} />
);
}
}
class Click extends React.Component {
constructor() {
super();
click(node);
}
render() {
return null;
}
}
let inst;
ReactDOM.render([<Example ref={n => inst = n} />], container);
const node = container.firstChild;
expect(node.tagName).toEqual('DIV');
function click(target) {
var fakeNativeEvent = {};
ReactTestUtils.simulateNativeEventOnNode(
'topClick',
target,
fakeNativeEvent
);
}
click(node);
expect(ops).toEqual(['A']);
ops = [];
// Render with the other event handler.
inst.flip();
click(node);
expect(ops).toEqual(['B']);
ops = [];
// Rerender without changing any props.
inst.tick();
click(node);
expect(ops).toEqual(['B']);
ops = [];
// Render a flip back to the A handler. The second component invokes the
// click handler during render to simulate a click during an aborted
// render. I use this hack because at current time we don't have a way to
// test aborted ReactDOM renders.
ReactDOM.render([<Example forceA={true} />, <Click />], container);
// Because the new click handler has not yet committed, we should still
// invoke B.
expect(ops).toEqual(['B']);
ops = [];
// Any click that happens after commit, should invoke A.
click(node);
expect(ops).toEqual(['A']);
});
it('should not crash encountering low-priority tree', () => { it('should not crash encountering low-priority tree', () => {
ReactDOM.render( ReactDOM.render(
<div hidden={true}> <div hidden={true}>
@@ -20,8 +20,11 @@ var invariant = require('invariant');
var ATTR_NAME = DOMProperty.ID_ATTRIBUTE_NAME; var ATTR_NAME = DOMProperty.ID_ATTRIBUTE_NAME;
var Flags = ReactDOMComponentFlags; var Flags = ReactDOMComponentFlags;
var internalInstanceKey = var randomKey = Math.random().toString(36).slice(2);
'__reactInternalInstance$' + Math.random().toString(36).slice(2);
var internalInstanceKey = '__reactInternalInstance$' + randomKey;
var internalEventHandlersKey = '__reactEventHandlers$' + randomKey;
/** /**
* Check if a given node should be cached. * Check if a given node should be cached.
@@ -218,6 +221,14 @@ function getNodeFromInstance(inst) {
return inst._hostNode; return inst._hostNode;
} }
function getFiberEventHandlersFromNode(node) {
return node[internalEventHandlersKey] || null;
}
function updateFiberEventHandlers(node, props) {
node[internalEventHandlersKey] = props;
}
var ReactDOMComponentTree = { var ReactDOMComponentTree = {
getClosestInstanceFromNode: getClosestInstanceFromNode, getClosestInstanceFromNode: getClosestInstanceFromNode,
getInstanceFromNode: getInstanceFromNode, getInstanceFromNode: getInstanceFromNode,
@@ -226,6 +237,8 @@ var ReactDOMComponentTree = {
precacheNode: precacheNode, precacheNode: precacheNode,
uncacheNode: uncacheNode, uncacheNode: uncacheNode,
precacheFiberNode: precacheFiberNode, precacheFiberNode: precacheFiberNode,
getFiberEventHandlersFromNode,
updateFiberEventHandlers,
}; };
module.exports = ReactDOMComponentTree; module.exports = ReactDOMComponentTree;
@@ -14,6 +14,7 @@
var invariant = require('invariant'); var invariant = require('invariant');
var instanceCache = {}; var instanceCache = {};
var instanceProps = {};
/** /**
* Drill down (through composites and empty components) until we get a host or * Drill down (through composites and empty components) until we get a host or
@@ -65,6 +66,14 @@ function getTagFromInstance(inst) {
return tag; return tag;
} }
function getFiberEventHandlersFromTag(tag) {
return instanceProps[tag] || null;
}
function updateFiberEventHandlers(tag, props) {
instanceProps[tag] = props;
}
var ReactNativeComponentTree = { var ReactNativeComponentTree = {
getClosestInstanceFromNode: getInstanceFromTag, getClosestInstanceFromNode: getInstanceFromTag,
getInstanceFromNode: getInstanceFromTag, getInstanceFromNode: getInstanceFromTag,
@@ -73,6 +82,8 @@ var ReactNativeComponentTree = {
precacheNode, precacheNode,
uncacheFiberNode, uncacheFiberNode,
uncacheNode, uncacheNode,
getFiberEventHandlersFromNode: getFiberEventHandlersFromTag,
updateFiberEventHandlers,
}; };
module.exports = ReactNativeComponentTree; module.exports = ReactNativeComponentTree;
+17 -11
View File
@@ -33,7 +33,11 @@ const emptyObject = require('emptyObject');
const findNodeHandle = require('findNodeHandle'); const findNodeHandle = require('findNodeHandle');
const invariant = require('invariant'); const invariant = require('invariant');
const { precacheFiberNode, uncacheFiberNode } = ReactNativeComponentTree; const {
precacheFiberNode,
uncacheFiberNode,
updateFiberEventHandlers,
} = ReactNativeComponentTree;
ReactNativeInjection.inject(); ReactNativeInjection.inject();
@@ -114,7 +118,6 @@ const NativeRenderer = ReactFiberReconciler({
instance : Instance, instance : Instance,
type : string, type : string,
newProps : Props, newProps : Props,
rootContainerInstance : Object,
internalInstanceHandle : Object internalInstanceHandle : Object
) : void { ) : void {
// Noop // Noop
@@ -122,15 +125,15 @@ const NativeRenderer = ReactFiberReconciler({
commitUpdate( commitUpdate(
instance : Instance, instance : Instance,
updatePayloadTODO : Object,
type : string, type : string,
oldProps : Props, oldProps : Props,
newProps : Props, newProps : Props,
rootContainerInstance : Object,
internalInstanceHandle : Object internalInstanceHandle : Object
) : void { ) : void {
const viewConfig = instance.viewConfig; const viewConfig = instance.viewConfig;
precacheFiberNode(internalInstanceHandle, instance._nativeTag); updateFiberEventHandlers(instance._nativeTag, newProps);
const updatePayload = ReactNativeAttributePayload.diff( const updatePayload = ReactNativeAttributePayload.diff(
oldProps, oldProps,
@@ -149,7 +152,7 @@ const NativeRenderer = ReactFiberReconciler({
type : string, type : string,
props : Props, props : Props,
rootContainerInstance : Container, rootContainerInstance : Container,
hostContext : Object, hostContext : {||},
internalInstanceHandle : Object internalInstanceHandle : Object
) : Instance { ) : Instance {
const tag = ReactNativeTagHandles.allocateTag(); const tag = ReactNativeTagHandles.allocateTag();
@@ -178,6 +181,7 @@ const NativeRenderer = ReactFiberReconciler({
const component = new NativeHostComponent(tag, viewConfig); const component = new NativeHostComponent(tag, viewConfig);
precacheFiberNode(internalInstanceHandle, tag); precacheFiberNode(internalInstanceHandle, tag);
updateFiberEventHandlers(tag, props);
return component; return component;
}, },
@@ -185,7 +189,7 @@ const NativeRenderer = ReactFiberReconciler({
createTextInstance( createTextInstance(
text : string, text : string,
rootContainerInstance : Container, rootContainerInstance : Container,
hostContext : Object, hostContext : {||},
internalInstanceHandle : Object, internalInstanceHandle : Object,
) : TextInstance { ) : TextInstance {
const tag = ReactNativeTagHandles.allocateTag(); const tag = ReactNativeTagHandles.allocateTag();
@@ -224,11 +228,11 @@ const NativeRenderer = ReactFiberReconciler({
return false; return false;
}, },
getRootHostContext() { getRootHostContext() : {||} {
return emptyObject; return emptyObject;
}, },
getChildHostContext() { getChildHostContext() : {||} {
return emptyObject; return emptyObject;
}, },
@@ -290,9 +294,11 @@ const NativeRenderer = ReactFiberReconciler({
instance : Instance, instance : Instance,
type : string, type : string,
oldProps : Props, oldProps : Props,
newProps : Props newProps : Props,
) : boolean { rootContainerInstance : Container,
return true; hostContext : {||}
) : null | Object {
return emptyObject;
}, },
removeChild( removeChild(
+5 -3
View File
@@ -29,6 +29,8 @@ var {
} = require('ReactPriorityLevel'); } = require('ReactPriorityLevel');
var emptyObject = require('emptyObject'); var emptyObject = require('emptyObject');
const UPDATE_SIGNAL = {};
var scheduledAnimationCallback = null; var scheduledAnimationCallback = null;
var scheduledDeferredCallback = null; var scheduledDeferredCallback = null;
@@ -78,15 +80,15 @@ var NoopRenderer = ReactFiberReconciler({
return false; return false;
}, },
prepareUpdate(instance : Instance, type : string, oldProps : Props, newProps : Props) : boolean { prepareUpdate(instance : Instance, type : string, oldProps : Props, newProps : Props) : null | {} {
return true; return UPDATE_SIGNAL;
}, },
commitMount(instance : Instance, type : string, newProps : Props) : void { commitMount(instance : Instance, type : string, newProps : Props) : void {
// Noop // Noop
}, },
commitUpdate(instance : Instance, type : string, oldProps : Props, newProps : Props) : void { commitUpdate(instance : Instance, updatePayload : Object, type : string, oldProps : Props, newProps : Props) : void {
instance.prop = newProps.prop; instance.prop = newProps.prop;
}, },
@@ -70,8 +70,8 @@ if (__DEV__) {
var warnedAboutStatelessRefs = {}; var warnedAboutStatelessRefs = {};
} }
module.exports = function<T, P, I, TI, PI, C, CX>( module.exports = function<T, P, I, TI, PI, C, CX, PL>(
config : HostConfig<T, P, I, TI, PI, C, CX>, config : HostConfig<T, P, I, TI, PI, C, CX, PL>,
hostContext : HostContext<C, CX>, hostContext : HostContext<C, CX>,
scheduleUpdate : (fiber : Fiber, priorityLevel : PriorityLevel) => void, scheduleUpdate : (fiber : Fiber, priorityLevel : PriorityLevel) => void,
getPriorityContext : () => PriorityLevel, getPriorityContext : () => PriorityLevel,
@@ -13,7 +13,6 @@
'use strict'; 'use strict';
import type { Fiber } from 'ReactFiber'; import type { Fiber } from 'ReactFiber';
import type { HostContext } from 'ReactFiberHostContext';
import type { HostConfig } from 'ReactFiberReconciler'; import type { HostConfig } from 'ReactFiberReconciler';
var ReactTypeOfWork = require('ReactTypeOfWork'); var ReactTypeOfWork = require('ReactTypeOfWork');
@@ -34,9 +33,8 @@ var {
ContentReset, ContentReset,
} = require('ReactTypeOfSideEffect'); } = require('ReactTypeOfSideEffect');
module.exports = function<T, P, I, TI, PI, C, CX>( module.exports = function<T, P, I, TI, PI, C, CX, PL>(
config : HostConfig<T, P, I, TI, PI, C, CX>, config : HostConfig<T, P, I, TI, PI, C, CX, PL>,
hostContext : HostContext<C, CX>,
captureError : (failedFiber : Fiber, error: Error) => ?Fiber captureError : (failedFiber : Fiber, error: Error) => ?Fiber
) { ) {
@@ -51,10 +49,6 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
getPublicInstance, getPublicInstance,
} = config; } = config;
const {
getRootHostContainer,
} = hostContext;
// Capture errors so they don't interrupt unmounting. // Capture errors so they don't interrupt unmounting.
function safelyCallComponentWillUnmount(current, instance) { function safelyCallComponentWillUnmount(current, instance) {
try { try {
@@ -367,9 +361,13 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
// Commit the work prepared earlier. // Commit the work prepared earlier.
const newProps = finishedWork.memoizedProps; const newProps = finishedWork.memoizedProps;
const oldProps = current.memoizedProps; const oldProps = current.memoizedProps;
const rootContainerInstance = getRootHostContainer();
const type = finishedWork.type; const type = finishedWork.type;
commitUpdate(instance, type, oldProps, newProps, rootContainerInstance, finishedWork); // TODO: Type the updateQueue to be specific to host components.
const updatePayload : null | PL = (finishedWork.updateQueue : any);
finishedWork.updateQueue = null;
if (updatePayload) {
commitUpdate(instance, updatePayload, type, oldProps, newProps, finishedWork);
}
} }
detachRefIfNeeded(current, finishedWork); detachRefIfNeeded(current, finishedWork);
return; return;
@@ -438,8 +436,7 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
) { ) {
const type = finishedWork.type; const type = finishedWork.type;
const props = finishedWork.memoizedProps; const props = finishedWork.memoizedProps;
const rootContainerInstance = getRootHostContainer(); commitMount(instance, type, props, finishedWork);
commitMount(instance, type, props, rootContainerInstance, finishedWork);
} }
return; return;
@@ -47,8 +47,8 @@ if (__DEV__) {
var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber'); var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber');
} }
module.exports = function<T, P, I, TI, PI, C, CX>( module.exports = function<T, P, I, TI, PI, C, CX, PL>(
config : HostConfig<T, P, I, TI, PI, C, CX>, config : HostConfig<T, P, I, TI, PI, C, CX, PL>,
hostContext : HostContext<C, CX>, hostContext : HostContext<C, CX>,
) { ) {
const { const {
@@ -186,8 +186,9 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
} }
return null; return null;
} }
case HostComponent: case HostComponent: {
popHostContext(workInProgress); popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type; const type = workInProgress.type;
const newProps = workInProgress.memoizedProps; const newProps = workInProgress.memoizedProps;
if (current && workInProgress.stateNode != null) { if (current && workInProgress.stateNode != null) {
@@ -200,8 +201,12 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
// Even better would be if children weren't special cased at all tho. // Even better would be if children weren't special cased at all tho.
const instance : I = workInProgress.stateNode; const instance : I = workInProgress.stateNode;
const currentHostContext = getHostContext(); const currentHostContext = getHostContext();
if (prepareUpdate(instance, type, oldProps, newProps, currentHostContext)) { const updatePayload = prepareUpdate(instance, type, oldProps, newProps, rootContainerInstance, currentHostContext);
// This returns true if there was something to update. // TODO: Type this specific to this type of component.
workInProgress.updateQueue = (updatePayload : any);
// If the update payload indicates that there is a change or if there
// is a new ref we mark this as an update.
if (updatePayload || current.ref !== workInProgress.ref) {
markUpdate(workInProgress); markUpdate(workInProgress);
} }
} else { } else {
@@ -214,7 +219,6 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
} }
} }
const rootContainerInstance = getRootHostContainer();
const currentHostContext = getHostContext(); const currentHostContext = getHostContext();
// TODO: Move createInstance to beginWork and keep it on a context // TODO: Move createInstance to beginWork and keep it on a context
// "stack" as the parent. Then append children as we go in beginWork // "stack" as the parent. Then append children as we go in beginWork
@@ -244,7 +248,8 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
} }
} }
return null; return null;
case HostText: }
case HostText: {
let newText = workInProgress.memoizedProps; let newText = workInProgress.memoizedProps;
if (current && workInProgress.stateNode != null) { if (current && workInProgress.stateNode != null) {
const oldText = current.memoizedProps; const oldText = current.memoizedProps;
@@ -268,6 +273,7 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
workInProgress.stateNode = textInstance; workInProgress.stateNode = textInstance;
} }
return null; return null;
}
case CoroutineComponent: case CoroutineComponent:
return moveCoroutineToHandlerPhase(current, workInProgress); return moveCoroutineToHandlerPhase(current, workInProgress);
case CoroutineHandlerPhase: case CoroutineHandlerPhase:
@@ -34,8 +34,8 @@ export type HostContext<C, CX> = {
resetHostContainer() : void, resetHostContainer() : void,
}; };
module.exports = function<T, P, I, TI, PI, C, CX>( module.exports = function<T, P, I, TI, PI, C, CX, PL>(
config : HostConfig<T, P, I, TI, PI, C, CX> config : HostConfig<T, P, I, TI, PI, C, CX, PL>
) : HostContext<C, CX> { ) : HostContext<C, CX> {
const { const {
getChildHostContext, getChildHostContext,
@@ -43,7 +43,7 @@ export type Deadline = {
type OpaqueNode = Fiber; type OpaqueNode = Fiber;
export type HostConfig<T, P, I, TI, PI, C, CX> = { export type HostConfig<T, P, I, TI, PI, C, CX, PL> = {
getRootHostContext(rootContainerInstance : C) : CX, getRootHostContext(rootContainerInstance : C) : CX,
getChildHostContext(parentHostContext : CX, type : T) : CX, getChildHostContext(parentHostContext : CX, type : T) : CX,
@@ -53,9 +53,9 @@ export type HostConfig<T, P, I, TI, PI, C, CX> = {
appendInitialChild(parentInstance : I, child : I | TI) : void, appendInitialChild(parentInstance : I, child : I | TI) : void,
finalizeInitialChildren(parentInstance : I, type : T, props : P, rootContainerInstance : C) : boolean, finalizeInitialChildren(parentInstance : I, type : T, props : P, rootContainerInstance : C) : boolean,
prepareUpdate(instance : I, type : T, oldProps : P, newProps : P, hostContext : CX) : boolean, prepareUpdate(instance : I, type : T, oldProps : P, newProps : P, rootContainerInstance : C, hostContext : CX) : null | PL,
commitUpdate(instance : I, type : T, oldProps : P, newProps : P, rootContainerInstance : C, internalInstanceHandle : OpaqueNode) : void, commitUpdate(instance : I, updatePayload : PL, type : T, oldProps : P, newProps : P, internalInstanceHandle : OpaqueNode) : void,
commitMount(instance : I, type : T, newProps : P, rootContainerInstance : C, internalInstanceHandle : OpaqueNode) : void, commitMount(instance : I, type : T, newProps : P, internalInstanceHandle : OpaqueNode) : void,
shouldSetTextContent(props : P) : boolean, shouldSetTextContent(props : P) : boolean,
resetTextContent(instance : I) : void, resetTextContent(instance : I) : void,
@@ -102,7 +102,7 @@ getContextForSubtree._injectFiber(function(fiber : Fiber) {
parentContext; parentContext;
}); });
module.exports = function<T, P, I, TI, PI, C, CX>(config : HostConfig<T, P, I, TI, PI, C, CX>) : Reconciler<C, I, TI> { module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P, I, TI, PI, C, CX, PL>) : Reconciler<C, I, TI> {
var { var {
scheduleUpdate, scheduleUpdate,
@@ -88,7 +88,7 @@ if (__DEV__) {
var timeHeuristicForUnitOfWork = 1; var timeHeuristicForUnitOfWork = 1;
module.exports = function<T, P, I, TI, PI, C, CX>(config : HostConfig<T, P, I, TI, PI, C, CX>) { module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P, I, TI, PI, C, CX, PL>) {
const hostContext = ReactFiberHostContext(config); const hostContext = ReactFiberHostContext(config);
const { popHostContainer, popHostContext, resetHostContainer } = hostContext; const { popHostContainer, popHostContext, resetHostContainer } = hostContext;
const { beginWork, beginFailedWork } = ReactFiberBeginWork( const { beginWork, beginFailedWork } = ReactFiberBeginWork(
@@ -104,7 +104,7 @@ module.exports = function<T, P, I, TI, PI, C, CX>(config : HostConfig<T, P, I, T
commitWork, commitWork,
commitLifeCycles, commitLifeCycles,
commitRef, commitRef,
} = ReactFiberCommitWork(config, hostContext, captureError); } = ReactFiberCommitWork(config, captureError);
const { const {
scheduleAnimationCallback: hostScheduleAnimationCallback, scheduleAnimationCallback: hostScheduleAnimationCallback,
scheduleDeferredCallback: hostScheduleDeferredCallback, scheduleDeferredCallback: hostScheduleDeferredCallback,
@@ -12,7 +12,6 @@
'use strict'; 'use strict';
var { HostComponent } = require('ReactTypeOfWork'); var { HostComponent } = require('ReactTypeOfWork');
var { getNodeFromInstance, getInstanceFromNode } = require('EventPluginUtils');
function getParent(inst) { function getParent(inst) {
if (inst._hostParent !== undefined) { if (inst._hostParent !== undefined) {
@@ -27,13 +26,8 @@ function getParent(inst) {
// host node but that wouldn't work for React Native and doesn't let us // host node but that wouldn't work for React Native and doesn't let us
// do the portal feature. // do the portal feature.
} while (inst && inst.tag !== HostComponent); } while (inst && inst.tag !== HostComponent);
// Going through the Host Node will guarantee that we get the "current"
// Fiber, instead of the alternate because that pointer is updated when
// props update.
// TODO: This is a bit hacky and possibly slow. We should ideally have
// something in the reconciler that allow us to do this safely.
if (inst) { if (inst) {
return getInstanceFromNode(getNodeFromInstance(inst)); return inst;
} }
} }
return null; return null;
@@ -126,7 +126,13 @@ var EventPluginHub = {
// TODO: shouldPreventMouseEvent is DOM-specific and definitely should not // TODO: shouldPreventMouseEvent is DOM-specific and definitely should not
// live here; needs to be moved to a better place soon // live here; needs to be moved to a better place soon
if (typeof inst.tag === 'number') { if (typeof inst.tag === 'number') {
const props = inst.memoizedProps; const props = EventPluginUtils.getFiberEventHandlersFromNode(
inst.stateNode
);
if (!props) {
// Work in progress.
return null;
}
listener = props[registrationName]; listener = props[registrationName];
if (shouldPreventMouseEvent(registrationName, inst.type, props)) { if (shouldPreventMouseEvent(registrationName, inst.type, props)) {
return null; return null;
@@ -219,6 +219,9 @@ var EventPluginUtils = {
executeDispatchesInOrderStopAtTrue: executeDispatchesInOrderStopAtTrue, executeDispatchesInOrderStopAtTrue: executeDispatchesInOrderStopAtTrue,
hasDispatches: hasDispatches, hasDispatches: hasDispatches,
getFiberEventHandlersFromNode: function(node) {
return ComponentTree.getFiberEventHandlersFromNode(node);
},
getInstanceFromNode: function(node) { getInstanceFromNode: function(node) {
return ComponentTree.getInstanceFromNode(node); return ComponentTree.getInstanceFromNode(node);
}, },
@@ -47,6 +47,8 @@ type TextInstance = {|
tag : 'TEXT', tag : 'TEXT',
|}; |};
const UPDATE_SIGNAL = {};
var TestRenderer = ReactFiberReconciler({ var TestRenderer = ReactFiberReconciler({
getRootHostContext() { getRootHostContext() {
return emptyObject; return emptyObject;
@@ -102,17 +104,18 @@ var TestRenderer = ReactFiberReconciler({
type : string, type : string,
oldProps : Props, oldProps : Props,
newProps : Props, newProps : Props,
rootContainerInstance : Container,
hostContext : Object, hostContext : Object,
) : boolean { ) : null | {} {
return true; return UPDATE_SIGNAL;
}, },
commitUpdate( commitUpdate(
instance : Instance, instance : Instance,
updatePayload : {},
type : string, type : string,
oldProps : Props, oldProps : Props,
newProps : Props, newProps : Props,
rootContainerInstance : Container,
internalInstanceHandle : Object, internalInstanceHandle : Object,
) : void { ) : void {
instance.type = type; instance.type = type;
@@ -123,7 +126,6 @@ var TestRenderer = ReactFiberReconciler({
instance : Instance, instance : Instance,
type : string, type : string,
newProps : Props, newProps : Props,
rootContainerInstance : Object,
internalInstanceHandle : Object internalInstanceHandle : Object
) : void { ) : void {
// noop // noop