在RN的性能监控中,有一个比较复杂的问题,是如何去计算一个按钮的点击响应时长。
我们知道,RN所有的UI控件都是在Native端渲染的,同样用户的点击操作也是发生在Native端,那诸如TouchableOpacity这样的按钮,他们的点击事件是如何产生的?又是如何传递到JS端,并且最终调用我们写好的 onPress方法呢?
首先,RN的按钮点击是一个典型的Native到JS的事件传递,通过在TouchableOpacity组件的onPress方法上打断点我们可以看到JS端的调用链路(中间省略了一些步骤,只截取关键的几个方法):
callFunctionReturnFlushedQuque ->
RCTEventEmitter.receiveTouches(topTouchEnd) ->
_receiveRootNodeIDEvent ->
batchedUpdated ->
Pressability.onResponderRelease ->
_receiveSignal(‘RESPONDER_RELEASE’) ->
Pressability._receiveSignal->
_performTransitionSideEffects->
onPress
其中callFunctionReturnFlushedQuque是原生调用JS的通用方法,经过RCTEventEmitter.receiveTouches接收touch事件,并把touch传递到全局的_receiveRootNodeIDEvent方法中
function receiveTouches(eventTopLevelType, touches, changedIndices) {
var changedTouches =
eventTopLevelType === "topTouchEnd" ||
eventTopLevelType === "topTouchCancel"
? removeTouchesAtIndices(touches, changedIndices)
: touchSubsequence(touches, changedIndices);
for (var jj = 0; jj < changedTouches.length; jj++) {
var touch = changedTouches[jj]; // Touch objects can fulfill the role of `DOM` `Event` objects if we set
// the `changedTouches`/`touches`. This saves allocations.
touch.changedTouches = changedTouches;
touch.touches = touches;
var nativeEvent = touch;
var rootNodeID = null;
var target = nativeEvent.target;
if (target !== null && target !== undefined) {
if (target < 1) {
{
error("A view is reporting that a touch occurred on tag zero.");
}
} else {
rootNodeID = target;
}
} // $FlowFixMe Shouldn't we *not* call it if rootNodeID is null?
_receiveRootNodeIDEvent(rootNodeID, eventTopLevelType, nativeEvent);
}
}
在_receiveRootNodeIDEvent方法中,我们通过rootNodeID拿到对应的节点(inst),调用batchedUpdates更新节点的状态
function _receiveRootNodeIDEvent(rootNodeID, topLevelType, nativeEventParam) {
var nativeEvent = nativeEventParam || EMPTY_NATIVE_EVENT;
var inst = getInstanceFromTag(rootNodeID);
var target = null;
if (inst != null) {
target = inst.stateNode;
}
batchedUpdates(function() {
runExtractedPluginEventsInBatch(
topLevelType,
inst,
nativeEvent,
target,
PLUGIN_EVENT_SYSTEM
);
}); // React Native doesn't use ReactControlledComponent but if it did, here's
// where it would do it.
}
经过中间一系列的计算和处理,我们会走到Pressability.onResponderRelease这个方法,这里会触发RESPONDER_RELEASE这个信号,并调用_performTransitionSideEffects这个方法,并最终触发节点的onPress方法。
onResponderRelease: (event: PressEvent): void => {
this._receiveSignal('RESPONDER_RELEASE', event);
},
_receiveSignal(signal: TouchSignal, event: PressEvent): void {
const prevState = this._touchState;
const nextState = Transitions[prevState]?.[signal];
if (this._responderID == null && signal === 'RESPONDER_RELEASE') {
return;
}
invariant(
nextState != null && nextState !== 'ERROR',
'Pressability: Invalid signal `%s` for state `%s` on responder: %s',
signal,
prevState,
typeof this._responderID === 'number'
? this._responderID
: '<>',
);
if (prevState !== nextState) {
this._performTransitionSideEffects(prevState, nextState, signal, event);
this._touchState = nextState;
}
}
前面我们可以看到一个关键的方法RCTEventEmitter.receiveTouches
,这是原生发过来的通知,带着这个关键字我们去iOS的源码里搜索,可以看到他是在RCTTouchEvent的moduleDotMethod方法里面定义的,继续搜索我们可以看到这个RCTTouchEvent是在RCTEventDispatch.sendEvent方法内作为参数被使用的,这个RCTEventDispatch.sendEvent会把我们的触摸事件放到队列里面,然后异步派发到JS线程执行他。
- (void)sendEvent:(id)event
{
[_observersLock lock];
for (id observer in _observers) {
[observer eventDispatcherWillDispatchEvent:event];
}
[_observersLock unlock];
[_eventQueueLock lock];
NSNumber *eventID;
if (event.canCoalesce) {
eventID = RCTGetEventID(event.viewTag, event.eventName, event.coalescingKey);
id previousEvent = _events[eventID];
if (previousEvent) {
event = [previousEvent coalesceWithEvent:event];
} else {
[_eventQueue addObject:eventID];
}
} else {
id previousEvent = _events[eventID];
eventID = RCTGetEventID(event.viewTag, event.eventName, RCTUniqueCoalescingKeyGenerator++);
RCTAssert(
previousEvent == nil,
@"Got event %@ which cannot be coalesced, but has the same eventID %@ as the previous event %@",
event,
eventID,
previousEvent);
[_eventQueue addObject:eventID];
}
_events[eventID] = event;
BOOL scheduleEventsDispatch = NO;
if (!_eventsDispatchScheduled) {
_eventsDispatchScheduled = YES;
scheduleEventsDispatch = YES;
}
// We have to release the lock before dispatching block with events,
// since dispatchBlock: can be executed synchronously on the same queue.
// (This is happening when chrome debugging is turned on.)
[_eventQueueLock unlock];
if (scheduleEventsDispatch) {
[_bridge
dispatchBlock:^{
[self flushEventsQueue];
}
queue:RCTJSThread];
}
}
这个RCTEventDispatch.sendEvent是被一个RCTTouchHandler类型的对象调用的,RCTTouchHandler是真正接受触摸事件的对象,他在RN页面创建的时候就被附加到RCTRootContentView上了,并作为UIGestureRecognizer 处理该页面上的所有手势。
RCTTouchHandler会实现touchesBegan/touchesMoved/touchesEnded/touchesCancelled等方法,以touchesBegan为例:他通过_recordNewTouches:touches记录了当前触摸的事件及对应组件的ReactTag,并调用_updateAndDispatchTouches把触摸事件传递到RCTEventDispatch,并且记录了当前手势的状态。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
[self _cacheRootView];
// "start" has to record new touches *before* extracting the event.
// "end"/"cancel" needs to remove the touch *after* extracting the event.
[self _recordNewTouches:touches];
[self _updateAndDispatchTouches:touches eventName:@"touchStart"];
if (self.state == UIGestureRecognizerStatePossible) {
self.state = UIGestureRecognizerStateBegan;
} else if (self.state == UIGestureRecognizerStateBegan) {
self.state = UIGestureRecognizerStateChanged;
}
}
@implementation RCTRootContentView
- (instancetype)initWithFrame:(CGRect)frame
bridge:(RCTBridge *)bridge
reactTag:(NSNumber *)reactTag
sizeFlexiblity:(RCTRootViewSizeFlexibility)sizeFlexibility
{
if ((self = [super initWithFrame:frame])) {
_bridge = bridge;
self.reactTag = reactTag;
_sizeFlexibility = sizeFlexibility;
_touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge];
[_touchHandler attachToView:self];
[_bridge.uiManager registerRootView:self];
}
return self;
}
因此,Native这边的整个调用链路是这样的:
UserClick on ReactNativePage->
RCTTouchHandler.touchBegan/touchMoved/touchEnded/touchCancelled->
RCTTouchHandler._updateAndDispatchTouches->
RCTEventDispatch.sendEvent(RCTTouchEvent)
分析了按钮点击的整个调用链路之后,我们怎么计算按钮的点击响应时长呢?且听下回分解。