RN 的触摸事件处理机制
触摸事件是用户与客户端 APP 主要的交互手段,本文我们来探索一下 RN 是如何通过组件来封装不同客户端的事件处理机制
不过在此之前,我们需要先简单了解下 Android 与 IOS 的触摸事件处理机制,这样才能了解 RN 在设计之初的权衡取舍
Android 的事件处理机制
我们先来看看 Android 是如何注册事件处理回调的:
java
public class ExampleActivity extends Activity implements OnClickListener {
protected void onCreate(Bundle savedValues) {
...
Button button = (Button)findViewById(R.id.corky);
// 给 button 元素注册点击事件的监听,如果 button 被点击,就会触发传入对象的 onClick 方法
button.setOnClickListener(this);
}
public void onClick(View v) {
// 处理点击事件
}
...
}
setOnClickListener 是 Android 中 View 这个类的接口方法,当用户在当前 View 触发了点击事件后,Android 就会主动调用被该方法注册的 onClick 方法
那么我们可以直接把这个方法传递到 js 侧处理吗?
答案是不行的,个人认为有几个原因:
setOnClickListener是 android 平台封装的方法,它没办法保证在多平台之间的一致性- 没办法定制一些更加原子化(例如 press-in/out)的表现
- 没办法实现/模拟 JS 的事件捕获以及事件冒泡
所以我们需要再深入一步,探索一下 Android 的事件有哪些以及是如何产生的
Android 的触摸事件
在 Android 的开发者手册中,常见的触摸事件被分成以下几类:
ACTION_DOWN:屏幕检测到手指放下的事件(一次触摸只会有一个)ACTION_MOVE:屏幕检测到放下的手指移动的事件(一般有多个)ACTION_UP:屏幕检测到放下的手指抬起来的事件(一次触摸最多只会有一个)ACTION_CANCEL:表示该触摸事件被取消了,比如被上层的 scroll view 接管了ACTION_OUTSIDE:表示手指离开了当前触摸元素的范围
这些事件都被封装进了一个 MotionEvent 类
上述的事件可以被组合成一个个语义化事件 semantic events,比如:
click = ACTION_DOWN + [ACTION_MOVE] + ACTION_UPlongPress = ACTION_DOWN + hold 200ms + [ACTION_MOVE] + ACTION_UP
在 RN 中,就是通过消费这些触摸事件,并且在 JS 侧自行组合成了给开发者消费的事件(比如 onPress)
Android 的事件处理流程
一般来说,Android 的事件从硬件接收到信号开始,到有对应的 View 消费结束会经过以下几步:
- 硬件层:接收到手指触摸的信号,然后将坐标与事件发送给操作系统
InputManager:Android 系统层的类,运行在一个单独的进程中;主要负责将原始事件发送到当前 APP 中InputChannel:Android 系统与 APP 沟通的桥梁,InputManager就是通过它把事件发送给 APP 的ViewRootImpl.WindowInputEventReceiver:运行在我们程序的主线程中,负责接受InputChannel中的事件ViewRootImpl.deliverInputEvent:负责分发不同事件到各自的 handler 手上ViewRootImpl.processPointerEvent:负责处理触摸事件的 handler,他会将事件包装成 MotionEvent 然后将其派发到视图树的根节点上mView.dispatchTouchEvent:mView 是我们插入视图树的根 View(在 RN 的例子中是ReactRootView);当我们的根节点接收到这个事件之后,他会判断自己是否要处理这个事件,如果不处理则会向他下面的节点进行派发(有点类似 JS 的事件捕获的过程,但不完全一样)
从第七步开始,事件终于进入了我们能够用程序控制的范围了,接下来我们用一个例子讲解剩余的事件派发逻辑:
jsx
ViewGroup (root)
├─ ViewA
│ └─ ViewC <-- finger is here
└─ ViewB
假设当前我们有一个占据全屏的 ViewGroup,其内部有两个互不重叠的元素 ViewA 与 ViewB,ViewA 元素内部还有一个 ViewC 元素
用户的触摸事件发生在 ViewC 的范围内
接下来我们需要分成两种情况讨论:ViewC 消费了触摸事件、没有元素消费触摸事件
ViewC 消费触摸事件
在这种情况下,当 ACTION_DOWN 事件被触发时,以下方法会被递归调用:
js
ViewGroup.dispatchTouchEvent(DOWN)
→ ViewA.dispatchTouchEvent(DOWN)
→ ViewC.dispatchTouchEvent(DOWN) // return true => ViewC becomes the target of the touch
在 dispatchTouchEvent 中,主要发生了三件事:
- 调用 onInterceptTouchEvent 方法判断是否该事件需要被当前元素拦截(在这个例子里只有 ViewC 返回了 true)
- 如果 onInterceptTouchEvent 返回了 false,则执行 hit-test 根据当前事件位置判断应该调用哪一个子元素的
dispatchTouchEvent方法(原则是在当前位置上层级最高的子元素优先被调用) - 如果有元素 onInterceptTouchEvent 返回了 true,则:
- 调用该元素的 onTouchEvent 方法处理该事件
- 父元素标记当前元素为该触摸事件的处理元素(这个是为了减少后续
ACTION_UP事件的 hit-test 开销)将当前的处理元素标记到mFirstTouchTarget中
当 ACTION_UP 事件被触发时,则有:
js
ViewGroup.dispatchTouchEvent(UP)
→ ViewA.dispatchTouchEvent(UP)
→ ViewC.dispatchTouchEvent(UP) // call onTouchEvent(UP) => perform onClick
由于在 ACTION_DOWN 的时候标记了处理元素,所以这次每一个元素只需要找到各自标记的 mFirstTouchTarget 然后直接调用就好
在这个例子中最后会直接触发 ViewC.dispatchTouchEvent,执行合成之后的高级事件
没有元素消费触摸事件
如果没有元素要消费这个事件的话,递归调用就会变成:
js
ViewGroup.dispatchTouchEvent(DOWN)
→ ViewA.dispatchTouchEvent(DOWN)
→ ViewC.dispatchTouchEvent(DOWN) // returns false (not clickable)
← back to ViewA → ViewA.onTouchEvent(DOWN) → false
← back to ViewGroup → ViewGroup.onTouchEvent(DOWN) → false
onTouchEvent 除了在当前元素的 onInterceptTouchEvent 返回 true 时会执行,在子元素不消费这个事件的时候,也会在归的时候被调用(可以想象成事件处理机制对该元素说:嘿,你的子元素都不处理这个事件,你要不要处理下兜个底呀?)
如果此时 ViewA 跟 ViewGroup 都返回了 false,则代表没有元素愿意消费这个事件,自然每个元素的 mFirstTouchTarget 就都是空的了
在这种情况下,如果 ACTION_UP 事件被触发时,当事件进入到 ViewGroup.dispatchTouchEvent 会很尴尬的发现,mFirstTouchTarget 为空,没有元素愿意消费自己,此时它会最后调用一下当前元素的 onTouchEvent 然后发现返回了 false,事件终结
IOS 的事件处理机制
IOS 从硬件接受到信号到处理事件一般会经过以下几步:
- 当屏幕检测到手指触摸,会将当前事件发送到当前应用主线程的运行循环(main run loop)中
- UIKit 会将事件封装成一个
UIEvent,一个UIEvent中会包含一个或多个UITouch对象(内部包含此次事件的信息) - IOS 应用会从
UIWindow(通常只会有一个,负责承载当前页面所有 UIView) 开始进行 hit-testing 流程 - Hit-testing 会根据一系列规则找到需要处理该事件的 View
UIWindow基于该 View 开始一系列的事件处理机制
在正式进入事件处理机制之前,我们先来看看原生 IOS 应用可以如何消费事件:
UIResponder
UIResponder 是参与 IOS 事件传递系统的一个基类,UIView 继承了这个基类
在 UIResponder 中提供了一系列低阶事件处理回调
由于 UIView 继承了它,所以在 UIView 中我们可以通过重载 override 这些回调来执行我们事件处理逻辑
比如:
objc
@interface DraggableView : UIView
@end
@implementation DraggableView {
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// override with custom logic
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// override with custom logic
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// override with custom logic
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// override with custom logic
}
@end
在这个例子中我们通过重载了 touchesBegan, touchesMoved, touchesEnded, touchesCancelled 方法定制了触摸事件发生过程的处理逻辑
默认情况下,命中 Hit-testing 的 View 重载的 touchesXXX 事件会被优先执行,如果当前 View 没有重载事件或者在重载中调用了 super touchesXXX:withEvent: 则该事件将会继续被其父节点消费
UIControl
UIControl 是 UIView 的子类(所以它也继承了 UIResponder)
与 UIResponder 不同的是,UIControl 内部维护了一个状态机 state machine,用来将多个低阶的事件封装成高阶的语义化事件
从应用开发者视角来看,UIControl 为 Button、Switches 等元素提供了一套语义化事件的消费方式,简化开发者处理常见事件的流程
这套方式被称之为 target-action,以下是例子:
objc
// 创建一个 button
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
// 在这个 button 中注册了一个针对 UIControlEventTouchUpInside 事件的监听,如果该事件被触发则执行 tapped 方法
[button addTarget:self action:@selector(tapped:) forControlEvents:UIControlEventTouchUpInside];
- (void)tapped:(UIButton *)sender {
// handle button press
}
相比 UIResponder,UIControl 弱化了开发者对 touch 低阶事件的感知,只需要处理高阶的语义化事件即可
UIGestureRecognizer
UIGestureRecognizer 是 IOS 事件处理中一个 "另类" 的处理机制
它不同于 UIResponder 与 UIControl 这种基于 View 的事件响应链机制,它是一个独立的基于 "手势" 的事件响应机制
我们先来看看它是怎么用的,再来聊聊它的机制:
objc
// 创建一个识别 tap 手势的 GestureRecognizer;当这个手势被成功识别调用 didTap 方法处理手势
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTap:)];
tap.numberOfTapsRequired = 1;
// 在当前 view 上注册这个 GestureRecognizer
[view addGestureRecognizer:tap];
- (void)didTap:(UITapGestureRecognizer *)gr {
if (gr.state == UIGestureRecognizerStateRecognized) {
// handle tap
}
}
通过上面代码,我们可以总结出两点结论:
- GestureRecognizer 需要由 view 主动添加(第 5 行的
addGestureRecognizer),而不像UIResponder自动继承 - GestureRecognizer 内部主要由状态 state 驱动
我们来看看 Event Handling Guide for iOS 中是如何描述 IOS 默认的事件传递机制的:

In the simple case, when a touch occurs, the touch object is passed from the UIApplication object to the UIWindow object. Then, the window first sends touches to any gesture recognizers attached the view where the touches occurred (or to that view's superviews), before it passes the touch to the view object itself.
(from Event Handling Guide for iOS page 25)
简单翻译:当一个触摸事件被触发的时候,会先从 UIApplication(对应图中的 APP) 传递到 UIWindow(对应图中的 Window),然后会优先传递给 view 上附着的 GestureRecognizer,其次才是传递给 view 触发 UIResponder 的处理方法
了解了 GestureRecognizer 的机制之后,我们来看看它内部做了哪些事情
首先在 GestureRecognizer 内部,也有跟 UIResponder 一样的 touchesBegan、touchesMoved 等回调来响应不同的事件
其次 GestureRecognizer 在此之上还维护了一套状态机,用来将不同的事件抽象成一个个手势的状态
让我们来举一个例子:
| 当前事件 | GestureRecognizer | tapGestureRecognizer.state | View |
|---|---|---|---|
| 1. 用户手指触摸屏幕 | touchesBegan | Possible | touchesBegan |
| 2. 用户手指在屏幕上动了 | touchesMoved | Possible | touchesMoved |
| 3. 用户手指离开屏幕 | touchesEnded | Recognized | touchesEnded |
我们把点击事件分成了三个步骤,分别对应到上面表格中的三个当前事件
- 当用户手指触摸屏幕时,首先 GestureRecognizer 内部的 touchesBegan 会被触发,它会将内部的 state 会维持 Possible(因为还没有识别到手势),然后 View 才会收到该事件并触发 View 的 touchesBegan
- 当用户手指在屏幕上动了时,情况与上述类似
- 当用户手指离开屏幕时,GestureRecognizer 的 touchesBegan 被触发,它确认了是一个点击事件,所以将内部的 state 切换成了 Recognized,然后 View 才会收到该事件并触发 View 的 touchesBegan
在 GestureRecognizer 的内部,将所有的手势分为两种类型:
- 离散型手势:比如点击事件的 UITapGestureRecognizer
- 持续型手势:比如长按事件的 UILongPressGestureRecorgnizer
每一种手势,都有一个内部的状态机来判断当前状态:
- 离散型手势:
Possible -> Recognized/Failed(Recognized 表示识别成功;Failed 表示识别失败) - 持续型手势:
Possible -> Begin -> [Changed] -> Ended/Cancel(Ended 表示识别成功;Cancel 表示识别失败)
开发者可以通过判断一个 GestureRecognizer 的状态,来执行不同的处理逻辑(参考上一个代码片段第 8 行)
UIGestureRecognizer 与 UIResponder
如果上文的 UIGestureRecognizer 把你彻底搞懵了的话,本小节是专门用来答疑解惑的
我们接下来会用一个例子来了解 IOS 中处理事件的全貌:
js
ViewA <- add TapGestureRecognizer
└─ ViewB
└─ ViewC <- override touchesXXX
假设我们有三个元素,ViewA 是 ViewB 的父元素、ViewB 是 ViewC 的父元素
用户点击了 ViewC 元素(Hit-testing 命中了 ViewC,对应到本章节开始的第 5 步)时:
UIWindow会把当前的 touch 事件发送到任何附着在当前 View(以及其所有父元素)的 GestureRecognizer 中(在我们的例子中,就是 ViewA 的 TapGestureRecognizer 会最先感知到事件发生)- 默认情况(GestureRecognizer.delaysTouchesBegan === false)下,
UIWindow会随后把 touch 事件发送给 ViewC,然后触发 touchesBegan 的重载逻辑(进入基于 View 的响应链) - 如果 TapGestureRecognizer 成功识别了该手势,它会触发自己的 action,调用用户注册的方法
- 与此同时,在默认情况(GestureRecognizer.cancelsTouchesInView === true)下,ViewC 会接收到当前事件的 cancel 终止当前事件处理
在这个过程中,有两个重点:
- 如果一个 View 被命中了,它所有父元素上附着的 GestureRecognizer 都会被 "告知"
- 当任何事件被触发时,GestureRecognizer 总是第一个被 "告知" 的
是不是很适配 RN 呢😆
RN 事件处理机制实现
在上面两个章节中,我们分别聊了原生的 IOS 与 Android 的事件处理机制
在这一个章节,我们要把这些机制带进 RN 的设计,看看 RN 是如何在不同的平台之上抽象出一套机制抹平平台差异的解决方案
为了方便理解,我们会以一个例子为切入点:
jsx
<Button
onPress={() => {
console.log('You tapped the button!');
}}
title="Press Me"
/>
这是一个基础的 button 组件,其中 onPress 属性注册了一个点击事件的回调方法、title 则定义了按钮要展示的文案
站在 RN 框架开发者的视角来看,我们想要将事件处理的逻辑尽可能的统一,从而抹平不同平台之间的差异
自然而然的,我们会需要区分出两个角色:
- 生产者:代表方是原生的平台。它们负责将用户事件尽可能全 、尽可能早 的包装好后发送给 JS 侧
- 消费者:代表方是 JS 代码。它负责接收事件 ,将事件语义化 ,最后触发事件回调
由于这套机制需要双方共同参与,我们接下来会先聊聊 RN 在 IOS 侧与 Android 侧分别做了什么,再来看看 JS 侧是如何处理双端传过来的数据的
Android 侧
在上文 Android 的事件处理机制 中我们聊到,Android 所有的事件都会通过根元素一层一层向下传递,直到找到一个愿意消费该事件的元素
所以,为了满足尽可能全以及尽可能早的把消息发送给 JS 侧,我们需要找到我们所能控制的最上层节点(在 Android 中,这个节点是 ReactRootView)
找到根节点以后,我们要开始骚操作了:重载 onInterceptTouchEvent 与 onTouchEvent
java
// in ReactRootView.java
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
dispatchJSTouchEvent(ev);
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
dispatchJSTouchEvent(ev);
super.onTouchEvent(ev);
// In case when there is no children interested in handling touch event, we return true from
// the root view in order to receive subsequent events related to that gesture
return true;
}
还记得我们在 Android 的事件处理机制 中说明的吗?当事件传递到对应的 View 时,会先调用
onInterceptTouchEvent判断当前 View 是否要拦截该事件,如果拦截了会直接调用当前 View 中的onTouchEvent方法消费该事件;否则会继续将该事件往下传递知道遇到愿意消费该事件的元素或者重新返回当前的 View 调用onTouchEvent方法兜底
在这里我们先重载了 onInterceptTouchEvent 方法,调用了 dispatchJSTouchEvent 方法把当前事件发送给 JS 侧 (这个方法我们本小节后面展开聊),在此之后它返回了一个 super.onInterceptTouchEvent(ev) (在没有特殊情况下它的值是 false,代表会继续将该事件往下传递)
其次我们还重载了 onTouchEvent 方法,并且返回了 true(代表如果下面的元素都没有消费这个事件,它会负责兜底)
比较有意思的是,onTouchEvent 中也调用了 dispatchJSTouchEvent 发送当前事件给 JS 侧,这不重复了吗?
答案是:如果所有的子元素都不消费这个事件的话,确实会重复,但是在大部份情况下子元素都会消费事件(我们后面会聊到)所以这里的 onTouchEvent 在绝大部分情况都不会被执行,只是一个兜底逻辑
Button 的例子
上面我们说到在 ReactRootView 中的 onInterceptTouchEvent 会返回 false 并将该消息传递下去直到找到真正的消费元素
接下来我们以本章节开头的 button 为例子,来看看在 Android 侧是谁来完成事件消费闭环的
先来看一段 Button 组件在 JS 侧是怎么被封装的吧:
jsx
// in Button.js
class Button extends React.Component<{...}> {
// ... 省略部份代码
render() {
// ... 省略部份代码
const Touchable =
Platform.OS === 'android' ? TouchableNativeFeedback : TouchableOpacity;
return (
<Touchable
// ... 省略部份代码
disabled={disabled}
onPress={onPress}>
<View style={buttonStyles}>
<Text style={textStyles} disabled={disabled}>
{formattedTitle}
</Text>
</View>
</Touchable>
);
}
}
可以看到 Button 组件在不同的原生平台下,分别由两个组件来实现:Android 中是 TouchableNativeFeedback;IOS 中是 TouchableOpacity
这两个组件封装了对不同原生平台差异的处理,在本小节我们先来看看 TouchableNativeFeedback(IOS 的 TouchableOpacity 可以看本文的 IOS 侧 部份):
js
// in TouchableNativeFeedback.android.js
const TouchableNativeFeedback = createReactClass({
// ... 省略部份代码
render: function() {
const child = React.Children.only(this.props.children);
const childProps = {
...child.props,
// ... 省略部份代码
}
// ... 省略部份代码
return React.cloneElement(child, childProps);
}
})
可以看到,TouchableNativeFeedback 的 render 函数最后返回了自己的 child(也就是上一个代码片段中的 View 元素)
这个 View 元素在经历了 RN 的布局绘制后,会被当成一个 ReactViewGroup 创建,所以我们来看看 ReactViewGroup 是如何处理事件的:
java
// in ReactViewGroup.java
public class ReactViewGroup extends ViewGroup implements
ReactInterceptingViewGroup, ReactClippingViewGroup, ReactPointerEventsView, ReactHitSlopView,
ReactZIndexedViewGroup {
// ... 省略部份代码
private PointerEvents mPointerEvents = PointerEvents.AUTO;
private @Nullable OnInterceptTouchEventListener mOnInterceptTouchEventListener;
// ... 省略部份代码
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mOnInterceptTouchEventListener != null &&
mOnInterceptTouchEventListener.onInterceptTouchEvent(this, ev)) {
return true;
}
// We intercept the touch event if the children are not supposed to receive it.
if (mPointerEvents == PointerEvents.NONE || mPointerEvents == PointerEvents.BOX_ONLY) {
return true;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
// We do not accept the touch event if this view is not supposed to receive it.
if (mPointerEvents == PointerEvents.NONE || mPointerEvents == PointerEvents.BOX_NONE) {
return false;
}
// The root view always assumes any view that was tapped wants the touch
// and sends the event to JS as such.
// We don't need to do bubbling in native (it's already happening in JS).
// For an explanation of bubbling and capturing, see
// http://javascript.info/tutorial/bubbling-and-capturing#capturing
return true;
}
代码有点长,我们重点看 onInterceptTouchEvent 以及 onTouchEvent 方法
在 onInterceptTouchEvent 中一共做了两次判断:
mOnInterceptTouchEventListener:这个是一个钩子,用来让 JS 动态决定是否要在这里拦截这个消息mPointerEvents:是一个属性,默认情况下是 AUTO,也是让 JS 决定是否要拦截这个消息,区别在于它是静态的
默认情况下,这两个判断都是 false,ReactViewGroup 会把这个事件继续传下去
关键在于 onTouchEvent 方法,在默认情况下,它会返回 true(也就是说,它会兜底成为该事件的消费方)
于是,ReactRootView 中的 dispatchJSTouchEvent 方法不会被触发,JS 侧也就不会接受到重复的事件了
dispatchJSTouchEvent
dispatchJSTouchEvent 的职责除了把事件从 Android 侧发送给 JS 侧,还需要把 Android 侧独有的事件封装成 JS 侧能够消费的事件
我们先来看看它的实现:
java
// in ReactRootView.java
public class ReactRootView extends SizeMonitoringFrameLayout
implements RootView, MeasureSpecProvider {
// ... 省略部份代码
private void dispatchJSTouchEvent(MotionEvent event) {
// ... 省略部份代码
ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
EventDispatcher eventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
// 处理触摸事件的方法
mJSTouchDispatcher.handleTouchEvent(event, eventDispatcher);
}
}
可以看到,它将触摸事件的处理委托给了 mJSTouchDispatcher 实例中的 handleTouchEvent 方法,以下是它的实现:
java
// in JSTouchDispatcher.java
public class JSTouchDispatcher {
// ... 省略部份代码
public void handleTouchEvent(MotionEvent ev, EventDispatcher eventDispatcher) {
// 判断当前事件类型,后续我们会根据不同的事件类型做不同的数据处理
int action = ev.getAction() & MotionEvent.ACTION_MASK;
// 处理 ACTION_DOWN 事件,标识当前用户手指放上屏幕了
if (action == MotionEvent.ACTION_DOWN) {
// 这里做了一个日志的报错,如果连续接收到两个 ACTION_DOWN 事件的时候就会触发
// mTargetTag 在下面会被赋值,代表处理此事件的目标组件/元素
if (mTargetTag != -1) {
FLog.e(
ReactConstants.TAG,
"Got DOWN touch before receiving UP or CANCEL from last gesture");
}
// 记录当前事件时间戳
mGestureStartTime = ev.getEventTime();
// RN 自己实现的 hit-test 逻辑,递归找到了应该消费的 View,并且返回了该 View 的 tag
// 这个 tag 是当 js 侧调用 createView 时传过来的 tag,唯一标识了一个 js 侧的元素
mTargetTag = findTargetTagAndSetCoordinates(ev);
// 把 ACTION_DOWN 事件包装后发给 js ce
eventDispatcher.dispatchEvent(
TouchEvent.obtain(
mTargetTag,
TouchEventType.START,
ev,
mGestureStartTime,
mTargetCoordinates[0],
mTargetCoordinates[1],
mTouchEventCoalescingKeyHelper));
// 以下的 else 省略了一些场景的处理逻辑,只重点放了点击事件相关的处理逻辑
} else if (action == MotionEvent.ACTION_UP) {
// 处理 ACTION_UP 事件,标识当前用户手指离开屏幕了
// 更新了 mTargetCoordinates,这个是用来表示当前用户手指位置
findTargetTagAndSetCoordinates(ev);
// 包装并发送事件给 js 侧
eventDispatcher.dispatchEvent(
TouchEvent.obtain(
mTargetTag,
TouchEventType.END,
ev,
mGestureStartTime,
mTargetCoordinates[0],
mTargetCoordinates[1],
mTouchEventCoalescingKeyHelper));
// 恢复成初始状态
mTargetTag = -1;
mGestureStartTime = TouchEvent.UNSET;
} else if (action == MotionEvent.ACTION_MOVE) {
// 处理 ACTION_MOVE 事件,标识当前用户手指在屏幕上的移动
// 更新了 mTargetCoordinates
findTargetTagAndSetCoordinates(ev);
// 包装并发送事件给 js 侧
eventDispatcher.dispatchEvent(
TouchEvent.obtain(
mTargetTag,
TouchEventType.MOVE,
ev,
mGestureStartTime,
mTargetCoordinates[0],
mTargetCoordinates[1],
mTouchEventCoalescingKeyHelper));
} else {
// 兜底逻辑
FLog.w(
ReactConstants.TAG,
"Warning : touch event was ignored. Action=" + action + " Target=" + mTargetTag);
}
}
}
代码部份看起来很多,但是其实只包含了三个部份:
- 对
ACTION_DOWN事件的处理 - 对
ACTION_UP事件的处理 - 对
ACTION_MOVE事件的处理
这三个事件的处理说白了就是把事件包装好后发送给 js 侧供 js 消费
至此,Android 侧的工作已完结撒花~🎉
IOS 侧
IOS 侧身为事件的另一个生产者,它的职责也是将事件尽可能早、尽可能快的包装好传递给 JS 侧
在上文 IOS 的事件处理机制 中,我们聊到了 UIGestureRecognizer 的两个特性:
- 如果一个 View 被命中了,它所有父元素上附着的 GestureRecognizer 都会被 "告知"
- 当任何事件被触发时,GestureRecognizer 总是第一个被 "告知" 的
根据这两点,我们可以在我们能控制的最上层节点(IOS 中这个节点是 RCTRootContentView)附着 UIGestureRecognizer,然后定制其中的 touchesBegan 等事件的回调,从而履行上述的职责
让我们来看看 RN 是如何实现的:
objc
// 第一步:把 UIGestureRecognizer 附着在 RCTRootContentView 上
// in RCTRootContentView.m
- (instancetype)initWithFrame:(CGRect)frame
bridge:(RCTBridge *)bridge
reactTag:(NSNumber *)reactTag
sizeFlexiblity:(RCTRootViewSizeFlexibility)sizeFlexibility
{
if ((self = [super initWithFrame:frame])) {
// ...省略部份代码
// 初始化 RCTTouchHandler 并取得其实例
_touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge];
// 调用 RCTTouchHandler 的 attachToView 方法(下面会有实现)
[_touchHandler attachToView:self];
// ...省略部份代码
}
return self;
}
// in RCTTouchHandler.m
- (void)attachToView:(UIView *)view
{
RCTAssert(self.view == nil, @"RCTTouchHandler already has attached view.");
// attachToView 方法的实现,本质上就是调用了父类的 addGestureRecognizer 把 UIGestureRecognizer 附着上了 RCTRootContentView
[view addGestureRecognizer:self];
}
上面的代码展示了 RN 是如何将 RCTTouchHandler 附着在 RCTRootContentView 节点上的,接下来我们来看看 RCTTouchHandler 是什么,以及做了什么事:
js
// 第二步:修改事件回调的实现
// in RCTTouchHandler.h
// ... 省略部份代码
// 从这里可以看到 RCTTouchHandler 继承了 UIGestureRecognizer
@interface RCTTouchHandler : UIGestureRecognizer
// in RCTTouchHandler.m
// 重载 UIGestureRecognizer 的 touchesBegan 方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 调用父类方法,保证 IOS 的事件系统同步到了这个事件(否则就被拦截在这里了)
[super touchesBegan:touches withEvent:event];
// ...省略部份代码
// 将 touchStart 事件包装后发送给 js 侧
[self _updateAndDispatchTouches:touches eventName:@"touchStart"];
// 保证 IOS Recognizer 状态机的正常流转
if (self.state == UIGestureRecognizerStatePossible) {
self.state = UIGestureRecognizerStateBegan;
} else if (self.state == UIGestureRecognizerStateBegan) {
self.state = UIGestureRecognizerStateChanged;
}
}
// 重载 UIGestureRecognizer 的 touchesMoved 方法
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 调用父类方法,保证 IOS 的事件系统同步到了这个事件(否则就被拦截在这里了)
[super touchesMoved:touches withEvent:event];
// 将 touchMove 事件包装后发送给 js 侧
[self _updateAndDispatchTouches:touches eventName:@"touchMove"];
// 保证 IOS Recognizer 状态机的正常流转
self.state = UIGestureRecognizerStateChanged;
}
// 重载 UIGestureRecognizer 的 touchesEnded 方法
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
// 将 touchEnd 事件包装后发送给 js 侧
[self _updateAndDispatchTouches:touches eventName:@"touchEnd"];
// 保证 IOS Recognizer 状态机的正常流转
if (RCTAllTouchesAreCancelledOrEnded(event.allTouches)) {
self.state = UIGestureRecognizerStateEnded;
} else if (RCTAnyTouchesChanged(event.allTouches)) {
self.state = UIGestureRecognizerStateChanged;
}
// ...省略部份代码
}
// 重载 UIGestureRecognizer 的 touchesCancelled 方法
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesCancelled:touches withEvent:event];
// 将 touchEnd 事件包装后发送给 js 侧
[self _updateAndDispatchTouches:touches eventName:@"touchCancel"];
// 保证 IOS Recognizer 状态机的正常流转
if (RCTAllTouchesAreCancelledOrEnded(event.allTouches)) {
self.state = UIGestureRecognizerStateCancelled;
} else if (RCTAnyTouchesChanged(event.allTouches)) {
self.state = UIGestureRecognizerStateChanged;
}
// ...省略部份代码
}
根据上面 RCTTouchHandler.m 的实现我们可以看到所有的事件回调都包含三个部份的内容:
- 调用父类的同名方法:这个是 IOS 系统要求的,因为 UIGestureRecognizer 还会负责协调不同的 Recognizer,所以我们必须跟它同步最新的消息
- 将当前事件打包后发送给 JS 侧
- 正常流转状态:在 UIGestureRecognizer 这个类中是没有状态流转的逻辑的,这些状态的变化都要仰赖子类的维护,而 UIKit 会根据这个状态来判断/执行一些逻辑,所以这个是必须实现的
最后还有一个小细节,前面我们在聊 UIGestureRecognizer 的状态的时候,我们说过点击属于离散型手势,它的特征就是手势的状态是 Possible -> Recognized/Failed
但是我们仔细观察 RCTTouchHandler 的实现,不难发现它的状态流转是 Possible -> Begin -> [Changed] -> Ended/Cancel
这是因为这个类的目的是处理所有的 Touch 事件,所以它必须向 JS 侧完整的传递所有事件,由 JS 侧来判断事件的类型
至此,IOS 侧的工作也已完结撒花~🎉
JS 侧
最后,我们来聊聊 JS 侧是如何处理双端发来的事件消息的
在 JS 侧,RN 对外定义了一个名为 RCTEventEmitter 的模块来接收客户端传来的事件,客户端可以通过 bridge 调用其中的 receiveTouches 方法来发送触摸事件给 JS 侧
当 JS 侧接收到了客户端发送过来的事件消息后,它有三个任务需要完成:
- 将原始的事件合成语义化事件
- 让语义化事件遵循事件捕获/事件冒泡机制到达目标节点
- 触发开发者注册的事件回调
为了了解 RN 时如何完成这三个任务,我们需要先来聊聊 React 的事件系统的核心:EventPluginHub
EventPluginHub

我们先来看图中深色字体的部份,EventPluginHub 做了三件事:
- 接收传过来的低级事件
- 把插件注入系统
- 接收插件传过来的合成事件,然后依次派发出去
可以看到 EventPluginHub 的职责其实非常简单,但是插件要考虑的事就很多了😆
但是在聊插件做了什么之前,我想先聊聊为什么要这么设计 React 的事件系统:
- 跨浏览器一致性:React 在设计之初就肩负跨浏览器的责任,所以它需要一个事件层来弭平不同浏览器之间的差异,合成事件 Synthetic event 也作为最终一致性的产物应运而生
- 跨平台能力抽象:从 React 跨到 RN,事件层接收到的事件也有显著区别,唯一不变的是事件处理的流程,所以把变化的部份插件化,根据场景注入插件处理
- 性能:不论是 React 或者 RN 都在尽可能靠近根元素的位置附着监听器拦截事件,不仅最大化减少了监听器的内存占用,也减少了管理多个监听器的复杂度
- 高扩展低耦合:插件的设计不仅让接入不同场景的成本更低;也可以让不同的插件区分不同的事件类型,增加插件内的耦合度
那么插件内部都在做什么呢?虽然每个插件实现都略有差异,但总体而言每个插件都有三个核心职责:
- 将不同类型的低级事件转换成合成事件
- 将合成事件通过捕获/冒泡找出负责处理该事件的组件 handler
- 将及时事件 On-Demond events 马上发送给 handler;将其他非及时事件 Deffered-Dispatch events 留给 EventPluginHub 处理
其中有两个非常有意思的概念:及时事件 On-Demond events 和 非及时事件 Deffered-Dispatch events
- 及时事件 On-Demond events 指的是那些在捕获/冒泡阶段遍历组件的时候就需要在插件中 实时发送的事件,如果这类事件没有实时发送,会阻碍后面事件的正常传递,比如 RN 中能在捕获阶段拦截事件的
onStartShouldSetResponderCapture(后文会有介绍) - 非及时事件 Deffered-Dispatch events 指的是在插件中没有发送,而是被收集起来在最后被 EventPluginHub 一起发送的事件,实际上大多数的事件都是此类事件,可以把及时事件看成是一种不得已而为之的 trade-off,比如 RN 中代表用户手指放下的事件
onResponderStart(后文会有介绍)
ResponderEventPlugin
了解了 EventPluginHub 以及插件的职责后,我们接下来要来深入聊聊 RN 场景下用到的插件了
根据上文的图片我们可以得知,RN 为 EventPluginHub 注入了两个插件:ResponderEventPlugin 和 ReactNativeBridgeEventPlugin
其中 ResponderEventPlugin 主要负责触摸相关的事件;而 ReactNativeBridgeEventPlugin 则负责其他由 Native 发送过来的事件
注意事项 1: 由于本文主要讲解触摸事件的处理,所以本章节只会讲解
ResponderEventPlugin的实现,实际上ReactNativeBridgeEventPlugin的实现可以看成ResponderEventPlugin的简化版,所以如果看懂了ResponderEventPlugin,自己看ReactNativeBridgeEventPlugin也能轻松理解注意事项 2: 在后文的代码阅读中,不需要太早关心具体的实现,我们只需要抓住上文插件的三个职责慢慢理解即可
注意事项 3: 后文代码是基于 React 版本 v16.5.2(该版本是 RN v0.57.0 的默认 React 版本) 由于 RN 中的对应代码是压缩过的,所以强烈建议去 React 的仓库阅读,我文件名也会留 React 仓库的文件名
首先我们先来看看 EventPluginHub 是如何暴露插件注入接口的:
js
// in EventPluginHub.js
/**
* Methods for injecting dependencies.
*/
export const injection = {
/**
* @param {array} InjectedEventPluginOrder
* @public
*/
injectEventPluginOrder,
/**
* @param {object} injectedNamesToPlugins Map from names to plugin modules.
*/
injectEventPluginsByName,
};
EventPluginHub 暴露了两个方法:injectEventPluginOrder 和 injectEventPluginsByName
injectEventPluginOrder代表插件的顺序,顺序越靠前的插件产生的事件会越早被 EventPluginHub 发送出去injectEventPluginsByName代表插件名称与插件实现的映射
接下来看看 RN 是怎么注入插件的:
js
// in ReactNativeInjectionShared.js
// 插入插件顺序
const ReactNativeEventPluginOrder = [
'ResponderEventPlugin',
'ReactNativeBridgeEventPlugin',
];
/**
* Inject module for resolving DOM hierarchy and plugin ordering.
*/
EventPluginHub.injection.injectEventPluginOrder(ReactNativeEventPluginOrder);
/**
* Some important event plugins included by default (without having to require
* them).
*/
EventPluginHub.injection.injectEventPluginsByName({
ResponderEventPlugin: ResponderEventPlugin,
ReactNativeBridgeEventPlugin: ReactNativeBridgeEventPlugin,
});
可以看到插件的顺序是先处理 ResponderEventPlugin 后处理 ReactNativeBridgeEventPlugin
插件准备就绪后,我们来看看事件是如何进入 EventPluginHub:
js
// in ReactNativeInjection.js
// RCTEventEmitter 模块的实现由 RN 提供
import RCTEventEmitter from 'RCTEventEmitter';
// 在 bridge 上注册了可以被 native 调用的模块
RCTEventEmitter.register(ReactNativeEventEmitter);
接下来看看 ReactNativeEventEmitter 模块做了什么
js
// in ReactNativeEventEmitter.js
// Native 发送普通事件(非触摸事件)用的方法
// 本质上就是把参数透传给 _receiveRootNodeIDEvent
export function receiveEvent(
rootNodeID: number,
topLevelType: TopLevelType,
nativeEventParam: AnyNativeEvent,
) {
_receiveRootNodeIDEvent(rootNodeID, topLevelType, nativeEventParam);
}
// Native 发送触摸事件用的方法
// 职责有 2:
// 1. 把 "有变化的事件" 找出来处理,忽略 "没有变化的事件"
// 2. 调用 _receiveRootNodeIDEvent 处理有变化的事件
export function receiveTouches(
// 事件类型,对应到 Native 的 touchStart, touchMove 等事件
eventTopLevelType: TopLevelType,
// 当前被触发的事件(如果有多个手指则有多个事件被触发)
touches: Array<Object>,
// 当前变化的事件下标
changedIndices: Array<number>,
) {
// 找出变化的触摸事件
const changedTouches =
eventTopLevelType === 'topTouchEnd' ||
eventTopLevelType === 'topTouchCancel'
? removeTouchesAtIndices(touches, changedIndices)
: touchSubsequence(touches, changedIndices);
for (let jj = 0; jj < changedTouches.length; jj++) {
const touch = changedTouches[jj];
// 构建一个符合 Dom event 标准的 touch 事件,这一步可以省去一个构建
touch.changedTouches = changedTouches;
touch.touches = touches;
const nativeEvent = touch;
let rootNodeID = null;
// 消费该事件的目标组件(Native 侧计算结果)
const target = nativeEvent.target;
if (target !== null && target !== undefined) {
if (target < 1) {
} else {
rootNodeID = target;
}
}
_receiveRootNodeIDEvent(rootNodeID, eventTopLevelType, nativeEvent);
}
}
// 负责调用 EventPluginHub 内部方法处理事件
export function _receiveRootNodeIDEvent(
rootNodeID: number,
topLevelType: TopLevelType,
nativeEventParam: ?AnyNativeEvent,
) {
const nativeEvent = nativeEventParam || EMPTY_NATIVE_EVENT;
const inst = getInstanceFromNode(rootNodeID);
// 将内部方法打包成一次更新
batchedUpdates(function() {
// 调用 EventPluginHub 内部方法,进入 EventPluginHu 逻辑
runExtractedEventsInBatch(
topLevelType,
inst,
nativeEvent,
nativeEvent.target,
);
});
}
总结:当 receiveTouches 接收到 Native 的事件后,它会挑选出有变化的部份委托 EventPluginHub 处理
所以接下来我们要正式开始运行 EventPluginHub 了:
js
// in EventPluginHub.js
export function runExtractedEventsInBatch(
topLevelType: TopLevelType,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: EventTarget,
) {
// 这个方法是 EventPluginHub 插件系统的核心
// EventPluginHub 要求每一个插件都要实现各自的 extractEvents 方法
// 目的就是履行上述插件的三个职责
const events = extractEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
);
// 这里的 events 就是我上文说的 Deffered-Dispatch events
// 这些事件会在 runEventsInBatch 中统一被发送给各自的 handler
runEventsInBatch(events, false);
}
// EventPluginHub 中的 extractEvents
// 它会按照顺序遍历插件,并且执行插件中实现的 extractEvents 方法
function extractEvents(
topLevelType: TopLevelType,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: EventTarget,
): Array<ReactSyntheticEvent> | ReactSyntheticEvent | null {
let events = null;
for (let i = 0; i < plugins.length; i++) {
// Not every plugin in the ordering may be loaded at runtime.
const possiblePlugin: PluginModule<AnyNativeEvent> = plugins[i];
if (possiblePlugin) {
const extractedEvents = possiblePlugin.extractEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
);
if (extractedEvents) {
events = accumulateInto(events, extractedEvents);
}
}
}
return events;
}
接下来我们看看 ResponderEventPlugin 是如何实现 extractEvents 的吧~
js
// in ResponderEventPlugin.js
const ResponderEventPlugin = {
// ... 省略部份代码
extractEvents: function(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
) {
// 修改当前处于激活状态的 touch events 数量
if (isStartish(topLevelType)) {
trackedTouchCount += 1;
} else if (isEndish(topLevelType)) {
if (trackedTouchCount >= 0) {
trackedTouchCount -= 1;
} else {
console.error(
'Ended a touch event which was not counted in `trackedTouchCount`.',
);
return null;
}
}
// 把当前的事件记录/更新到 ResponderTouchHistoryStore 中
// 主要是为了多点触控时计算几何信息用的(感兴趣可以看看 RN Libraries/Interaction/TouchHistoryMath.js 的实现)
ResponderTouchHistoryStore.recordTouchTrack(topLevelType, nativeEvent);
// canTriggerTransfer:判断是否需要重新计算事件的 handler 组件
// setResponderAndExtractTransfer:通过事件捕获/冒泡流程得到新的 handler 组件并更新 responderInst(指向 handler 组件的实例)
// 这两个方法在下文会有详细说明
// 这里声明的 extracted 就是我们上问说的 Deffered-Dispatch events
let extracted = canTriggerTransfer(topLevelType, targetInst, nativeEvent)
? setResponderAndExtractTransfer(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
)
: null;
// 根据事件类型取得不同的 eventTypes(枚举记录在本代码片段后面)
// eventTypes 记录了不同事件注册事件回调的组件属性
const isResponderTouchStart = responderInst && isStartish(topLevelType);
const isResponderTouchMove = responderInst && isMoveish(topLevelType);
const isResponderTouchEnd = responderInst && isEndish(topLevelType);
const incrementalTouch = isResponderTouchStart
? eventTypes.responderStart
: isResponderTouchMove
? eventTypes.responderMove
: isResponderTouchEnd
? eventTypes.responderEnd
: null;
if (incrementalTouch) {
// 根据 eventTypes 构建 SyntheticEvent
// SyntheticEvent 中有一个有趣的设计:eventPool
// eventPool 用来保存当前 SyntheticEvent 的多个实例
// 如果有事件生命周期完结了,eventPool 不会马上销毁这个实例,而是会用新的事件覆盖该实例以减少性能开销
// 感兴趣可以看看:SyntheticEvent.js
const gesture = ResponderSyntheticEvent.getPooled(
incrementalTouch,
responderInst,
nativeEvent,
nativeEventTarget,
);
// 记录 touchHistory
gesture.touchHistory = ResponderTouchHistoryStore.touchHistory;
// 维护事件的 handler 以及 listener 回调
accumulateDirectDispatches(gesture);
// 把处理好的事件加入到 Deffered-Dispatch events 的数组中
extracted = accumulate(extracted, gesture);
}
// 处理 touchCancel 事件或者当前 handler 没有任何事件的情况
const isResponderTerminate =
responderInst && topLevelType === TOP_TOUCH_CANCEL;
const isResponderRelease =
responderInst &&
!isResponderTerminate &&
isEndish(topLevelType) &&
noResponderTouches(nativeEvent);
const finalTouch = isResponderTerminate
? eventTypes.responderTerminate
: isResponderRelease
? eventTypes.responderRelease
: null;
// 如果有 touchCancel 事件或者 handler release 了,构建其事件到 Deffered-Dispatch events 的数组中
if (finalTouch) {
const finalEvent = ResponderSyntheticEvent.getPooled(
finalTouch,
responderInst,
nativeEvent,
nativeEventTarget,
);
finalEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
accumulateDirectDispatches(finalEvent);
extracted = accumulate(extracted, finalEvent);
changeResponder(null);
}
// 返回所有的 Deffered-Dispatch events
return extracted;
},
}
我们可以看到 ResponderEventPlugin 的 extractEvents 目的就是维护不同情况下,需要给 handler 的各种事件回调
其中有一个寻找 handler 的核心方法 setResponderAndExtractTransfer 下面我们来重点揭秘一下:
js
// in ResponderEventPlugin.js
/**
* 主要职责有 2:
* 1. 通过捕获/冒泡寻找 handler 组件,并且更新 handler(也就是代码中的 responderInst)
* 2. 收集过程中产生的 Deffered-Dispatch events 并返回
*/
function setResponderAndExtractTransfer(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
) {
// 根据当前事件选择合适的 eventTypes
const shouldSetEventType = isStartish(topLevelType)
? eventTypes.startShouldSetResponder
: isMoveish(topLevelType)
? eventTypes.moveShouldSetResponder
: topLevelType === TOP_SELECTION_CHANGE
? eventTypes.selectionChangeShouldSetResponder
: eventTypes.scrollShouldSetResponder;
// 判断事件冒泡应该从哪一个组件开始
// 如果当前没有 handler,则我们以当前事件的 target 为开始点
// 如果当前已经有了 handler,我们需要找到当前 handler 与 target 的最小共同祖先
const bubbleShouldSetFrom = !responderInst
? targetInst
: getLowestCommonAncestor(responderInst, targetInst);
// 判断事件捕获/冒泡时是否应该跳过目标组件
// 下面我们会说到,RN 寻找 handler 时,会经过三个步骤:
// 1. 捕获阶段:找到 bubbleShouldSetFrom 最高的祖先组件,然后开始往下遍历
// 2. 目标阶段:遍历到 bubbleShouldSetFrom 组件
// 3. 冒泡阶段:从 bubbleShouldSetFrom 往回遍历到开始的祖先组件
// 这三个阶段是顺序执行,一旦遇到任意一个组件声明了想要成为 handler,整个流程就会停止
// 如果当前 bubbleShouldSetFrom 就是 handler(responderInst) 到第二阶段的时候就必定会停止
// 所以这个时候需要跳过目标阶段,直接从捕获阶段跳到冒泡阶段
const skipOverBubbleShouldSetFrom = bubbleShouldSetFrom === responderInst;
const shouldSetEvent = ResponderSyntheticEvent.getPooled(
shouldSetEventType,
bubbleShouldSetFrom,
nativeEvent,
nativeEventTarget,
);
shouldSetEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
if (skipOverBubbleShouldSetFrom) {
// 对应到我们上面说的跳过目标阶段的遍历
accumulateTwoPhaseDispatchesSkipTarget(shouldSetEvent);
} else {
// 对应到我们上面说的三个阶段的遍历
accumulateTwoPhaseDispatches(shouldSetEvent);
}
// 执行遍历并找到想要成为 handler 的组件
// 这个遍历过程发送的事件就是 On-Demond events
const wantsResponderInst = executeDispatchesInOrderStopAtTrue(shouldSetEvent);
if (!shouldSetEvent.isPersistent()) {
shouldSetEvent.constructor.release(shouldSetEvent);
}
if (!wantsResponderInst || wantsResponderInst === responderInst) {
return null;
}
// 接下来我们要处理更新 handler 的逻辑了
let extracted;
const grantEvent = ResponderSyntheticEvent.getPooled(
eventTypes.responderGrant,
wantsResponderInst,
nativeEvent,
nativeEventTarget,
);
grantEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
accumulateDirectDispatches(grantEvent);
// 发送事件给新的 handler 告知其成为了新的 handler
// 当一个 handler 被选中的时候,它有机会可以阻止其他原生组件成为新的 responder(仅对 Android 有用)
// 声明方法为在 onResponderGrant 回调中返回 true
const blockHostResponder = executeDirectDispatch(grantEvent) === true;
if (responderInst) {
// 如果当前有 handler,要更新 handler 之前我们需要征求原来 handler 的 "意见"
const terminationRequestEvent = ResponderSyntheticEvent.getPooled(
eventTypes.responderTerminationRequest,
responderInst,
nativeEvent,
nativeEventTarget,
);
terminationRequestEvent.touchHistory =
ResponderTouchHistoryStore.touchHistory;
accumulateDirectDispatches(terminationRequestEvent);
// 如果原来的 handler 同意了,为 true
// 否则为 false
const shouldSwitch =
!hasDispatches(terminationRequestEvent) ||
executeDirectDispatch(terminationRequestEvent);
if (!terminationRequestEvent.isPersistent()) {
terminationRequestEvent.constructor.release(terminationRequestEvent);
}
if (shouldSwitch) {
// 如果确定要更换 handler,要给原来 handler 发送 responderTerminate 消息
const terminateEvent = ResponderSyntheticEvent.getPooled(
eventTypes.responderTerminate,
responderInst,
nativeEvent,
nativeEventTarget,
);
terminateEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
accumulateDirectDispatches(terminateEvent);
extracted = accumulate(extracted, [grantEvent, terminateEvent]);
// 更新 handler
changeResponder(wantsResponderInst, blockHostResponder);
} else {
// 否则需要发送 responderReject 消息
const rejectEvent = ResponderSyntheticEvent.getPooled(
eventTypes.responderReject,
wantsResponderInst,
nativeEvent,
nativeEventTarget,
);
rejectEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
accumulateDirectDispatches(rejectEvent);
extracted = accumulate(extracted, rejectEvent);
}
} else {
// 如果当前没有有 handler,直接更新
extracted = accumulate(extracted, grantEvent);
changeResponder(wantsResponderInst, blockHostResponder);
}
// 返回 Deffered-Dispatch events
return extracted;
}
至此,我们介绍完了 ResponderEventPlugin 的实现
最后我会放上所有 eventTypes 的介绍,以及不同 eventTypes 流转过程:
js
// eventTypes 记录了不同情况下,会触发的组件事件回调
const eventTypes = {
/**
* 这个部份的事件都会有捕获/冒泡的阶段
*/
// 在 touchStart 阶段,判断该组件都否该在捕获/冒泡阶段拦截消息并且指定自身为 handler
startShouldSetResponder: {
phasedRegistrationNames: {
bubbled: 'onStartShouldSetResponder',
captured: 'onStartShouldSetResponderCapture',
},
dependencies: startDependencies,
},
// 在 scroll 事件中,判断该组件都否该在捕获/冒泡阶段拦截消息并且指定自身为 handler
scrollShouldSetResponder: {
phasedRegistrationNames: {
bubbled: 'onScrollShouldSetResponder',
captured: 'onScrollShouldSetResponderCapture',
},
dependencies: [TOP_SCROLL],
},
// 在文字选择事件中,判断该组件都否该在捕获/冒泡阶段拦截消息并且指定自身为 handler
selectionChangeShouldSetResponder: {
phasedRegistrationNames: {
bubbled: 'onSelectionChangeShouldSetResponder',
captured: 'onSelectionChangeShouldSetResponderCapture',
},
dependencies: [TOP_SELECTION_CHANGE],
},
// 在 touchMove 阶段,判断该组件都否该在捕获/冒泡阶段拦截消息并且指定自身为 handler
moveShouldSetResponder: {
phasedRegistrationNames: {
bubbled: 'onMoveShouldSetResponder',
captured: 'onMoveShouldSetResponderCapture',
},
dependencies: moveDependencies,
},
/**
* 这个部份的事件会直接传送到 handler 组件上,不会冒泡
*/
// touchStart 事件的回调
responderStart: {
registrationName: 'onResponderStart',
dependencies: startDependencies,
},
// touchMove 事件的回调
responderMove: {
registrationName: 'onResponderMove',
dependencies: moveDependencies,
},
// touchEnd 事件的回调
responderEnd: {
registrationName: 'onResponderEnd',
dependencies: endDependencies,
},
// 当前 handler 没有任何事件时触发的回调
responderRelease: {
registrationName: 'onResponderRelease',
dependencies: endDependencies,
},
// touchCancel 事件的回调
responderTerminationRequest: {
registrationName: 'onResponderTerminationRequest',
dependencies: [],
},
// 当前组件获得 handler 的回调
responderGrant: {
registrationName: 'onResponderGrant',
dependencies: [],
},
// 原来的 handler 组件拒绝释放 handler 权限的回调
responderReject: {
registrationName: 'onResponderReject',
dependencies: [],
},
// 原来的 handler 释放 handler 权限的回调
responderTerminate: {
registrationName: 'onResponderTerminate',
dependencies: [],
},
};
流转过程:
js
// in ResponderEventPlugin.js
/* Negotiation Performed
+-----------------------+
/ \
Process low level events to + Current Responder + wantsResponderID
determine who to perform negot-| (if any exists at all) |
iation/transition | Otherwise just pass through|
-------------------------------+----------------------------+------------------+
Bubble to find first ID | |
to return true:wantsResponderID| |
| |
+-------------+ | |
| onTouchStart| | |
+------+------+ none | |
| return| |
+-----------v-------------+true| +------------------------+ |
|onStartShouldSetResponder|----->|onResponderStart (cur) |<-----------+
+-----------+-------------+ | +------------------------+ | |
| | | +--------+-------+
| returned true for| false:REJECT +-------->|onResponderReject
| wantsResponderID | | | +----------------+
| (now attempt | +------------------+-----+ |
| handoff) | | onResponder | |
+------------------->| TerminationRequest| |
| +------------------+-----+ |
| | | +----------------+
| true:GRANT +-------->|onResponderGrant|
| | +--------+-------+
| +------------------------+ | |
| | onResponderTerminate |<-----------+
| +------------------+-----+ |
| | | +----------------+
| +-------->|onResponderStart|
| | +----------------+
Bubble to find first ID | |
to return true:wantsResponderID| |
| |
+-------------+ | |
| onTouchMove | | |
+------+------+ none | |
| return| |
+-----------v-------------+true| +------------------------+ |
|onMoveShouldSetResponder |----->|onResponderMove (cur) |<-----------+
+-----------+-------------+ | +------------------------+ | |
| | | +--------+-------+
| returned true for| false:REJECT +-------->|onResponderRejec|
| wantsResponderID | | | +----------------+
| (now attempt | +------------------+-----+ |
| handoff) | | onResponder | |
+------------------->| TerminationRequest| |
| +------------------+-----+ |
| | | +----------------+
| true:GRANT +-------->|onResponderGrant|
| | +--------+-------+
| +------------------------+ | |
| | onResponderTerminate |<-----------+
| +------------------+-----+ |
| | | +----------------+
| +-------->|onResponderMove |
| | +----------------+
| |
| |
Some active touch started| |
inside current responder | +------------------------+ |
+------------------------->| onResponderEnd | |
| | +------------------------+ |
+---+---------+ | |
| onTouchEnd | | |
+---+---------+ | |
| | +------------------------+ |
+------------------------->| onResponderEnd | |
No active touches started| +-----------+------------+ |
inside current responder | | |
| v |
| +------------------------+ |
| | onResponderRelease | |
| +------------------------+ |
| |
+ + */
至此,React 的事件处理机制 EventPluginHub 以及 RN 的事件插件 ResponderEventPlugin 都讲完了
下一个章节是本文的最后一个章节,会介绍 RN 是如何基于这个机制实现 Button 组件
Button 组件
本章节来一起聊聊 RN 触摸事件处理的最后一块拼图:触发开发者注册的事件回调
有细心的读者可能会发现,我们上一章节说的 eventTypes 中是没有大家在开发中常用到的 onPress 的
那么 onPress 是怎么被触发的呢?究其原因我们需要来看看 RN 是如何实现 Button 组件的(了解了这个以后,你也能自己定制一个 Button)
RN 的 Button 组件有这么个结构:
js
+----------------------------------+
| Touchable.js |
+-----------------+----------------+
| |
Mixin | |Mixin
v v
+----------------------------------+ +--------------------------------+
| TouchableNativeFeedback.android.js | | TouchableOpacity.js |
+-----------------+------------------+ +----------------+---------------+
| |
Implement | Implement |
v v
+----------------------------------+
| Button.js |
+----------------------------------+
我们从下到上一一解释:
Button.js:这个就是 RN 对外暴露的 Button 组件,内部有 disabled、onPress 等属性TouchableNativeFeedback.android.js:Android 中 Button 的默认内部实现,主要目的是为了实现 Android 的水波纹效果,其次是作为事件的 handler 注册 onResponderGrant,onResponderMove 等属性回调TouchableOpacity.js:IOS 中 Button 的默认内部实现,主要目的是实现点击后透明度变化的效果,其次是作为事件的 handler 注册 onResponderGrant,onResponderMove 等属性回调Touchable.js:最核心 的事件消费引擎,内部实现了一个状态机,会根据不同的状态决定是否触发 onPress 等回调;它通过 Mixin 的方式将自己混入TouchableNativeFeedback.android.js与TouchableOpacity.js之中
要想了解 Touchable.js 内部做了什么,我们要先了解一下想要处理好 "点击" 这个交互,需要先做那些事
以下选自 RN 代码 Touchable.js 中的注释:
====================== Touchable Tutorial ===============================
The
Touchablemixin helps you handle the "press" interaction. It analyzes the geometry of elements, and observes when another responder (scroll view etc) has stolen the touch lock. It notifies your component when it should give feedback to the user. (bouncing/highlighting/unhighlighting).
- When a touch was activated (typically you highlight)
- When a touch was deactivated (typically you unhighlight)
- When a touch was "pressed" - a touch ended while still within the geometry of the element, and no other element (like scroller) has "stolen" touch lock ("responder") (Typically you bounce the element).
A good tap interaction isn't as simple as you might think. There should be a slight delay before showing a highlight when starting a touch. If a subsequent touch move exceeds the boundary of the element, it should unhighlight, but if that same touch is brought back within the boundary, it should rehighlight again. A touch can move in and out of that boundary several times, each time toggling highlighting, but a "press" is only triggered if that touch ends while within the element's boundary and no scroller (or anything else) has stolen the lock on touches.
简单来说,Touchable 需要处理:
- 受点击元素的几何位置信息
- 判断当前点击事件是否被其他 handler 抢占
- 在需要的时候给用户反馈(比如点击时高亮、离开时取消高亮等)
此外,点击交互还有一些关于几何位置的细节需要处理(以下选自 RN 代码 Touchable.js 中的注释):
js
========== Geometry =========
`Touchable` only assumes that there exists a `HitRect` node. The `PressRect`
is an abstract box that is extended beyond the `HitRect`.
+--------------------------+
| | - "Start" events in `HitRect` cause `HitRect`
| +--------------------+ | to become the responder.
| | +--------------+ | | - `HitRect` is typically expanded around
| | | | | | the `VisualRect`, but shifted downward.
| | | VisualRect | | | - After pressing down, after some delay,
| | | | | | and before letting up, the Visual React
| | +--------------+ | | will become "active". This makes it eligible
| | HitRect | | for being highlighted (so long as the
| +--------------------+ | press remains in the `PressRect`).
| PressRect o |
+----------------------|---+
Out Region |
+-----+ This gap between the `HitRect` and
`PressRect` allows a touch to move far away
from the original hit rect, and remain
highlighted, and eligible for a "Press".
Customize this via
`touchableGetPressRectOffset()`.
简单总结一下:
-
图中的 VisualRect 是我们看到的 Button 组件的视图范围,但是实际上用户可点击的范围其实是 HitRect 以内的部份
-
如果用户在 HitRect 范围内按下了手指,在 PressRect 的范围内松开,我们就认为这是一个有效的点击;否则不构成点击行为
-
如果用户在 HitRect 范围内按下了手指,在一些 delay 过后,按钮会进入 "激活状态",在此状态下的按钮会给用户一些反馈(比如高亮)
-
上面我们说到当用户按下按钮的时候我们需要给用户一些反馈,这个反馈的范围就是在 PressRect 之内,以下列举几个情况:
- 如果用户在 HitRect 之内按下手指,移动到 PressRect 的区域,此时按钮的反馈会持续存在;
- 如果用户移动到了 Out Region,则反馈会消失;
- 如果用户调皮在没有松开手的情况下移回来 PressRect,这个反馈会重新出现;
- 如果用户在一直没有松手的情况反复执行 2,3 种情况,反馈会一直出现/消失
为了实现上述效果,Touchable 需要一个状态机来处理不同状态的流转(以下选自 RN 代码 Touchable.js 中的注释):
js
======= 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 -
当用户点击按钮且按钮获得 handler 之后,状态会马上变成
RESPONDER_INACTIVE_PRESS_IN -
状态变成
RESPONDER_INACTIVE_PRESS_IN后持续保持DELAY毫秒后,状态变成RESPONDER_ACTIVE_PRESS_IN -
状态变成
RESPONDER_ACTIVE_PRESS_IN后等待T毫秒后,状态会变成RESPONDER_ACTIVE_LONG_PRESS_IN -
在状态 2~4 下如果用户手指移到 PressRect 之外,则状态会变成对应的 OUT 状态
有了状态机,Touchable 就可以在接收到 ResponderEventPlugin 的 eventTypes 后根据状态流转并且触发相对应的属性回调了
受限篇幅这里就不再展开 Touchable 的代码一行行看了,有兴趣的读者可以自行查看
总结
最后总结下,本文从 Android 与 IOS 的事件处理机制开始,聊到 RN 是如何结合 React 抽象出一套统一事件系统,最后还介绍了 JS 是如何利用这套系统搭建出 Button 组件
有了这些背景知识可能并不一定能让我们写出更好的 RN 代码,但是我们一定可以在出现问题后能够更快的抓住问题本质,也能够以更加全面的视角来看待不同的问题
到这,RN 的触摸事件处理篇结束了,但这并不代表这就是 RN 事件处理的全貌。事实上,看完本文之后可能我们的问题更多了
剩下的部分就靠各位读者一起交流学习吧,这也是学习有意思的地方!