美文网首页
RN按钮点击的处理流程(一)

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

作者: FingerStyle | 来源:发表于2022-08-28 23:24 被阅读0次

    在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
            : '<<host component>>',
        );
        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<RCTEvent>)event
    {
      [_observersLock lock];
    
      for (id<RCTEventDispatcherObserver> observer in _observers) {
        [observer eventDispatcherWillDispatchEvent:event];
      }
    
      [_observersLock unlock];
    
      [_eventQueueLock lock];
    
      NSNumber *eventID;
      if (event.canCoalesce) {
        eventID = RCTGetEventID(event.viewTag, event.eventName, event.coalescingKey);
        id<RCTEvent> previousEvent = _events[eventID];
        if (previousEvent) {
          event = [previousEvent coalesceWithEvent:event];
        } else {
          [_eventQueue addObject:eventID];
        }
      } else {
        id<RCTEvent> 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<UITouch *> *)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按钮点击的处理流程(一)

          本文链接:https://www.haomeiwen.com/subject/vxacnrtx.html