mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-04-01 09:23:16 +08:00
Summary:Previously, (mostly touch and scroll) event handling on iOS worked in a hybrid way: * All incoming coalesce-able events would be pooled and retrieved by js thread in the beginning of its frame (all of this happens on js thread) * Any non-coalesce-able event would be immediately dispatched on a js thread (triggered from main thread), and if there would be pooled coalesce-able events they would be immediately dispatched at first too. This behavior has a subtle race condition, where two events are produced (on MT) in one order and received in js in different order. See https://github.com/facebook/react-native/issues/5246#issuecomment-198326673 for further explanation of this case. The new event handling is (afaik) what Android already does. When an event comes we add it into a pool of events and dispatch a block on js thread to inform js there are events to be processed. We keep track of whether we did so, so there is at most one of these blocks waiting to be processed. When the block is executed js will process all events that are in pool at that time (NOT at time of enqueuing the block). This creates a single way of processing events and makes it impossible to process them in different order in js. The tricky part was making sure we don't coalesce events across gestures/different scrolls. Before this was achieved by knowing that gestures and scrolls start/end with non-coalesce-able event, so the pool never contained events that shouldn't be coalesced together. That "assumption" doesn't hold now. I've re-added `coalescingKey` and made touch and scroll events use it to prevent coalescing events of the same type that should remain separate in previous diffs (see dependencies). On top of it it decreases latency in events processing in case where we get only coalesce-able events. Previously these would be processed at begging of the next js frame, even when js would be free and could process them sooner. This delay is done, since they would get processed as soon as the enqueued block would run. To illustrate this improvement let's look at these two systraces. Before: https://cloud.githubusercontent.com/assets/713625/14021417/47b35b7a-f1d3-11e5-93dd-4363edfa1923.png After: https://cloud.githubusercontent.com/assets/713625/14021415/4798582a-f1d3-11e5-8715-425596e0781c.png Reviewed By: javache Differential Revision: D3092867 fb-gh-sync-id: 29071780f00fcddb0b1886a46caabdb3da1d5d84 fbshipit-source-id: 29071780f00fcddb0b1886a46caabdb3da1d5d84
251 lines
7.5 KiB
Objective-C
251 lines
7.5 KiB
Objective-C
/**
|
|
* The examples provided by Facebook are for non-commercial testing and
|
|
* evaluation purposes only.
|
|
*
|
|
* Facebook reserves all rights not expressly granted.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
|
|
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
#import <UIKit/UIKit.h>
|
|
#import <XCTest/XCTest.h>
|
|
|
|
#import <OCMock/OCMock.h>
|
|
#import "RCTEventDispatcher.h"
|
|
#import "RCTBridge+Private.h"
|
|
|
|
@interface RCTTestEvent : NSObject <RCTEvent>
|
|
@property (atomic, assign, readwrite) BOOL canCoalesce;
|
|
@end
|
|
|
|
@implementation RCTTestEvent
|
|
{
|
|
NSDictionary<NSString *, id> *_body;
|
|
}
|
|
|
|
@synthesize viewTag = _viewTag;
|
|
@synthesize eventName = _eventName;
|
|
@synthesize coalescingKey = _coalescingKey;
|
|
|
|
- (instancetype)initWithViewTag:(NSNumber *)viewTag
|
|
eventName:(NSString *)eventName
|
|
body:(NSDictionary<NSString *, id> *)body
|
|
coalescingKey:(uint16_t)coalescingKey
|
|
{
|
|
if (self = [super init]) {
|
|
_viewTag = viewTag;
|
|
_eventName = eventName;
|
|
_body = body;
|
|
_canCoalesce = YES;
|
|
_coalescingKey = coalescingKey;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (id<RCTEvent>)coalesceWithEvent:(id<RCTEvent>)newEvent
|
|
{
|
|
return newEvent;
|
|
}
|
|
|
|
+ (NSString *)moduleDotMethod
|
|
{
|
|
return @"RCTDeviceEventEmitter.emit";
|
|
}
|
|
|
|
- (NSArray *)arguments
|
|
{
|
|
return @[_eventName, _body];
|
|
}
|
|
|
|
@end
|
|
|
|
@interface RCTEventDispatcherTests : XCTestCase
|
|
@end
|
|
|
|
@implementation RCTEventDispatcherTests
|
|
{
|
|
id _bridge;
|
|
RCTEventDispatcher *_eventDispatcher;
|
|
|
|
NSString *_eventName;
|
|
NSDictionary<NSString *, id> *_body;
|
|
RCTTestEvent *_testEvent;
|
|
NSString *_JSMethod;
|
|
}
|
|
|
|
|
|
- (void)setUp
|
|
{
|
|
[super setUp];
|
|
|
|
_bridge = [OCMockObject mockForClass:[RCTBatchedBridge class]];
|
|
|
|
_eventDispatcher = [RCTEventDispatcher new];
|
|
[_eventDispatcher setValue:_bridge forKey:@"bridge"];
|
|
|
|
_eventName = RCTNormalizeInputEventName(@"sampleEvent");
|
|
_body = @{ @"foo": @"bar" };
|
|
_testEvent = [[RCTTestEvent alloc] initWithViewTag:nil
|
|
eventName:_eventName
|
|
body:_body
|
|
coalescingKey:0];
|
|
|
|
_JSMethod = [[_testEvent class] moduleDotMethod];
|
|
}
|
|
|
|
- (void)testLegacyEventsAreImmediatelyDispatched
|
|
{
|
|
[[_bridge expect] enqueueJSCall:_JSMethod
|
|
args:[_testEvent arguments]];
|
|
|
|
[_eventDispatcher sendDeviceEventWithName:_eventName body:_body];
|
|
|
|
[_bridge verify];
|
|
}
|
|
|
|
- (void)testNonCoalescingEventIsImmediatelyDispatched
|
|
{
|
|
_testEvent.canCoalesce = NO;
|
|
|
|
[[_bridge expect] dispatchBlock:OCMOCK_ANY queue:RCTJSThread];
|
|
|
|
[_eventDispatcher sendEvent:_testEvent];
|
|
|
|
[_bridge verify];
|
|
}
|
|
|
|
- (void)testCoalescingEventIsImmediatelyDispatched
|
|
{
|
|
_testEvent.canCoalesce = YES;
|
|
|
|
[[_bridge expect] dispatchBlock:OCMOCK_ANY queue:RCTJSThread];
|
|
|
|
[_eventDispatcher sendEvent:_testEvent];
|
|
|
|
[_bridge verify];
|
|
}
|
|
|
|
- (void)testMultipleEventsResultInOnlyOneDispatchAfterTheFirstOne
|
|
{
|
|
[[_bridge expect] dispatchBlock:OCMOCK_ANY queue:RCTJSThread];
|
|
[_eventDispatcher sendEvent:_testEvent];
|
|
[_eventDispatcher sendEvent:_testEvent];
|
|
[_eventDispatcher sendEvent:_testEvent];
|
|
[_eventDispatcher sendEvent:_testEvent];
|
|
[_eventDispatcher sendEvent:_testEvent];
|
|
[_bridge verify];
|
|
}
|
|
|
|
- (void)testRunningTheDispatchedBlockResultInANewOneBeingEnqueued
|
|
{
|
|
__block dispatch_block_t eventsEmittingBlock;
|
|
[[_bridge expect] dispatchBlock:[OCMArg checkWithBlock:^(dispatch_block_t block) {
|
|
eventsEmittingBlock = block;
|
|
return YES;
|
|
}] queue:RCTJSThread];
|
|
[_eventDispatcher sendEvent:_testEvent];
|
|
[_bridge verify];
|
|
|
|
|
|
// eventsEmittingBlock would be called when js is no longer busy, which will result in emitting events
|
|
[[_bridge expect] enqueueJSCall:@"RCTDeviceEventEmitter.emit"
|
|
args:[_testEvent arguments]];
|
|
eventsEmittingBlock();
|
|
[_bridge verify];
|
|
|
|
|
|
[[_bridge expect] dispatchBlock:OCMOCK_ANY queue:RCTJSThread];
|
|
[_eventDispatcher sendEvent:_testEvent];
|
|
[_bridge verify];
|
|
}
|
|
|
|
- (void)testBasicCoalescingReturnsLastEvent
|
|
{
|
|
__block dispatch_block_t eventsEmittingBlock;
|
|
[[_bridge expect] dispatchBlock:[OCMArg checkWithBlock:^(dispatch_block_t block) {
|
|
eventsEmittingBlock = block;
|
|
return YES;
|
|
}] queue:RCTJSThread];
|
|
[[_bridge expect] enqueueJSCall:@"RCTDeviceEventEmitter.emit"
|
|
args:[_testEvent arguments]];
|
|
|
|
RCTTestEvent *ignoredEvent = [[RCTTestEvent alloc] initWithViewTag:nil
|
|
eventName:_eventName
|
|
body:@{ @"other": @"body" }
|
|
coalescingKey:0];
|
|
[_eventDispatcher sendEvent:ignoredEvent];
|
|
[_eventDispatcher sendEvent:_testEvent];
|
|
eventsEmittingBlock();
|
|
|
|
[_bridge verify];
|
|
}
|
|
|
|
- (void)testDifferentEventTypesDontCoalesce
|
|
{
|
|
NSString *firstEventName = RCTNormalizeInputEventName(@"firstEvent");
|
|
RCTTestEvent *firstEvent = [[RCTTestEvent alloc] initWithViewTag:nil
|
|
eventName:firstEventName
|
|
body:_body
|
|
coalescingKey:0];
|
|
|
|
__block dispatch_block_t eventsEmittingBlock;
|
|
[[_bridge expect] dispatchBlock:[OCMArg checkWithBlock:^(dispatch_block_t block) {
|
|
eventsEmittingBlock = block;
|
|
return YES;
|
|
}] queue:RCTJSThread];
|
|
[[_bridge expect] enqueueJSCall:@"RCTDeviceEventEmitter.emit"
|
|
args:[firstEvent arguments]];
|
|
[[_bridge expect] enqueueJSCall:@"RCTDeviceEventEmitter.emit"
|
|
args:[_testEvent arguments]];
|
|
|
|
|
|
[_eventDispatcher sendEvent:firstEvent];
|
|
[_eventDispatcher sendEvent:_testEvent];
|
|
eventsEmittingBlock();
|
|
|
|
[_bridge verify];
|
|
}
|
|
|
|
- (void)testSameEventTypesWithDifferentCoalesceKeysDontCoalesce
|
|
{
|
|
NSString *eventName = RCTNormalizeInputEventName(@"firstEvent");
|
|
RCTTestEvent *firstEvent = [[RCTTestEvent alloc] initWithViewTag:nil
|
|
eventName:eventName
|
|
body:_body
|
|
coalescingKey:0];
|
|
RCTTestEvent *secondEvent = [[RCTTestEvent alloc] initWithViewTag:nil
|
|
eventName:eventName
|
|
body:_body
|
|
coalescingKey:1];
|
|
|
|
__block dispatch_block_t eventsEmittingBlock;
|
|
[[_bridge expect] dispatchBlock:[OCMArg checkWithBlock:^(dispatch_block_t block) {
|
|
eventsEmittingBlock = block;
|
|
return YES;
|
|
}] queue:RCTJSThread];
|
|
[[_bridge expect] enqueueJSCall:@"RCTDeviceEventEmitter.emit"
|
|
args:[firstEvent arguments]];
|
|
[[_bridge expect] enqueueJSCall:@"RCTDeviceEventEmitter.emit"
|
|
args:[secondEvent arguments]];
|
|
|
|
|
|
[_eventDispatcher sendEvent:firstEvent];
|
|
[_eventDispatcher sendEvent:secondEvent];
|
|
[_eventDispatcher sendEvent:firstEvent];
|
|
[_eventDispatcher sendEvent:secondEvent];
|
|
[_eventDispatcher sendEvent:secondEvent];
|
|
[_eventDispatcher sendEvent:firstEvent];
|
|
[_eventDispatcher sendEvent:firstEvent];
|
|
|
|
eventsEmittingBlock();
|
|
|
|
[_bridge verify];
|
|
}
|
|
|
|
@end
|