美文网首页
React Native 按钮的实现与源码解析

React Native 按钮的实现与源码解析

作者: Arnold134777 | 来源:发表于2017-07-18 22:23 被阅读358次

    [图片上传中...(pic1.png-835f42-1510063522595-0)]
    iOS与安卓的原生实现实际都已经提供了UIButtonButton的的按钮组件。但是我们发现RN并没有基于这两个组件实现统一的按钮组件。那如何在RN中实现按钮组件呢?其内部实现又是如何处理的?本文将从设计按钮,使用按钮,具体实现,三部分解析整个按钮的实现逻辑。

    依赖

    • RN的版本 0.38
    • 系统:iOS

    按钮设计

    MyCustomButton.js

    import React, {Component} from 'react';
    import { 
        StyleSheet,
        Text,
        TouchableHighlight
    } from 'react-native';
    
    class MyCustomButton extends React.Component {
      props: Props;
    
      constructor(props: Props) {
        super(props);
      }
    
      render() {
        return (
          <TouchableHighlight
            style={styles.button}
            underlayColor="#a5a5a5"
            onPress={this.props.onPress}>
            <Text style={styles.buttonText}>{this.props.text}</Text>
          </TouchableHighlight>
        );
      }
    }
    
    const styles = StyleSheet.create({
        button: {
            borderWidth: 1,
        },
        buttonText: {
            fontSize: 18,
            color: 'red',
            backgroundColor:'transparent',
            alignSelf:'center'
        },
        text: {
            fontSize: 16,
            marginBottom:20
        },
    });
    
    module.exports = MyCustomButton;
    

    具体使用

    import React, { Component } from 'react';
    import { 
        StyleSheet,
        View
    } from 'react-native';
    import MyCustomButton from './MyCustomButton'
    
    class TestScreen extends Component{
        _onPress(){
            alert('按钮点击');
        }
        
        render(){
            return (<View style={styles.container}>
              <View
                style={styles.buttonWrap}
              >
              <MyCustomButton
                onPress={this._onPress}
                text={"按钮"}
              ></MyCustomButton>
              </View>
            </View>)
        }
    }
    
    let styles = StyleSheet.create({
      container: {
        marginTop: 20,
        flex: 1
      },
      buttonWrap:{
        height:100,
        width:50
      }
    });
    
    module.exports = TestScreen;
    

    主要想分析按钮实现,因此该Demo例子只是简单满足按钮的要求。

    TouchableHighlight及相关组件

    我们查看上述例子中的TouchableHighlight的实现代码中以下这一段,由此可以推断是在TouchableWithoutFeedback的基础上做的扩展。

    var TouchableHighlight = React.createClass({
      propTypes: {
        ...TouchableWithoutFeedback.propTypes,
      }
    });
    

    我们看看TouchableWithoutFeedback以及其他几种扩展的效果:

    组件 描述 效果图
    TouchableWithoutFeedback 响应点击事件,无任何反馈
    TouchableHighlight 点击状态背景变暗
    TouchableOpacity 点击状态改变背景的透明度
    TouchableNativeFeedback 此组件只支持Android,不作分析 -

    组件API的调用此处就不作具体介绍,可以查看React Native的官方文档

    Native与jS端按钮一块的交互逻辑

    下面我们来具体分析RN中按钮的内部实现,从Native切入考虑。想到iOS端能处理手势事件的类--UIGestureRecognizer

    我们进入到node_modules目录执行grep "UIGestureRecognizer" -rn .得到如下结果:

    不难看出RN中继承UIGestureRecognizer实现了自己的手势处理的派生类RCTTouchHandler。按钮的点击功能的实现便从RCTTouchHandler开始分析。

    Native端的处理

    RCTTouchHandler入口

    全局搜索RCTTouchHandler我们发现RN页面的承载容器RCTRootView中的子组件RCTRootContentView存在RCTTouchHandler的属性:

    @interface RCTRootContentView : RCTView <RCTInvalidating>
    
    @property (nonatomic, readonly) BOOL contentHasAppeared;
    @property (nonatomic, readonly, strong) RCTTouchHandler *touchHandler;
    @property (nonatomic, assign) BOOL passThroughTouches;
    
    - (instancetype)initWithFrame:(CGRect)frame
                           bridge:(RCTBridge *)bridge
                         reactTag:(NSNumber *)reactTag
                   sizeFlexiblity:(RCTRootViewSizeFlexibility)sizeFlexibility NS_DESIGNATED_INITIALIZER;
    @end
    
    @implementation RCTRootContentView
    {
      __weak RCTBridge *_bridge;
      UIColor *_backgroundColor;
    }
    
    - (instancetype)initWithFrame:(CGRect)frame
                           bridge:(RCTBridge *)bridge
                         reactTag:(NSNumber *)reactTag
                   sizeFlexiblity:(RCTRootViewSizeFlexibility)sizeFlexibility
    {
      if ((self = [super initWithFrame:frame])) {
        _bridge = bridge;
        self.reactTag = reactTag;
        
        // 注意此处_touchHandler手势实例初始化完成,然后添加到contentView上,这样contentView便可以处理手势事件了
        _touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge];
        [self addGestureRecognizer:_touchHandler];
        [_bridge.uiManager registerRootView:self withSizeFlexibility:sizeFlexibility];
        self.layer.backgroundColor = NULL;
      }
      return self;
    }
    // 省略部分代码段
    @end
    

    RCTTouchHandler初始化逻辑

    以下是RCTTouchHandler的初始化逻辑,

    - (instancetype)initWithBridge:(RCTBridge *)bridge
    {
      RCTAssertParam(bridge);
    
      // 初始化绑定事件的处理函数
      if ((self = [super initWithTarget:self action:@selector(handleGestureUpdate:)])) {
    
        _eventDispatcher = [bridge moduleForClass:[RCTEventDispatcher class]];
        _dispatchedInitialTouches = NO;
        _nativeTouches = [NSMutableOrderedSet new];
        _reactTouches = [NSMutableArray new];
        _touchViews = [NSMutableArray new];
    
        // `cancelsTouchesInView` is needed in order to be used as a top level
        // event delegated recognizer. Otherwise, lower-level components not built
        // using RCT, will fail to recognize gestures.
        self.cancelsTouchesInView = NO;
      }
      return self;
    }
    

    由此得出按钮的点击必将触发handleGestureUpdate函数。

    RCTTouchHandler中手势处理

    我们都知道手势触发会先执行如下的函数:

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
    - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
    - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
    - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
    

    因此我们在RCTTouchHandler中的handleGestureUpdate函数以及上述函数中加入断点调试分析。根据代码执行顺序来看一下具体实现逻辑。

    开始触摸topTouchStart代码执行流程:

    // 1.点击按钮时触发
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event 
     
        // 2.记录点击view,点击事件touch,点击事件对应的reactTouch
        - (void)_recordNewTouches:(NSSet<UITouch *> *)touches 
        
        // 3.监听手势触发的函数
        - (void)handleGestureUpdate:(__unused UIGestureRecognizer *)gesture
        
        // 4.更新reactTouch,生成touchEvent,使用_eventDispatcher事件将该触发事件发送给js端
        - (void)_updateAndDispatchTouches:(NSSet<UITouch *> *)touches
                            eventName:(NSString *)eventName
                      originatingTime:(__unused CFTimeInterval)originatingTime
            // 5.将native统计的touch信息同步到react的touch中       
            - (void)_updateReactTouchAtIndex:(NSInteger)touchIndex
            
             // 6.将touch通过发送事件的方式通知给js
            RCTTouchEvent *event = [[RCTTouchEvent alloc] initWithEventName:eventName
                                                                 reactTag:self.view.reactTag
                                                             reactTouches:reactTouches
                                                           changedIndexes:changedIndexes
                                                            coalescingKey:_coalescingKey];
            [_eventDispatcher sendEvent:event];             
    

    上述的代码段6中,self.view.reactTag,reactTag是表示reactView的唯一标识。是由js端的ReactNativeTagHandles.allocateTag()生成,有兴趣可以自己研究,此处不作扩展分析。

    topTouchEnd的流程与上述的类似不作额外分析。

    js端的处理

    Touchable手势处理类

    上面我们看了Native端是如何触发按钮点击事件的,如何将native的触摸事件传给js端。下面我们将从js继续分析按钮点击事件的整个流程:PRESSIN -> PRESSEND。
    我们查看TouchableWithoutFeedback中如下一段代码:

    
    // mixins 模式的使用使得TouchableWithoutFeedback拥有Touchable的属性与方法
    const TouchableWithoutFeedback = React.createClass({
      mixins: [TimerMixin, Touchable.Mixin],
    });
    
    /**
     * `Touchable.Mixin` self callbacks. The mixin will invoke these if  they are
     * defined on your component.
     */
    touchableHandlePress: function(e: Event) {
    this.props.onPress && this.props.onPress(e);
    },
    

    显然的按钮的点击事件onPress实际是触发Touchable中的onPress函数的执行。下面我们具体看看Touchable的处理逻辑。

    Touchable手势处理流程图

     * ======= State Machine =======
     *
     * +-------------+ <---+ RESPONDER_RELEASE
     * |NOT_RESPONDER|
     * +-------------+ <---+ RESPONDER_TERMINATED
     *     +
     *     | RESPONDER_GRANT (HitRect)
     *     v
     * +---------------------------+  DELAY   +-------------------------+  T + DELAY     +------------------------------+
     * |RESPONDER_INACTIVE_PRESS_IN|+-------->|RESPONDER_ACTIVE_PRESS_IN| +------------> |RESPONDER_ACTIVE_LONG_PRESS_IN|
     * +---------------------------+          +-------------------------+                +------------------------------+
     *     +            ^                         +           ^                                 +           ^
     *     |LEAVE_      |ENTER_                   |LEAVE_     |ENTER_                           |LEAVE_     |ENTER_
     *     |PRESS_RECT  |PRESS_RECT               |PRESS_RECT |PRESS_RECT                       |PRESS_RECT |PRESS_RECT
     *     |            |                         |           |                                 |           |
     *     v            +                         v           +                                 v           +
     * +----------------------------+  DELAY  +--------------------------+               +-------------------------------+
     * |RESPONDER_INACTIVE_PRESS_OUT|+------->|RESPONDER_ACTIVE_PRESS_OUT|               |RESPONDER_ACTIVE_LONG_PRESS_OUT|
     * +----------------------------+         +--------------------------+               +-------------------------------+
     *
     * T + DELAY => LONG_PRESS_DELAY_MS + DELAY
     *
    

    从中我们能简单分析到按钮点击事件的几个状态变化:NOT_RESPONDER -> [RESPONDER_GRANT] -> RESPONDER_INACTIVE_PRESS_IN -> [LEAVE_PRESS_RECT] -> RESPONDER_ACTIVE_PRESS_OUT。我们开启RN的调试模式利用Chrome浏览器验证一下。

    Touchable touchableHandleResponderGrant

    由上述的流程图我们将断点加入到当前的函数,点击按钮(先不释放),我们看到如下的调试结果:

    ,初步验证了猜测,我们看看touchableHandleResponderGrant中的处理:
    touchableHandleResponderGrant: function(e) {
        var dispatchID = e.currentTarget;
        // Since e is used in a callback invoked on another event loop
        // (as in setTimeout etc), we need to call e.persist() on the
        // event to make sure it doesn't get reused in the event object pool.
        
        // 1.标记为已经处理,避免该event被重复处理
        e.persist();
        
        // 2.清理掉pressOutDelayTimeout 
        this.pressOutDelayTimeout && clearTimeout(this.pressOutDelayTimeout);
        this.pressOutDelayTimeout = null;
        
        // 3.初始化当前的touchState为States.NOT_RESPONDER;
        this.state.touchable.touchState = States.NOT_RESPONDER;
        this.state.touchable.responderID = dispatchID;
        
        // 4.接收触发开始信号,处理逻辑见下
        this._receiveSignal(Signals.RESPONDER_GRANT, e);
        
        // 5.设置点击事件有效的时间间隔,执行_handleDelay函数
        var delayMS =
          this.touchableGetHighlightDelayMS !== undefined ?
          Math.max(this.touchableGetHighlightDelayMS(), 0) : HIGHLIGHT_DELAY_MS;
        delayMS = isNaN(delayMS) ? HIGHLIGHT_DELAY_MS : delayMS;
        if (delayMS !== 0) {
          this.touchableDelayTimeout = setTimeout(
            this._handleDelay.bind(this, e),
            delayMS
          );
        } else {
          this._handleDelay(e);
        }
    
        // 6.设置长按事件的触发时间间隔,执行_handleLongDelay函数
        var longDelayMS =
          this.touchableGetLongPressDelayMS !== undefined ?
          Math.max(this.touchableGetLongPressDelayMS(), 10) : LONG_PRESS_DELAY_MS;
        longDelayMS = isNaN(longDelayMS) ? LONG_PRESS_DELAY_MS : longDelayMS;
        this.longPressDelayTimeout = setTimeout(
          this._handleLongDelay.bind(this, e),
          longDelayMS + delayMS
        );
      },
    

    Touchable _receiveSignal

    /**
       * Receives a state machine signal, performs side effects of the transition
       * and stores the new state. Validates the transition as well.
       *
       * @param {Signals} signal State machine signal.
       * @throws Error if invalid state transition or unrecognized signal.
       * @sideeffects
       */
      _receiveSignal: function(signal, e) {
        var responderID = this.state.touchable.responderID;
        var curState = this.state.touchable.touchState;
        
        // 1.Transitions是全局维护的字典:state ->(singal) -> nextState,
        // 具体可以自己查看Transitions定义
        var nextState = Transitions[curState] && Transitions[curState][signal];
        if (!responderID && signal === Signals.RESPONDER_RELEASE) {
          return;
        }
        if (!nextState) {
          throw new Error(
            'Unrecognized signal `' + signal + '` or state `' + curState +
            '` for Touchable responder `' + responderID + '`'
          );
        }
        if (nextState === States.ERROR) {
          throw new Error(
            'Touchable cannot transition from `' + curState + '` to `' + signal +
            '` for responder `' + responderID + '`'
          );
        }
        if (curState !== nextState) {
        
          // 2.根据state,nextState,singal来判断当前的操作状态,改变按钮的状态,执行相关回调
          this._performSideEffectsForTransition(curState, nextState, signal, e);
          this.state.touchable.touchState = nextState;
        }
      },
    

    我们在_performSideEffectsForTransition中看到了如下的代码段:

    if (IsPressingIn[curState] && signal === Signals.RESPONDER_RELEASE) {
      var hasLongPressHandler = !!this.props.onLongPress;
      var pressIsLongButStillCallOnPress =
        IsLongPressingIn[curState] && (    // We *are* long pressing..
          !hasLongPressHandler ||          // But either has no long handler
          !this.touchableLongPressCancelsPress() // or we're told to ignore it.
        );
    
      var shouldInvokePress =  !IsLongPressingIn[curState] || pressIsLongButStillCallOnPress;
      if (shouldInvokePress && this.touchableHandlePress) {
        if (!newIsHighlight && !curIsHighlight) {
          // we never highlighted because of delay, but we should highlight now
          this._startHighlight(e);
          this._endHighlight(e);
        }
        // 此处是真正触发onPress函数的调用
        this.touchableHandlePress(e);
      }
    }
    

    因此我们大胆的猜测,当按钮点击完成即触摸离开时触发Signals.RESPONDER_RELEASE的行为,完成整个的按钮点击的操作。搜索全局查看到如下代码段,加入断点分析。

    /**
     * Place as callback for a DOM element's `onResponderRelease` event.
     */
    touchableHandleResponderRelease: function(e) {
       this._receiveSignal(Signals.RESPONDER_RELEASE, e);
    },
    
    当我们松开按钮时,我们看到如下的调试结果:

    进而印证了猜想。

    Native中的event到js端处理

    上述的流程分别分析了Native与js端针对按钮点击事件的处理,尚且留下一个疑问就是以下这段代码,即Native中的event到js端具体的处理流程是什么?

    RCTTouchEvent *event = [[RCTTouchEvent alloc] initWithEventName:eventName
                                                         reactTag:self.view.reactTag
                                                     reactTouches:reactTouches
                                                   changedIndexes:changedIndexes
                                                    coalescingKey:_coalescingKey];
    [_eventDispatcher sendEvent:event];
    

    查看了sendEvent:函数的实现,按钮点击的整个js的调用栈如下图,有点吓到,RN中的额event的设计也不是几句话分析清楚的,后续将写一篇博文重点介绍这一块的设计。本文就只需要知道Native的按钮触摸的信息是通过事件(event)的方式传送给js端的就行了,当然你有兴趣也可以自己研究。

    整个React Native中按钮的设计到具体实现基本告一段落,其中部分细节未展开分析,包括按钮高亮状态,禁用状态,长按事件等,有兴趣可以自己分析。后续将继续展开分析event的实现,欢迎关注。文章中有错误的地方欢迎指正,谢谢。

    相关文章

      网友评论

          本文标题:React Native 按钮的实现与源码解析

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