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

[图片上传中...(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的实现,欢迎关注。文章中有错误的地方欢迎指正,谢谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容