diff --git a/src/browser/ReactEventTopLevelCallback.js b/src/browser/ReactEventTopLevelCallback.js index 5d202ccb3a5..821bf24d491 100644 --- a/src/browser/ReactEventTopLevelCallback.js +++ b/src/browser/ReactEventTopLevelCallback.js @@ -19,11 +19,13 @@ "use strict"; +var PooledClass = require('PooledClass'); var ReactEventEmitter = require('ReactEventEmitter'); var ReactInstanceHandles = require('ReactInstanceHandles'); var ReactMount = require('ReactMount'); var getEventTarget = require('getEventTarget'); +var mixInto = require('mixInto'); /** * @type {boolean} @@ -49,6 +51,53 @@ function findParent(node) { return parent; } +/** + * Calls ReactEventEmitter.handleTopLevel for each node stored in bookKeeping's + * ancestor list. Separated from createTopLevelCallback to avoid try/finally + * deoptimization. + * + * @param {string} topLevelType + * @param {DOMEvent} nativeEvent + * @param {TopLevelCallbackBookKeeping} bookKeeping + */ +function handleTopLevelImpl(topLevelType, nativeEvent, bookKeeping) { + var topLevelTarget = ReactMount.getFirstReactDOM( + getEventTarget(nativeEvent) + ) || window; + + // Loop through the hierarchy, in case there's any nested components. + // It's important that we build the array of ancestors before calling any + // event handlers, because event handlers can modify the DOM, leading to + // inconsistencies with ReactMount's node cache. See #1105. + var ancestor = topLevelTarget; + while (ancestor) { + bookKeeping.ancestors.push(ancestor); + ancestor = findParent(ancestor); + } + + for (var i = 0, l = bookKeeping.ancestors.length; i < l; i++) { + topLevelTarget = bookKeeping.ancestors[i]; + var topLevelTargetID = ReactMount.getID(topLevelTarget) || ''; + ReactEventEmitter.handleTopLevel( + topLevelType, + topLevelTarget, + topLevelTargetID, + nativeEvent + ); + } +} + +// Used to store ancestor hierarchy in top level callback +function TopLevelCallbackBookKeeping() { + this.ancestors = []; +} +mixInto(TopLevelCallbackBookKeeping, { + destructor: function() { + this.ancestors.length = 0; + } +}); +PooledClass.addPoolingTo(TopLevelCallbackBookKeeping); + /** * Top-level callback creator used to implement event handling using delegation. * This is used via dependency injection. @@ -85,21 +134,12 @@ var ReactEventTopLevelCallback = { if (!_topLevelListenersEnabled) { return; } - var topLevelTarget = ReactMount.getFirstReactDOM( - getEventTarget(nativeEvent) - ) || window; - - // Loop through the hierarchy, in case there's any nested components. - while (topLevelTarget) { - var topLevelTargetID = ReactMount.getID(topLevelTarget) || ''; - ReactEventEmitter.handleTopLevel( - topLevelType, - topLevelTarget, - topLevelTargetID, - nativeEvent - ); - - topLevelTarget = findParent(topLevelTarget); + + var bookKeeping = TopLevelCallbackBookKeeping.getPooled(); + try { + handleTopLevelImpl(topLevelType, nativeEvent, bookKeeping); + } finally { + TopLevelCallbackBookKeeping.release(bookKeeping); } }; } diff --git a/src/browser/__tests__/ReactEventTopLevelCallback-test.js b/src/browser/__tests__/ReactEventTopLevelCallback-test.js index 6782d006c2b..6b7dc0cde0f 100644 --- a/src/browser/__tests__/ReactEventTopLevelCallback-test.js +++ b/src/browser/__tests__/ReactEventTopLevelCallback-test.js @@ -84,6 +84,39 @@ describe('ReactEventTopLevelCallback', function() { expect(calls[2][EVENT_TARGET_PARAM]) .toBe(grandParentControl.getDOMNode()); }); + + it('should not get confused by disappearing elements', function() { + var childContainer = document.createElement('div'); + var childControl =
Child
; + var parentContainer = document.createElement('div'); + var parentControl =
Parent
; + ReactMount.renderComponent(childControl, childContainer); + ReactMount.renderComponent(parentControl, parentContainer); + parentControl.getDOMNode().appendChild(childContainer); + + // ReactEventEmitter.handleTopLevel might remove the target from the DOM. + // Here, we have handleTopLevel remove the node when the first event + // handlers are called; we'll still expect to receive a second call for + // the parent control. + var childNode = childControl.getDOMNode(); + ReactEventEmitter.handleTopLevel.mockImplementation( + function(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) { + if (topLevelTarget === childNode) { + ReactMount.unmountComponentAtNode(childContainer); + } + } + ); + + var callback = ReactEventTopLevelCallback.createTopLevelCallback('test'); + callback({ + target: childNode + }); + + var calls = ReactEventEmitter.handleTopLevel.mock.calls; + expect(calls.length).toBe(2); + expect(calls[0][EVENT_TARGET_PARAM]).toBe(childNode); + expect(calls[1][EVENT_TARGET_PARAM]).toBe(parentControl.getDOMNode()); + }); }); it('should not fire duplicate events for a React DOM tree', function() {