-
Notifications
You must be signed in to change notification settings - Fork 48.4k
7128 - React document listener memory leak fix. #7209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -189,6 +189,15 @@ var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, { | |
ReactEventListener.setHandleTopLevel( | ||
ReactBrowserEventEmitter.handleTopLevel | ||
); | ||
ReactEventListener.setDocumentResetCallback(function(doc) { | ||
// This must be called during a document reset (@see ReactEventListener.resetDocument) | ||
// after unmounting the last component, otherwise the next render will be visible, | ||
// but document level event listeners will be missing, and input (clicks, typing etc) | ||
// wont work. | ||
reactTopListenersCounter = 0; | ||
alreadyListeningTo = {}; | ||
delete doc[topListenersIDKey]; | ||
}); | ||
ReactBrowserEventEmitter.ReactEventListener = ReactEventListener; | ||
}, | ||
}, | ||
|
@@ -234,13 +243,37 @@ var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, { | |
* | ||
* @param {string} registrationName Name of listener (e.g. `onClick`). | ||
* @param {object} contentDocumentHandle Document which owns the container | ||
* @return {function} A function that removes ALL listeners that were added. | ||
*/ | ||
listenTo: function(registrationName, contentDocumentHandle) { | ||
var mountAt = contentDocumentHandle; | ||
var isListening = getListeningForDocument(mountAt); | ||
var dependencies = | ||
EventPluginRegistry.registrationNameDependencies[registrationName]; | ||
|
||
// aggregates handler remove functions... | ||
var listenerRemovers = []; | ||
var trapBubbledEvent = function(topLevelType, handlerBaseName) { | ||
var remover = ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent( | ||
topLevelType, | ||
handlerBaseName, | ||
mountAt | ||
); | ||
if (remover && remover.remove) { | ||
listenerRemovers.push(remover.remove); | ||
} | ||
}; | ||
var trapCapturedEvent = function(topLevelType, handlerBaseName, handle) { | ||
var remover = ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent( | ||
topLevelType, | ||
handlerBaseName, | ||
handle | ||
); | ||
if (remover && remover.remove) { | ||
listenerRemovers.push(remover.remove); | ||
} | ||
}; | ||
|
||
var topLevelTypes = EventConstants.topLevelTypes; | ||
for (var i = 0; i < dependencies.length; i++) { | ||
var dependency = dependencies[i]; | ||
|
@@ -250,21 +283,21 @@ var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, { | |
)) { | ||
if (dependency === topLevelTypes.topWheel) { | ||
if (isEventSupported('wheel')) { | ||
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent( | ||
trapBubbledEvent( | ||
topLevelTypes.topWheel, | ||
'wheel', | ||
mountAt | ||
); | ||
} else if (isEventSupported('mousewheel')) { | ||
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent( | ||
trapBubbledEvent( | ||
topLevelTypes.topWheel, | ||
'mousewheel', | ||
mountAt | ||
); | ||
} else { | ||
// Firefox needs to capture a different mouse scroll event. | ||
// @see http://www.quirksmode.org/dom/events/tests/scroll.html | ||
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent( | ||
trapBubbledEvent( | ||
topLevelTypes.topWheel, | ||
'DOMMouseScroll', | ||
mountAt | ||
|
@@ -273,13 +306,13 @@ var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, { | |
} else if (dependency === topLevelTypes.topScroll) { | ||
|
||
if (isEventSupported('scroll', true)) { | ||
ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent( | ||
trapCapturedEvent( | ||
topLevelTypes.topScroll, | ||
'scroll', | ||
mountAt | ||
); | ||
} else { | ||
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent( | ||
trapBubbledEvent( | ||
topLevelTypes.topScroll, | ||
'scroll', | ||
ReactBrowserEventEmitter.ReactEventListener.WINDOW_HANDLE | ||
|
@@ -289,25 +322,25 @@ var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, { | |
dependency === topLevelTypes.topBlur) { | ||
|
||
if (isEventSupported('focus', true)) { | ||
ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent( | ||
trapCapturedEvent( | ||
topLevelTypes.topFocus, | ||
'focus', | ||
mountAt | ||
); | ||
ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent( | ||
trapCapturedEvent( | ||
topLevelTypes.topBlur, | ||
'blur', | ||
mountAt | ||
); | ||
} else if (isEventSupported('focusin')) { | ||
// IE has `focusin` and `focusout` events which bubble. | ||
// @see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html | ||
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent( | ||
trapBubbledEvent( | ||
topLevelTypes.topFocus, | ||
'focusin', | ||
mountAt | ||
); | ||
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent( | ||
trapBubbledEvent( | ||
topLevelTypes.topBlur, | ||
'focusout', | ||
mountAt | ||
|
@@ -318,7 +351,7 @@ var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, { | |
isListening[topLevelTypes.topBlur] = true; | ||
isListening[topLevelTypes.topFocus] = true; | ||
} else if (topEventMapping.hasOwnProperty(dependency)) { | ||
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent( | ||
trapBubbledEvent( | ||
dependency, | ||
topEventMapping[dependency], | ||
mountAt | ||
|
@@ -328,6 +361,14 @@ var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, { | |
isListening[dependency] = true; | ||
} | ||
} | ||
|
||
return function() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Quickly reading this I'm having a little trouble understanding the exact flow, if I understand this correctly, this seems like a wasteful way of doing it. Allocating a new callback which holds an array of callbacks, instead of keeping a single global array of callbacks? Or am I missing something. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function can accept documents and other types of targets. In both these cases it returns a remover that removes all registered listeners. To give a consistent interface it must therefore always return a remover for all types of target. In this case, the caller knows if its adding to a document, and it does the right thing with the returned remover. Its simply trying to match the style that whoever registers a listener also takes care of removing them at the right time. Regarding the wastefulness of creating an array for each call, it hardly matters, as this isnt called that frequently, actually it only happens once if your "root" is the document, or once for the other "targets", which again wont be that many. |
||
listenerRemovers.forEach( | ||
function(remover) { | ||
remover(); | ||
} | ||
); | ||
}; | ||
}, | ||
|
||
trapBubbledEvent: function(topLevelType, handlerBaseName, handle) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,17 @@ var ReactUpdates = require('ReactUpdates'); | |
var getEventTarget = require('getEventTarget'); | ||
var getUnboundedScrollPosition = require('getUnboundedScrollPosition'); | ||
|
||
/** | ||
* Tracks all listeners added to a document, for later batch removal. | ||
* @type {Array} | ||
*/ | ||
var documentEventListenerRemovers = []; | ||
|
||
/** | ||
* This is invoked when the document is reset. | ||
*/ | ||
var documentResetCallback = function() {}; | ||
|
||
/** | ||
* Find the deepest React component completely containing the root of the | ||
* passed-in instance (for use when entire React trees are nested within each | ||
|
@@ -172,6 +183,40 @@ var ReactEventListener = { | |
TopLevelCallbackBookKeeping.release(bookKeeping); | ||
} | ||
}, | ||
/** | ||
* Records the remover for a previously added document event listener. | ||
* @param {function} remover A function that removes an added listener. | ||
*/ | ||
addDocumentEventListenerRemover: function(remover) { | ||
if (remover) { | ||
documentEventListenerRemovers.push(remover); | ||
} | ||
}, | ||
|
||
/** | ||
* This callback will be invoked when resetDocument is also called. | ||
* @param callback | ||
*/ | ||
setDocumentResetCallback: function(callback) { | ||
documentResetCallback = callback; | ||
}, | ||
|
||
/** | ||
* Removes all previously added document or global event listeners. | ||
* @param {object} The document holding the last component. | ||
*/ | ||
resetDocument: function(doc) { | ||
for (;;) { | ||
var remover = documentEventListenerRemovers.pop(); | ||
if (!remover) { | ||
break; | ||
} | ||
remover(); | ||
} | ||
|
||
documentResetCallback(doc); | ||
}, | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately i had to add these "public" or "visible" apis because other places need to call them. |
||
}; | ||
|
||
module.exports = ReactEventListener; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure if the names $trapBubbledEvent and $trapCapturedEvent should be different so theres no confusion about them being refs to the ones that live in ReactEventListener.