@samitouri / QOS-React / commits / b354db22a9

[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.

Sebastian Markbåge committed Jan 19, 2017 at 21:56 UTC b354db22a9a3ecf79945a618cf97bed688a9364c
20 files changed +412 -173
scripts/fiber/tests-failing.txt
+3
@@ -37,6 +37,9 @@ src/renderers/dom/shared/__tests__/ReactRenderDocument-test.js
37 * should throw on full document render w/ no markup
38 * supports findDOMNode on full-page components
39
40 +src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js
41 +* should control a value in reentrant events
42 +
43 src/renderers/shared/__tests__/ReactPerf-test.js
44 * should count no-op update as waste
45 * should count no-op update in child as waste
scripts/fiber/tests-passing.txt
+1 -1
@@ -546,6 +546,7 @@ src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
546 * findDOMNode should find dom element after expanding a fragment
547 * should bubble events from the portal to the parent
548 * should not onMouseLeave when staying in the portal
549 +* should not update event handlers until commit
550 * should not crash encountering low-priority tree
551 * throws if non-element passed to top-level render
552 * 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
960
961 src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js
962 * should properly control a value even if no event listener exists
962 -* should control a value in reentrant events
963 * should control values in reentrant events with different targets
964 * should display `defaultValue` of number 0
965 * only assigns defaultValue if it changes
src/renderers/art/ReactARTFiber.js
+4 -2
@@ -42,6 +42,8 @@ const TYPES = {
42 TEXT: 'Text',
43 };
44
45 +const UPDATE_SIGNAL = {};
46 +
47 /** Helper Methods */
48
49 function addEventListeners(instance, type, listener) {
@@ -418,7 +420,7 @@ const ARTRenderer = ReactFiberReconciler({
420 // Noop
421 },
422
421 - commitUpdate(instance, type, oldProps, newProps) {
423 + commitUpdate(instance, updatePayload, type, oldProps, newProps) {
424 instance._applyProps(instance, newProps, oldProps);
425 },
426
@@ -482,7 +484,7 @@ const ARTRenderer = ReactFiberReconciler({
484 },
485
486 prepareUpdate(domElement, type, oldProps, newProps) {
485 - return true;
487 + return UPDATE_SIGNAL;
488 },
489
490 removeChild(parentInstance, child) {
src/renderers/dom/fiber/ReactDOMFiber.js
+15 -9
@@ -35,9 +35,13 @@ var {
35 createElement,
36 getChildNamespace,
37 setInitialProperties,
38 + diffProperties,
39 updateProperties,
40 } = ReactDOMFiberComponent;
40 -var { precacheFiberNode } = ReactDOMComponentTree;
41 +var {
42 + precacheFiberNode,
43 + updateFiberEventHandlers,
44 +} = ReactDOMComponentTree;
45
46 if (__DEV__) {
47 var validateDOMNesting = require('validateDOMNesting');
@@ -184,6 +188,7 @@ var DOMRenderer = ReactFiberReconciler({
188 }
189 const domElement : Instance = createElement(type, props, rootContainerInstance, parentNamespace);
190 precacheFiberNode(internalInstanceHandle, domElement);
191 + updateFiberEventHandlers(domElement, props);
192 return domElement;
193 },
194
@@ -206,8 +211,9 @@ var DOMRenderer = ReactFiberReconciler({
211 type : string,
212 oldProps : Props,
213 newProps : Props,
214 + rootContainerInstance : Container,
215 hostContext : HostContext,
210 - ) : boolean {
216 + ) : null | Array<mixed> {
217 if (__DEV__) {
218 const hostContextDev = ((hostContext : any) : HostContextDev);
219 if (typeof newProps.children !== typeof oldProps.children && (
@@ -218,14 +224,13 @@ var DOMRenderer = ReactFiberReconciler({
224 validateDOMNesting(null, String(newProps.children), null, ownAncestorInfo);
225 }
226 }
221 - return true;
227 + return diffProperties(domElement, type, oldProps, newProps, rootContainerInstance);
228 },
229
230 commitMount(
231 domElement : Instance,
232 type : string,
233 newProps : Props,
228 - rootContainerInstance : Container,
234 internalInstanceHandle : Object,
235 ) : void {
236 ((domElement : any) : HTMLButtonElement | HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement).focus();
@@ -233,16 +238,17 @@ var DOMRenderer = ReactFiberReconciler({
238
239 commitUpdate(
240 domElement : Instance,
241 + updatePayload : Array<mixed>,
242 type : string,
243 oldProps : Props,
244 newProps : Props,
239 - rootContainerInstance : Container,
245 internalInstanceHandle : Object,
246 ) : void {
242 - // Update the internal instance handle so that we know which props are
243 - // the current ones.
244 - precacheFiberNode(internalInstanceHandle, domElement);
245 - updateProperties(domElement, type, oldProps, newProps, rootContainerInstance);
247 + // Update the props handle so that we know which props are the ones with
248 + // with current event handlers.
249 + updateFiberEventHandlers(domElement, newProps);
250 + // Apply the diff to the DOM node.
251 + updateProperties(domElement, updatePayload, type, oldProps, newProps);
252 },
253
254 shouldSetTextContent(props : Props) : boolean {
src/renderers/dom/fiber/ReactDOMFiberComponent.js
+208 -103
@@ -301,69 +301,15 @@ function isCustomComponent(tagName, props) {
301 return tagName.indexOf('-') >= 0 || props.is != null;
302 }
303
304 -/**
305 - * Reconciles the properties by detecting differences in property values and
306 - * updating the DOM as necessary. This function is probably the single most
307 - * critical path for performance optimization.
308 - *
309 - * TODO: Benchmark whether checking for changed values in memory actually
310 - * improves performance (especially statically positioned elements).
311 - * TODO: Benchmark the effects of putting this at the top since 99% of props
312 - * do not change for a given reconciliation.
313 - * TODO: Benchmark areas that can be improved with caching.
314 - */
315 -function updateDOMProperties(
304 +function setInitialDOMProperties(
305 domElement : Element,
306 rootContainerElement : Element,
318 - lastProps : null | Object,
307 nextProps : Object,
320 - wasCustomComponentTag : boolean,
308 isCustomComponentTag : boolean,
309 ) : void {
323 - var propKey;
324 - var styleName;
325 - var styleUpdates;
326 - for (propKey in lastProps) {
327 - if (nextProps.hasOwnProperty(propKey) ||
328 - !lastProps.hasOwnProperty(propKey) ||
329 - lastProps[propKey] == null) {
330 - continue;
331 - }
332 - if (propKey === STYLE) {
333 - var lastStyle = lastProps[propKey];
334 - for (styleName in lastStyle) {
335 - if (lastStyle.hasOwnProperty(styleName)) {
336 - styleUpdates = styleUpdates || {};
337 - styleUpdates[styleName] = '';
338 - }
339 - }
340 - } else if (propKey === DANGEROUSLY_SET_INNER_HTML ||
341 - propKey === CHILDREN) {
342 - // TODO: Clear innerHTML. This is currently broken in Fiber because we are
343 - // too late to clear everything at this point because new children have
344 - // already been inserted.
345 - } else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING) {
346 - // Noop
347 - } else if (registrationNameModules.hasOwnProperty(propKey)) {
348 - // Do nothing for deleted listeners.
349 - } else if (wasCustomComponentTag) {
350 - DOMPropertyOperations.deleteValueForAttribute(
351 - domElement,
352 - propKey
353 - );
354 - } else if (
355 - DOMProperty.properties[propKey] ||
356 - DOMProperty.isCustomAttribute(propKey)) {
357 - DOMPropertyOperations.deleteValueForProperty(domElement, propKey);
358 - }
359 - }
360 - for (propKey in nextProps) {
310 + for (var propKey in nextProps) {
311 var nextProp = nextProps[propKey];
362 - var lastProp =
363 - lastProps != null ? lastProps[propKey] : undefined;
364 - if (!nextProps.hasOwnProperty(propKey) ||
365 - nextProp === lastProp ||
366 - nextProp == null && lastProp == null) {
312 + if (!nextProps.hasOwnProperty(propKey)) {
313 continue;
314 }
315 if (propKey === STYLE) {
@@ -374,38 +320,16 @@ function updateDOMProperties(
320 Object.freeze(nextProp);
321 }
322 }
377 - if (lastProp) {
378 - // Unset styles on `lastProp` but not on `nextProp`.
379 - for (styleName in lastProp) {
380 - if (lastProp.hasOwnProperty(styleName) &&
381 - (!nextProp || !nextProp.hasOwnProperty(styleName))) {
382 - styleUpdates = styleUpdates || {};
383 - styleUpdates[styleName] = '';
384 - }
385 - }
386 - // Update styles that changed since `lastProp`.
387 - for (styleName in nextProp) {
388 - if (nextProp.hasOwnProperty(styleName) &&
389 - lastProp[styleName] !== nextProp[styleName]) {
390 - styleUpdates = styleUpdates || {};
391 - styleUpdates[styleName] = nextProp[styleName];
392 - }
393 - }
394 - } else {
395 - // Relies on `updateStylesByID` not mutating `styleUpdates`.
396 - styleUpdates = nextProp;
397 - }
323 + // Relies on `updateStylesByID` not mutating `styleUpdates`.
324 + // TODO: call ReactInstrumentation.debugTool.onHostOperation in DEV.
325 + CSSPropertyOperations.setValueForStyles(
326 + domElement,
327 + nextProp,
328 + );
329 } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
330 var nextHtml = nextProp ? nextProp[HTML] : undefined;
400 - var lastHtml = lastProp ? lastProp[HTML] : undefined;
401 - // Intentional use of != to avoid catching zero/false.
331 if (nextHtml != null) {
403 - if (lastHtml !== nextHtml) {
404 - setInnerHTML(domElement, '' + nextHtml);
405 - }
406 - } else {
407 - // TODO: It might be too late to clear this if we have children
408 - // inserted already.
332 + setInnerHTML(domElement, nextHtml);
333 }
334 } else if (propKey === CHILDREN) {
335 if (typeof nextProp === 'string') {
@@ -433,18 +357,57 @@ function updateDOMProperties(
357 // brings us in line with the same behavior we have on initial render.
358 if (nextProp != null) {
359 DOMPropertyOperations.setValueForProperty(domElement, propKey, nextProp);
360 + }
361 + }
362 + }
363 +}
364 +
365 +function updateDOMProperties(
366 + domElement : Element,
367 + updatePayload : Array<any>,
368 + wasCustomComponentTag : boolean,
369 + isCustomComponentTag : boolean,
370 +) : void {
371 + // TODO: Handle wasCustomComponentTag
372 + for (var i = 0; i < updatePayload.length; i+=2) {
373 + var propKey = updatePayload[i];
374 + var propValue = updatePayload[i + 1];
375 + if (propKey === STYLE) {
376 + // TODO: call ReactInstrumentation.debugTool.onHostOperation in DEV.
377 + CSSPropertyOperations.setValueForStyles(
378 + domElement,
379 + propValue,
380 + );
381 + } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
382 + setInnerHTML(domElement, propValue);
383 + } else if (propKey === CHILDREN) {
384 + setTextContent(domElement, propValue);
385 + } else if (isCustomComponentTag) {
386 + if (propValue != null) {
387 + DOMPropertyOperations.setValueForAttribute(
388 + domElement,
389 + propKey,
390 + propValue
391 + );
392 + } else {
393 + DOMPropertyOperations.deleteValueForAttribute(
394 + domElement,
395 + propKey
396 + );
397 + }
398 + } else if (
399 + DOMProperty.properties[propKey] ||
400 + DOMProperty.isCustomAttribute(propKey)) {
401 + // If we're updating to null or undefined, we should remove the property
402 + // from the DOM node instead of inadvertently setting to a string. This
403 + // brings us in line with the same behavior we have on initial render.
404 + if (propValue != null) {
405 + DOMPropertyOperations.setValueForProperty(domElement, propKey, propValue);
406 } else {
407 DOMPropertyOperations.deleteValueForProperty(domElement, propKey);
408 }
409 }
410 }
441 - if (styleUpdates) {
442 - // TODO: call ReactInstrumentation.debugTool.onHostOperation in DEV.
443 - CSSPropertyOperations.setValueForStyles(
444 - domElement,
445 - styleUpdates,
446 - );
447 - }
411 }
412
413 // Assumes there is no parent namespace.
@@ -592,12 +555,10 @@ var ReactDOMFiberComponent = {
555
556 assertValidProps(tag, props);
557
595 - updateDOMProperties(
558 + setInitialDOMProperties(
559 domElement,
560 rootContainerElement,
598 - null,
561 props,
600 - false,
562 isCustomComponentTag
563 );
564
@@ -626,35 +587,42 @@ var ReactDOMFiberComponent = {
587 }
588 },
589
629 - updateProperties(
590 + // Calculate the diff between the two objects.
591 + diffProperties(
592 domElement : Element,
593 tag : string,
594 lastRawProps : Object,
595 nextRawProps : Object,
596 rootContainerElement : Element,
635 - ) : void {
597 + ) : null | Array<mixed> {
598 if (__DEV__) {
599 validatePropertiesInDevelopment(tag, nextRawProps);
600 }
601
602 + var updatePayload : null | Array<any> = null;
603 +
604 var lastProps : Object;
605 var nextProps : Object;
606 switch (tag) {
607 case 'input':
608 lastProps = ReactDOMFiberInput.getHostProps(domElement, lastRawProps);
609 nextProps = ReactDOMFiberInput.getHostProps(domElement, nextRawProps);
610 + updatePayload = [];
611 break;
612 case 'option':
613 lastProps = ReactDOMFiberOption.getHostProps(domElement, lastRawProps);
614 nextProps = ReactDOMFiberOption.getHostProps(domElement, nextRawProps);
615 + updatePayload = [];
616 break;
617 case 'select':
618 lastProps = ReactDOMFiberSelect.getHostProps(domElement, lastRawProps);
619 nextProps = ReactDOMFiberSelect.getHostProps(domElement, nextRawProps);
620 + updatePayload = [];
621 break;
622 case 'textarea':
623 lastProps = ReactDOMFiberTextarea.getHostProps(domElement, lastRawProps);
624 nextProps = ReactDOMFiberTextarea.getHostProps(domElement, nextRawProps);
625 + updatePayload = [];
626 break;
627 default:
628 lastProps = lastRawProps;
@@ -668,17 +636,154 @@ var ReactDOMFiberComponent = {
636 }
637
638 assertValidProps(tag, nextProps);
671 - var wasCustomComponentTag = isCustomComponent(tag, lastProps);
672 - var isCustomComponentTag = isCustomComponent(tag, nextProps);
639 +
640 + var propKey;
641 + var styleName;
642 + var styleUpdates = null;
643 + for (propKey in lastProps) {
644 + if (nextProps.hasOwnProperty(propKey) ||
645 + !lastProps.hasOwnProperty(propKey) ||
646 + lastProps[propKey] == null) {
647 + continue;
648 + }
649 + if (propKey === STYLE) {
650 + var lastStyle = lastProps[propKey];
651 + for (styleName in lastStyle) {
652 + if (lastStyle.hasOwnProperty(styleName)) {
653 + if (!styleUpdates) {
654 + styleUpdates = {};
655 + }
656 + styleUpdates[styleName] = '';
657 + }
658 + }
659 + } else if (propKey === DANGEROUSLY_SET_INNER_HTML ||
660 + propKey === CHILDREN) {
661 + // Noop. This is handled by the clear text mechanism.
662 + } else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING) {
663 + // Noop
664 + } else if (registrationNameModules.hasOwnProperty(propKey)) {
665 + // This is a special case. If any listener updates we need to ensure
666 + // that the "current" fiber pointer gets updated so we need a commit
667 + // to update this element.
668 + if (!updatePayload) {
669 + updatePayload = [];
670 + }
671 + } else {
672 + // For all other deleted properties we add it to the queue. We use
673 + // the whitelist in the commit phase instead.
674 + (updatePayload = updatePayload || []).push(propKey, null);
675 + }
676 + }
677 + for (propKey in nextProps) {
678 + var nextProp = nextProps[propKey];
679 + var lastProp =
680 + lastProps != null ? lastProps[propKey] : undefined;
681 + if (!nextProps.hasOwnProperty(propKey) ||
682 + nextProp === lastProp ||
683 + nextProp == null && lastProp == null) {
684 + continue;
685 + }
686 + if (propKey === STYLE) {
687 + if (__DEV__) {
688 + if (nextProp) {
689 + // Freeze the next style object so that we can assume it won't be
690 + // mutated. We have already warned for this in the past.
691 + Object.freeze(nextProp);
692 + }
693 + }
694 + if (lastProp) {
695 + // Unset styles on `lastProp` but not on `nextProp`.
696 + for (styleName in lastProp) {
697 + if (lastProp.hasOwnProperty(styleName) &&
698 + (!nextProp || !nextProp.hasOwnProperty(styleName))) {
699 + if (!styleUpdates) {
700 + styleUpdates = {};
701 + }
702 + styleUpdates[styleName] = '';
703 + }
704 + }
705 + // Update styles that changed since `lastProp`.
706 + for (styleName in nextProp) {
707 + if (nextProp.hasOwnProperty(styleName) &&
708 + lastProp[styleName] !== nextProp[styleName]) {
709 + if (!styleUpdates) {
710 + styleUpdates = {};
711 + }
712 + styleUpdates[styleName] = nextProp[styleName];
713 + }
714 + }
715 + } else {
716 + // Relies on `updateStylesByID` not mutating `styleUpdates`.
717 + if (!styleUpdates) {
718 + if (!updatePayload) {
719 + updatePayload = [];
720 + }
721 + updatePayload.push(propKey, styleUpdates);
722 + }
723 + styleUpdates = nextProp;
724 + }
725 + } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
726 + var nextHtml = nextProp ? nextProp[HTML] : undefined;
727 + var lastHtml = lastProp ? lastProp[HTML] : undefined;
728 + if (nextHtml != null) {
729 + if (lastHtml !== nextHtml) {
730 + (updatePayload = updatePayload || []).push(propKey, '' + nextHtml);
731 + }
732 + } else {
733 + // TODO: It might be too late to clear this if we have children
734 + // inserted already.
735 + }
736 + } else if (propKey === CHILDREN) {
737 + if (lastProp !== nextProp && (
738 + typeof nextProp === 'string' || typeof nextProp === 'number'
739 + )) {
740 + (updatePayload = updatePayload || []).push(propKey, '' + nextProp);
741 + }
742 + } else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING) {
743 + // Noop
744 + } else if (registrationNameModules.hasOwnProperty(propKey)) {
745 + if (nextProp) {
746 + // We eagerly listen to this even though we haven't committed yet.
747 + ensureListeningTo(rootContainerElement, propKey);
748 + }
749 + if (!updatePayload && lastProp !== nextProp) {
750 + // This is a special case. If any listener updates we need to ensure
751 + // that the "current" props pointer gets updated so we need a commit
752 + // to update this element.
753 + updatePayload = [];
754 + }
755 + } else {
756 + // For any other property we always add it to the queue and then we
757 + // filter it out using the whitelist during the commit.
758 + (updatePayload = updatePayload || []).push(propKey, nextProp);
759 + }
760 + }
761 + if (styleUpdates) {
762 + (updatePayload = updatePayload || []).push(STYLE, styleUpdates);
763 + }
764 + return updatePayload;
765 + },
766 +
767 + // Apply the diff.
768 + updateProperties(
769 + domElement : Element,
770 + updatePayload : Array<any>,
771 + tag : string,
772 + lastRawProps : Object,
773 + nextRawProps : Object
774 + ) : void {
775 + var wasCustomComponentTag = isCustomComponent(tag, lastRawProps);
776 + var isCustomComponentTag = isCustomComponent(tag, nextRawProps);
777 + // Apply the diff.
778 updateDOMProperties(
779 domElement,
675 - rootContainerElement,
676 - lastProps,
677 - nextProps,
780 + updatePayload,
781 wasCustomComponentTag,
782 isCustomComponentTag
783 );
784
785 + // TODO: Ensure that an update gets scheduled if any of the special props
786 + // changed.
787 switch (tag) {
788 case 'input':
789 // Update the wrapper around inputs *after* updating props. This has to
src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
+83
@@ -994,6 +994,89 @@ describe('ReactDOMFiber', () => {
994 ]);
995 });
996
997 + it('should not update event handlers until commit', () => {
998 + let ops = [];
999 + const handlerA = () => ops.push('A');
1000 + const handlerB = () => ops.push('B');
1001 +
1002 + class Example extends React.Component {
1003 + state = { flip: false, count: 0 };
1004 + flip() {
1005 + this.setState({ flip: true, count: this.state.count + 1 });
1006 + }
1007 + tick() {
1008 + this.setState({ count: this.state.count + 1 });
1009 + }
1010 + render() {
1011 + const useB = !this.props.forceA && this.state.flip;
1012 + return (
1013 + <div onClick={useB ? handlerB : handlerA} />
1014 + );
1015 + }
1016 + }
1017 +
1018 + class Click extends React.Component {
1019 + constructor() {
1020 + super();
1021 + click(node);
1022 + }
1023 + render() {
1024 + return null;
1025 + }
1026 + }
1027 +
1028 + let inst;
1029 + ReactDOM.render([<Example ref={n => inst = n} />], container);
1030 + const node = container.firstChild;
1031 + expect(node.tagName).toEqual('DIV');
1032 +
1033 + function click(target) {
1034 + var fakeNativeEvent = {};
1035 + ReactTestUtils.simulateNativeEventOnNode(
1036 + 'topClick',
1037 + target,
1038 + fakeNativeEvent
1039 + );
1040 + }
1041 +
1042 + click(node);
1043 +
1044 + expect(ops).toEqual(['A']);
1045 + ops = [];
1046 +
1047 + // Render with the other event handler.
1048 + inst.flip();
1049 +
1050 + click(node);
1051 +
1052 + expect(ops).toEqual(['B']);
1053 + ops = [];
1054 +
1055 + // Rerender without changing any props.
1056 + inst.tick();
1057 +
1058 + click(node);
1059 +
1060 + expect(ops).toEqual(['B']);
1061 + ops = [];
1062 +
1063 + // Render a flip back to the A handler. The second component invokes the
1064 + // click handler during render to simulate a click during an aborted
1065 + // render. I use this hack because at current time we don't have a way to
1066 + // test aborted ReactDOM renders.
1067 + ReactDOM.render([<Example forceA={true} />, <Click />], container);
1068 +
1069 + // Because the new click handler has not yet committed, we should still
1070 + // invoke B.
1071 + expect(ops).toEqual(['B']);
1072 + ops = [];
1073 +
1074 + // Any click that happens after commit, should invoke A.
1075 + click(node);
1076 + expect(ops).toEqual(['A']);
1077 +
1078 + });
1079 +
1080 it('should not crash encountering low-priority tree', () => {
1081 ReactDOM.render(
1082 <div hidden={true}>
src/renderers/dom/shared/ReactDOMComponentTree.js
+15 -2
@@ -20,8 +20,11 @@ var invariant = require('invariant');
20 var ATTR_NAME = DOMProperty.ID_ATTRIBUTE_NAME;
21 var Flags = ReactDOMComponentFlags;
22
23 -var internalInstanceKey =
24 - '__reactInternalInstance$' + Math.random().toString(36).slice(2);
23 +var randomKey = Math.random().toString(36).slice(2);
24 +
25 +var internalInstanceKey = '__reactInternalInstance$' + randomKey;
26 +
27 +var internalEventHandlersKey = '__reactEventHandlers$' + randomKey;
28
29 /**
30 * Check if a given node should be cached.
@@ -218,6 +221,14 @@ function getNodeFromInstance(inst) {
221 return inst._hostNode;
222 }
223
224 +function getFiberEventHandlersFromNode(node) {
225 + return node[internalEventHandlersKey] || null;
226 +}
227 +
228 +function updateFiberEventHandlers(node, props) {
229 + node[internalEventHandlersKey] = props;
230 +}
231 +
232 var ReactDOMComponentTree = {
233 getClosestInstanceFromNode: getClosestInstanceFromNode,
234 getInstanceFromNode: getInstanceFromNode,
@@ -226,6 +237,8 @@ var ReactDOMComponentTree = {
237 precacheNode: precacheNode,
238 uncacheNode: uncacheNode,
239 precacheFiberNode: precacheFiberNode,
240 + getFiberEventHandlersFromNode,
241 + updateFiberEventHandlers,
242 };
243
244 module.exports = ReactDOMComponentTree;
src/renderers/native/ReactNativeComponentTree.js
+11
@@ -14,6 +14,7 @@
14 var invariant = require('invariant');
15
16 var instanceCache = {};
17 +var instanceProps = {};
18
19 /**
20 * Drill down (through composites and empty components) until we get a host or
@@ -65,6 +66,14 @@ function getTagFromInstance(inst) {
66 return tag;
67 }
68
69 +function getFiberEventHandlersFromTag(tag) {
70 + return instanceProps[tag] || null;
71 +}
72 +
73 +function updateFiberEventHandlers(tag, props) {
74 + instanceProps[tag] = props;
75 +}
76 +
77 var ReactNativeComponentTree = {
78 getClosestInstanceFromNode: getInstanceFromTag,
79 getInstanceFromNode: getInstanceFromTag,
@@ -73,6 +82,8 @@ var ReactNativeComponentTree = {
82 precacheNode,
83 uncacheFiberNode,
84 uncacheNode,
85 + getFiberEventHandlersFromNode: getFiberEventHandlersFromTag,
86 + updateFiberEventHandlers,
87 };
88
89 module.exports = ReactNativeComponentTree;
src/renderers/native/ReactNativeFiber.js
+17 -11
@@ -33,7 +33,11 @@ const emptyObject = require('emptyObject');
33 const findNodeHandle = require('findNodeHandle');
34 const invariant = require('invariant');
35
36 -const { precacheFiberNode, uncacheFiberNode } = ReactNativeComponentTree;
36 +const {
37 + precacheFiberNode,
38 + uncacheFiberNode,
39 + updateFiberEventHandlers,
40 +} = ReactNativeComponentTree;
41
42 ReactNativeInjection.inject();
43
@@ -114,7 +118,6 @@ const NativeRenderer = ReactFiberReconciler({
118 instance : Instance,
119 type : string,
120 newProps : Props,
117 - rootContainerInstance : Object,
121 internalInstanceHandle : Object
122 ) : void {
123 // Noop
@@ -122,15 +125,15 @@ const NativeRenderer = ReactFiberReconciler({
125
126 commitUpdate(
127 instance : Instance,
128 + updatePayloadTODO : Object,
129 type : string,
130 oldProps : Props,
131 newProps : Props,
128 - rootContainerInstance : Object,
132 internalInstanceHandle : Object
133 ) : void {
134 const viewConfig = instance.viewConfig;
135
133 - precacheFiberNode(internalInstanceHandle, instance._nativeTag);
136 + updateFiberEventHandlers(instance._nativeTag, newProps);
137
138 const updatePayload = ReactNativeAttributePayload.diff(
139 oldProps,
@@ -149,7 +152,7 @@ const NativeRenderer = ReactFiberReconciler({
152 type : string,
153 props : Props,
154 rootContainerInstance : Container,
152 - hostContext : Object,
155 + hostContext : {||},
156 internalInstanceHandle : Object
157 ) : Instance {
158 const tag = ReactNativeTagHandles.allocateTag();
@@ -178,6 +181,7 @@ const NativeRenderer = ReactFiberReconciler({
181 const component = new NativeHostComponent(tag, viewConfig);
182
183 precacheFiberNode(internalInstanceHandle, tag);
184 + updateFiberEventHandlers(tag, props);
185
186 return component;
187 },
@@ -185,7 +189,7 @@ const NativeRenderer = ReactFiberReconciler({
189 createTextInstance(
190 text : string,
191 rootContainerInstance : Container,
188 - hostContext : Object,
192 + hostContext : {||},
193 internalInstanceHandle : Object,
194 ) : TextInstance {
195 const tag = ReactNativeTagHandles.allocateTag();
@@ -224,11 +228,11 @@ const NativeRenderer = ReactFiberReconciler({
228 return false;
229 },
230
227 - getRootHostContext() {
231 + getRootHostContext() : {||} {
232 return emptyObject;
233 },
234
231 - getChildHostContext() {
235 + getChildHostContext() : {||} {
236 return emptyObject;
237 },
238
@@ -290,9 +294,11 @@ const NativeRenderer = ReactFiberReconciler({
294 instance : Instance,
295 type : string,
296 oldProps : Props,
293 - newProps : Props
294 - ) : boolean {
295 - return true;
297 + newProps : Props,
298 + rootContainerInstance : Container,
299 + hostContext : {||}
300 + ) : null | Object {
301 + return emptyObject;
302 },
303
304 removeChild(
src/renderers/noop/ReactNoop.js
+5 -3
@@ -29,6 +29,8 @@ var {
29 } = require('ReactPriorityLevel');
30 var emptyObject = require('emptyObject');
31
32 +const UPDATE_SIGNAL = {};
33 +
34 var scheduledAnimationCallback = null;
35 var scheduledDeferredCallback = null;
36
@@ -78,15 +80,15 @@ var NoopRenderer = ReactFiberReconciler({
80 return false;
81 },
82
81 - prepareUpdate(instance : Instance, type : string, oldProps : Props, newProps : Props) : boolean {
82 - return true;
83 + prepareUpdate(instance : Instance, type : string, oldProps : Props, newProps : Props) : null | {} {
84 + return UPDATE_SIGNAL;
85 },
86
87 commitMount(instance : Instance, type : string, newProps : Props) : void {
88 // Noop
89 },
90
89 - commitUpdate(instance : Instance, type : string, oldProps : Props, newProps : Props) : void {
91 + commitUpdate(instance : Instance, updatePayload : Object, type : string, oldProps : Props, newProps : Props) : void {
92 instance.prop = newProps.prop;
93 },
94
src/renderers/shared/fiber/ReactFiberBeginWork.js
+2 -2
@@ -70,8 +70,8 @@ if (__DEV__) {
70 var warnedAboutStatelessRefs = {};
71 }
72
73 -module.exports = function<T, P, I, TI, PI, C, CX>(
74 - config : HostConfig<T, P, I, TI, PI, C, CX>,
73 +module.exports = function<T, P, I, TI, PI, C, CX, PL>(
74 + config : HostConfig<T, P, I, TI, PI, C, CX, PL>,
75 hostContext : HostContext<C, CX>,
76 scheduleUpdate : (fiber : Fiber, priorityLevel : PriorityLevel) => void,
77 getPriorityContext : () => PriorityLevel,
src/renderers/shared/fiber/ReactFiberCommitWork.js
+9 -12
@@ -13,7 +13,6 @@
13 'use strict';
14
15 import type { Fiber } from 'ReactFiber';
16 -import type { HostContext } from 'ReactFiberHostContext';
16 import type { HostConfig } from 'ReactFiberReconciler';
17
18 var ReactTypeOfWork = require('ReactTypeOfWork');
@@ -34,9 +33,8 @@ var {
33 ContentReset,
34 } = require('ReactTypeOfSideEffect');
35
37 -module.exports = function<T, P, I, TI, PI, C, CX>(
38 - config : HostConfig<T, P, I, TI, PI, C, CX>,
39 - hostContext : HostContext<C, CX>,
36 +module.exports = function<T, P, I, TI, PI, C, CX, PL>(
37 + config : HostConfig<T, P, I, TI, PI, C, CX, PL>,
38 captureError : (failedFiber : Fiber, error: Error) => ?Fiber
39 ) {
40
@@ -51,10 +49,6 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
49 getPublicInstance,
50 } = config;
51
54 - const {
55 - getRootHostContainer,
56 - } = hostContext;
57 -
52 // Capture errors so they don't interrupt unmounting.
53 function safelyCallComponentWillUnmount(current, instance) {
54 try {
@@ -367,9 +361,13 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
361 // Commit the work prepared earlier.
362 const newProps = finishedWork.memoizedProps;
363 const oldProps = current.memoizedProps;
370 - const rootContainerInstance = getRootHostContainer();
364 const type = finishedWork.type;
372 - commitUpdate(instance, type, oldProps, newProps, rootContainerInstance, finishedWork);
365 + // TODO: Type the updateQueue to be specific to host components.
366 + const updatePayload : null | PL = (finishedWork.updateQueue : any);
367 + finishedWork.updateQueue = null;
368 + if (updatePayload) {
369 + commitUpdate(instance, updatePayload, type, oldProps, newProps, finishedWork);
370 + }
371 }
372 detachRefIfNeeded(current, finishedWork);
373 return;
@@ -438,8 +436,7 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
436 ) {
437 const type = finishedWork.type;
438 const props = finishedWork.memoizedProps;
441 - const rootContainerInstance = getRootHostContainer();
442 - commitMount(instance, type, props, rootContainerInstance, finishedWork);
439 + commitMount(instance, type, props, finishedWork);
440 }
441
442 return;
src/renderers/shared/fiber/ReactFiberCompleteWork.js
+13 -7
@@ -47,8 +47,8 @@ if (__DEV__) {
47 var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber');
48 }
49
50 -module.exports = function<T, P, I, TI, PI, C, CX>(
51 - config : HostConfig<T, P, I, TI, PI, C, CX>,
50 +module.exports = function<T, P, I, TI, PI, C, CX, PL>(
51 + config : HostConfig<T, P, I, TI, PI, C, CX, PL>,
52 hostContext : HostContext<C, CX>,
53 ) {
54 const {
@@ -186,8 +186,9 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
186 }
187 return null;
188 }
189 - case HostComponent:
189 + case HostComponent: {
190 popHostContext(workInProgress);
191 + const rootContainerInstance = getRootHostContainer();
192 const type = workInProgress.type;
193 const newProps = workInProgress.memoizedProps;
194 if (current && workInProgress.stateNode != null) {
@@ -200,8 +201,12 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
201 // Even better would be if children weren't special cased at all tho.
202 const instance : I = workInProgress.stateNode;
203 const currentHostContext = getHostContext();
203 - if (prepareUpdate(instance, type, oldProps, newProps, currentHostContext)) {
204 - // This returns true if there was something to update.
204 + const updatePayload = prepareUpdate(instance, type, oldProps, newProps, rootContainerInstance, currentHostContext);
205 + // TODO: Type this specific to this type of component.
206 + workInProgress.updateQueue = (updatePayload : any);
207 + // If the update payload indicates that there is a change or if there
208 + // is a new ref we mark this as an update.
209 + if (updatePayload || current.ref !== workInProgress.ref) {
210 markUpdate(workInProgress);
211 }
212 } else {
@@ -214,7 +219,6 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
219 }
220 }
221
217 - const rootContainerInstance = getRootHostContainer();
222 const currentHostContext = getHostContext();
223 // TODO: Move createInstance to beginWork and keep it on a context
224 // "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>(
248 }
249 }
250 return null;
247 - case HostText:
251 + }
252 + case HostText: {
253 let newText = workInProgress.memoizedProps;
254 if (current && workInProgress.stateNode != null) {
255 const oldText = current.memoizedProps;
@@ -268,6 +273,7 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
273 workInProgress.stateNode = textInstance;
274 }
275 return null;
276 + }
277 case CoroutineComponent:
278 return moveCoroutineToHandlerPhase(current, workInProgress);
279 case CoroutineHandlerPhase:
src/renderers/shared/fiber/ReactFiberHostContext.js
+2 -2
@@ -34,8 +34,8 @@ export type HostContext<C, CX> = {
34 resetHostContainer() : void,
35 };
36
37 -module.exports = function<T, P, I, TI, PI, C, CX>(
38 - config : HostConfig<T, P, I, TI, PI, C, CX>
37 +module.exports = function<T, P, I, TI, PI, C, CX, PL>(
38 + config : HostConfig<T, P, I, TI, PI, C, CX, PL>
39 ) : HostContext<C, CX> {
40 const {
41 getChildHostContext,
src/renderers/shared/fiber/ReactFiberReconciler.js
+5 -5
@@ -43,7 +43,7 @@ export type Deadline = {
43
44 type OpaqueNode = Fiber;
45
46 -export type HostConfig<T, P, I, TI, PI, C, CX> = {
46 +export type HostConfig<T, P, I, TI, PI, C, CX, PL> = {
47
48 getRootHostContext(rootContainerInstance : C) : CX,
49 getChildHostContext(parentHostContext : CX, type : T) : CX,
@@ -53,9 +53,9 @@ export type HostConfig<T, P, I, TI, PI, C, CX> = {
53 appendInitialChild(parentInstance : I, child : I | TI) : void,
54 finalizeInitialChildren(parentInstance : I, type : T, props : P, rootContainerInstance : C) : boolean,
55
56 - prepareUpdate(instance : I, type : T, oldProps : P, newProps : P, hostContext : CX) : boolean,
57 - commitUpdate(instance : I, type : T, oldProps : P, newProps : P, rootContainerInstance : C, internalInstanceHandle : OpaqueNode) : void,
58 - commitMount(instance : I, type : T, newProps : P, rootContainerInstance : C, internalInstanceHandle : OpaqueNode) : void,
56 + prepareUpdate(instance : I, type : T, oldProps : P, newProps : P, rootContainerInstance : C, hostContext : CX) : null | PL,
57 + commitUpdate(instance : I, updatePayload : PL, type : T, oldProps : P, newProps : P, internalInstanceHandle : OpaqueNode) : void,
58 + commitMount(instance : I, type : T, newProps : P, internalInstanceHandle : OpaqueNode) : void,
59
60 shouldSetTextContent(props : P) : boolean,
61 resetTextContent(instance : I) : void,
@@ -102,7 +102,7 @@ getContextForSubtree._injectFiber(function(fiber : Fiber) {
102 parentContext;
103 });
104
105 -module.exports = function<T, P, I, TI, PI, C, CX>(config : HostConfig<T, P, I, TI, PI, C, CX>) : Reconciler<C, I, TI> {
105 +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> {
106
107 var {
108 scheduleUpdate,
src/renderers/shared/fiber/ReactFiberScheduler.js
+2 -2
@@ -88,7 +88,7 @@ if (__DEV__) {
88
89 var timeHeuristicForUnitOfWork = 1;
90
91 -module.exports = function<T, P, I, TI, PI, C, CX>(config : HostConfig<T, P, I, TI, PI, C, CX>) {
91 +module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P, I, TI, PI, C, CX, PL>) {
92 const hostContext = ReactFiberHostContext(config);
93 const { popHostContainer, popHostContext, resetHostContainer } = hostContext;
94 const { beginWork, beginFailedWork } = ReactFiberBeginWork(
@@ -104,7 +104,7 @@ module.exports = function<T, P, I, TI, PI, C, CX>(config : HostConfig<T, P, I, T
104 commitWork,
105 commitLifeCycles,
106 commitRef,
107 - } = ReactFiberCommitWork(config, hostContext, captureError);
107 + } = ReactFiberCommitWork(config, captureError);
108 const {
109 scheduleAnimationCallback: hostScheduleAnimationCallback,
110 scheduleDeferredCallback: hostScheduleDeferredCallback,
src/renderers/shared/shared/ReactTreeTraversal.js
+1 -7
@@ -12,7 +12,6 @@
12 'use strict';
13
14 var { HostComponent } = require('ReactTypeOfWork');
15 -var { getNodeFromInstance, getInstanceFromNode } = require('EventPluginUtils');
15
16 function getParent(inst) {
17 if (inst._hostParent !== undefined) {
@@ -27,13 +26,8 @@ function getParent(inst) {
26 // host node but that wouldn't work for React Native and doesn't let us
27 // do the portal feature.
28 } while (inst && inst.tag !== HostComponent);
30 - // Going through the Host Node will guarantee that we get the "current"
31 - // Fiber, instead of the alternate because that pointer is updated when
32 - // props update.
33 - // TODO: This is a bit hacky and possibly slow. We should ideally have
34 - // something in the reconciler that allow us to do this safely.
29 if (inst) {
36 - return getInstanceFromNode(getNodeFromInstance(inst));
30 + return inst;
31 }
32 }
33 return null;
src/renderers/shared/shared/event/EventPluginHub.js
+7 -1
@@ -126,7 +126,13 @@ var EventPluginHub = {
126 // TODO: shouldPreventMouseEvent is DOM-specific and definitely should not
127 // live here; needs to be moved to a better place soon
128 if (typeof inst.tag === 'number') {
129 - const props = inst.memoizedProps;
129 + const props = EventPluginUtils.getFiberEventHandlersFromNode(
130 + inst.stateNode
131 + );
132 + if (!props) {
133 + // Work in progress.
134 + return null;
135 + }
136 listener = props[registrationName];
137 if (shouldPreventMouseEvent(registrationName, inst.type, props)) {
138 return null;
src/renderers/shared/shared/event/EventPluginUtils.js
+3
@@ -219,6 +219,9 @@ var EventPluginUtils = {
219 executeDispatchesInOrderStopAtTrue: executeDispatchesInOrderStopAtTrue,
220 hasDispatches: hasDispatches,
221
222 + getFiberEventHandlersFromNode: function(node) {
223 + return ComponentTree.getFiberEventHandlersFromNode(node);
224 + },
225 getInstanceFromNode: function(node) {
226 return ComponentTree.getInstanceFromNode(node);
227 },
src/renderers/testing/ReactTestRendererFiber.js
+6 -4
@@ -47,6 +47,8 @@ type TextInstance = {|
47 tag : 'TEXT',
48 |};
49
50 +const UPDATE_SIGNAL = {};
51 +
52 var TestRenderer = ReactFiberReconciler({
53 getRootHostContext() {
54 return emptyObject;
@@ -102,17 +104,18 @@ var TestRenderer = ReactFiberReconciler({
104 type : string,
105 oldProps : Props,
106 newProps : Props,
107 + rootContainerInstance : Container,
108 hostContext : Object,
106 - ) : boolean {
107 - return true;
109 + ) : null | {} {
110 + return UPDATE_SIGNAL;
111 },
112
113 commitUpdate(
114 instance : Instance,
115 + updatePayload : {},
116 type : string,
117 oldProps : Props,
118 newProps : Props,
115 - rootContainerInstance : Container,
119 internalInstanceHandle : Object,
120 ) : void {
121 instance.type = type;
@@ -123,7 +126,6 @@ var TestRenderer = ReactFiberReconciler({
126 instance : Instance,
127 type : string,
128 newProps : Props,
126 - rootContainerInstance : Object,
129 internalInstanceHandle : Object
130 ) : void {
131 // noop