Fix bugs that occur when event responder unmounts during a touch event sequence

> This PR adds a test (initially failing) for this case along with a fix for stack and fiber. The stack fix was copied from a Diff submitted by @sema. The fiber fix just required us to stop leaking properties for unmounted views.
> Longer term we may want to explicitly invoke a release event listener for a responder just before unmounting it. This PR does not do that.
This commit is contained in:
Brian Vaughn 2017-02-22 10:21:24 -08:00 committed by Brian Vaughn
parent 42c375b136
commit a44a5d68d8
5 changed files with 104 additions and 0 deletions

View File

@ -1454,6 +1454,7 @@ src/renderers/native/__tests__/ReactNativeAttributePayload-test.js
src/renderers/native/__tests__/ReactNativeEvents-test.js
* handles events
* handles when a responder is unmounted while a touch sequence is in progress
src/renderers/native/__tests__/ReactNativeMount-test.js
* should be able to create and render a native component

View File

@ -12,7 +12,9 @@
// Mock of the Native Hooks
var RCTUIManager = {
clearJSResponder: jest.fn(),
createView: jest.fn(),
setJSResponder: jest.fn(),
setChildren: jest.fn(),
manageChildren: jest.fn(),
updateView: jest.fn(),

View File

@ -53,6 +53,7 @@ function uncacheNode(inst) {
function uncacheFiberNode(tag) {
delete instanceCache[tag];
delete instanceProps[tag];
}
function getInstanceFromTag(tag) {

View File

@ -15,6 +15,7 @@ var RCTEventEmitter;
var React;
var ReactErrorUtils;
var ReactNative;
var ResponderEventPlugin;
var UIManager;
var createReactNativeComponentClass;
@ -25,6 +26,7 @@ beforeEach(() => {
React = require('React');
ReactErrorUtils = require('ReactErrorUtils');
ReactNative = require('ReactNative');
ResponderEventPlugin = require('ResponderEventPlugin');
UIManager = require('UIManager');
createReactNativeComponentClass = require('createReactNativeComponentClass');
@ -91,3 +93,97 @@ it('handles events', () => {
'outer touchend',
]);
});
it('handles when a responder is unmounted while a touch sequence is in progress', () => {
var EventEmitter = RCTEventEmitter.register.mock.calls[0][0];
var View = createReactNativeComponentClass({
validAttributes: { id: true },
uiViewClassName: 'View',
});
function getViewById(id) {
return UIManager.createView.mock.calls.find(
args => args[3] && args[3].id === id
)[0];
}
function getResponderId() {
const responder = ResponderEventPlugin._getResponder();
if (responder === null) {
return null;
}
const props = typeof responder.tag === 'number'
? responder.memoizedProps
: responder._currentElement.props;
return props ? props.id : null;
}
var log = [];
ReactNative.render(
<View id="parent">
<View key={1}>
<View
id="one"
onResponderEnd={() => log.push('one responder end')}
onResponderStart={() => log.push('one responder start')}
onStartShouldSetResponder={() => true}
/>
</View>
<View key={2}>
<View
id="two"
onResponderEnd={() => log.push('two responder end')}
onResponderStart={() => log.push('two responder start')}
onStartShouldSetResponder={() => true}
/>
</View>
</View>,
1
);
EventEmitter.receiveTouches(
'topTouchStart',
[{target: getViewById('one'), identifier: 17}],
[0]
);
expect(getResponderId()).toBe('one');
expect(log).toEqual(['one responder start']);
log.splice(0);
ReactNative.render(
<View id="parent">
<View key={2}>
<View
id="two"
onResponderEnd={() => log.push('two responder end')}
onResponderStart={() => log.push('two responder start')}
onStartShouldSetResponder={() => true}
/>
</View>
</View>,
1
);
// TODO Verify the onResponderEnd listener has been called (before the unmount)
// expect(log).toEqual(['one responder end']);
// log.splice(0);
EventEmitter.receiveTouches(
'topTouchEnd',
[{target: getViewById('two'), identifier: 17}],
[0]
);
expect(getResponderId()).toBeNull();
expect(log).toEqual([]);
EventEmitter.receiveTouches(
'topTouchStart',
[{target: getViewById('two'), identifier: 17}],
[0]
);
expect(getResponderId()).toBe('two');
expect(log).toEqual(['two responder start']);
});

View File

@ -142,6 +142,10 @@ var EventPluginHub = {
// Text node, let it bubble through.
return null;
}
if (!inst._rootNodeID) {
// If the instance is already unmounted, we have no listeners.
return null;
}
const props = inst._currentElement.props;
listener = props[registrationName];
if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, props)) {