RN按钮点击的处理流程(一)

在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)

分析了按钮点击的整个调用链路之后,我们怎么计算按钮的点击响应时长呢?且听下回分解。

你可能感兴趣的:(RN按钮点击的处理流程(一))