@samitouri / QOS-React-2 / commits / 933dde9c6d

Batch updates caused by handlers in multiple roots

Fixes #1227. It seems rare that event handlers in two roots nested in the DOM will update the same component in the same tick, but if that happens, the updates should be batched together.

Ben Alpert committed Mar 7, 2014 at 01:33 UTC 933dde9c6d788519785b47e9dad9c9330504d69c
3 files changed +59 -13
src/browser/ui/ReactEventTopLevelCallback.js
+21 -10
@@ -23,6 +23,7 @@ var PooledClass = require('PooledClass');
23 var ReactEventEmitter = require('ReactEventEmitter');
24 var ReactInstanceHandles = require('ReactInstanceHandles');
25 var ReactMount = require('ReactMount');
26 +var ReactUpdates = require('ReactUpdates');
27
28 var getEventTarget = require('getEventTarget');
29 var mixInto = require('mixInto');
@@ -56,13 +57,11 @@ function findParent(node) {
57 * ancestor list. Separated from createTopLevelCallback to avoid try/finally
58 * deoptimization.
59 *
59 - * @param {string} topLevelType
60 - * @param {DOMEvent} nativeEvent
60 * @param {TopLevelCallbackBookKeeping} bookKeeping
61 */
63 -function handleTopLevelImpl(topLevelType, nativeEvent, bookKeeping) {
62 +function handleTopLevelImpl(bookKeeping) {
63 var topLevelTarget = ReactMount.getFirstReactDOM(
65 - getEventTarget(nativeEvent)
64 + getEventTarget(bookKeeping.nativeEvent)
65 ) || window;
66
67 // Loop through the hierarchy, in case there's any nested components.
@@ -79,24 +78,31 @@ function handleTopLevelImpl(topLevelType, nativeEvent, bookKeeping) {
78 topLevelTarget = bookKeeping.ancestors[i];
79 var topLevelTargetID = ReactMount.getID(topLevelTarget) || '';
80 ReactEventEmitter.handleTopLevel(
82 - topLevelType,
81 + bookKeeping.topLevelType,
82 topLevelTarget,
83 topLevelTargetID,
85 - nativeEvent
84 + bookKeeping.nativeEvent
85 );
86 }
87 }
88
89 // Used to store ancestor hierarchy in top level callback
91 -function TopLevelCallbackBookKeeping() {
90 +function TopLevelCallbackBookKeeping(topLevelType, nativeEvent) {
91 + this.topLevelType = topLevelType;
92 + this.nativeEvent = nativeEvent;
93 this.ancestors = [];
94 }
95 mixInto(TopLevelCallbackBookKeeping, {
96 destructor: function() {
97 + this.topLevelType = null;
98 + this.nativeEvent = null;
99 this.ancestors.length = 0;
100 }
101 });
99 -PooledClass.addPoolingTo(TopLevelCallbackBookKeeping);
102 +PooledClass.addPoolingTo(
103 + TopLevelCallbackBookKeeping,
104 + PooledClass.twoArgumentPooler
105 +);
106
107 /**
108 * Top-level callback creator used to implement event handling using delegation.
@@ -135,9 +141,14 @@ var ReactEventTopLevelCallback = {
141 return;
142 }
143
138 - var bookKeeping = TopLevelCallbackBookKeeping.getPooled();
144 + var bookKeeping = TopLevelCallbackBookKeeping.getPooled(
145 + topLevelType,
146 + nativeEvent
147 + );
148 try {
140 - handleTopLevelImpl(topLevelType, nativeEvent, bookKeeping);
149 + // Event queue being processed in the same cycle allows
150 + // `preventDefault`.
151 + ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
152 } finally {
153 TopLevelCallbackBookKeeping.release(bookKeeping);
154 }
src/browser/ui/__tests__/ReactEventTopLevelCallback-test.js
+37
@@ -117,6 +117,43 @@ describe('ReactEventTopLevelCallback', function() {
117 expect(calls[0][EVENT_TARGET_PARAM]).toBe(childNode);
118 expect(calls[1][EVENT_TARGET_PARAM]).toBe(parentControl.getDOMNode());
119 });
120 +
121 + it('should batch between handlers from different roots', function() {
122 + var childContainer = document.createElement('div');
123 + var parentContainer = document.createElement('div');
124 + var childControl = ReactMount.renderComponent(
125 + <div>Child</div>,
126 + childContainer
127 + );
128 + var parentControl = ReactMount.renderComponent(
129 + <div>Parent</div>,
130 + parentContainer
131 + );
132 + parentControl.getDOMNode().appendChild(childContainer);
133 +
134 + // Suppose an event handler in each root enqueues an update to the
135 + // childControl element -- the two updates should get batched together.
136 + var childNode = childControl.getDOMNode();
137 + ReactEventEmitter.handleTopLevel.mockImplementation(
138 + function(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) {
139 + ReactMount.renderComponent(
140 + <div>{topLevelTarget === childNode ? '1' : '2'}</div>,
141 + childContainer
142 + );
143 + // Since we're batching, neither update should yet have gone through.
144 + expect(childNode.textContent).toBe('Child');
145 + }
146 + );
147 +
148 + var callback = ReactEventTopLevelCallback.createTopLevelCallback('test');
149 + callback({
150 + target: childNode
151 + });
152 +
153 + var calls = ReactEventEmitter.handleTopLevel.mock.calls;
154 + expect(calls.length).toBe(2);
155 + expect(childNode.textContent).toBe('2');
156 + });
157 });
158
159 it('should not fire duplicate events for a React DOM tree', function() {
src/core/ReactEventEmitterMixin.js
+1 -3
@@ -19,7 +19,6 @@
19 "use strict";
20
21 var EventPluginHub = require('EventPluginHub');
22 -var ReactUpdates = require('ReactUpdates');
22
23 function runEventQueueInBatch(events) {
24 EventPluginHub.enqueueEvents(events);
@@ -49,8 +48,7 @@ var ReactEventEmitterMixin = {
48 nativeEvent
49 );
50
52 - // Event queue being processed in the same cycle allows `preventDefault`.
53 - ReactUpdates.batchedUpdates(runEventQueueInBatch, events);
51 + runEventQueueInBatch(events);
52 }
53 };
54