您的位置:首页 > 移动开发 > Android开发

Android的事件分发源码分析,告别事件冲突。

2015-12-31 21:48 603 查看

一、前言

android的事件分发,大多数人都是似懂非懂,很多时候就卡在事件冲突这一步。比如在按钮上不能滑动出侧边栏,比如说ViewPager和banner冲突。我之前也是这样,然后狠下心去看了一遍源码,并且看了很多大神的博客,然后以我自身的理解配合源码来查看一个事件的传递过程。源码用的是API-8的,因为版本越高,健壮性越好,代码阅读性越差。

因为篇幅比较长,所以更底层的代码我也不准备写了,日后有机会再研究。在看博客之前,我们需要先来了解一些事件分发的基本流程,然后再一步步的深入去研究。

这其中有三个关键方法,首先我们先来理解几个方法的方法名和它们的返回值所代表的意义。

dispatchTouchEvent: 简单的理解就是分发Touch事件,如果return true,表示事件已经被消费,不继续分发。return false表示没有被消费,继续分发。

onInterceptTouchEvent: 拦截Touch事件,ViewGroup在dispatchTouchEvent返回之前就会调用这个方法,根据onInterceptTouchEvent的返回值决定事件是否继续往下分发。onInterceptTouchEvent的默认返回值都是false,表示不拦截。想要拦截的话需要开发者自己去重载这个方法。

onTouchEvent: 处理Touch事件,如果return ture,那么这个时间就消费掉了。

细心的网友发现了,dispatch无论return true还是false,都不往下继续分发,不对吧!

dispatchTouchEvent一般不会直接return true或false。而是将事件抛给onInterceptTouchEvent和onTouchevent处理,问它们需不需要,最后再由dispatchTouchEvent进行最终的返回。这是很多新手开发者的理解误区,包括以前的我……

二、事件分发的基本过程

事件是怎么产生的这种底层问题我们先不管,我们只从知道的地方开始说起——Activity。

当一个事件开始传递后,最先接收到的是当前的Activity。然后Activity调用
public boolean dispatchTouchEvent(MotionEvent ev)
开始分发事件。内部又会调用PhoneWindow的内部对象DecorView的superdispatchKeyEvent,也就是ViewGroup的dispatchTouchEvent开始事件分发,DecorView是最顶层的View。

在dispatchTouchEvent方法中,ViewGroup会遍历自身的child(move事件和up事件有点不同,不会遍历child,下面会另外说明)

,再去调用子child的dispathTouchEvent方法,直到该事件被消费。传递途中还有onInterceptTouchEvent和onTouchEvent方法参与。

事件是由最外层的View开始传递,然后结果从最底层往外层返回。

如下图:ViewGroupA,ViewGroupB ,View的关系: A 是B的parent, B是C的parent。

图中的call是“调用”



我们再用代码来看下大致流程。

我们先建一个项目,然后写几个类继承FrameLayout,重写dispatchEvnet、onInterceptTouchEvent,onTouchEvent几个方法,看log输出。

ViewGroupA,B一样。

package com.aitsuki.touchevent;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.FrameLayout;

/**
* Created by AItsuki on 2015/12/30.
*/
public class ViewGroupA extends FrameLayout {

public ViewGroupA(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("Event","===ViewGroup:A======onTouchEvent===============Down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("Event","===ViewGroup:A======onTouchEvent===============Move");
break;
case MotionEvent.ACTION_UP:
Log.e("Event","===ViewGroup:A======onTouchEvent===============Up");
break;
}
return super.onTouchEvent(event);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("Event","===ViewGroup:A======onInterceptTouchEvent======Down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("Event","===ViewGroup:A======onInterceptTouchEvent======Move");
break;
case MotionEvent.ACTION_UP:
Log.e("Event","===ViewGroup:A======onInterceptTouchEvent======Up");
break;
}
return super.onInterceptTouchEvent(ev);
}

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("Event","===ViewGroup:A======dispatchTouchEvent========Down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("Event","===ViewGroup:A======dispatchTouchEvent========Move");
break;
case MotionEvent.ACTION_UP:
Log.e("Event","===ViewGroup:A======dispatchTouchEvent========Up");
break;
}
return super.dispatchTouchEvent(ev);
}
}


MyView

package com.aitsuki.touchevent;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

/**
* Created by AItsuki on 2015/12/30.
*/
public class MyView extends View {
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("Event","===View=============onTouchEvent===============Down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("Event","===View=============onTouchEvent===============Move");
break;
case MotionEvent.ACTION_UP:
Log.e("Event","===View=============onTouchEvent===============Up");
break;
}
return super.onTouchEvent(event);
}

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("Event","===View=============dispatchTouchEvent=========Down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("Event","===View=============dispatchTouchEvent=========Move");
break;
case MotionEvent.ACTION_UP:
Log.e("Event","===View=============dispatchTouchEvent=========Up");
break;
}
return super.dispatchTouchEvent(event);
}
}


布局activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<com.aitsuki.touchevent.ViewGroupA
android:layout_width="300dp"
android:layout_height="300dp"
android:background="#FF6A6A"
android:layout_gravity="center">

<com.aitsuki.touchevent.ViewGroupB
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center"
android:background="#9ACD32">

<com.aitsuki.touchevent.MyView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#1E90FF"
android:layout_gravity="center"/>

</com.aitsuki.touchevent.ViewGroupB>

</com.aitsuki.touchevent.ViewGroupA>

</FrameLayout>


布局预览:



红色:ViewGroupA

绿色:ViewGroupB

蓝色:View

现在我点击一下蓝色区域(View),看Log输出。



我们给ViewGroupA加上拦截之后再看看(让onInterceptTouchEvent返回true)



好了,基本流程和我们的图是一致的。

但是要注意 一、前言 的那段红字

三、 View的源码走读

大概理解了事件的传递过程之后,我们来看一下源码。

为什么先看View的源码而不看ViewGroup的原因有两点:

1. View是ViewGroup的父类,ViewGroup的onTouchEvent方法继承自View, 并没有重写。

2. View没有child,事件传递简单,不会打消各位的阅读源码积极性。

那么,开始吧。

先来看View的dispatchTouchView方法,我直接在上面注释。

mViewFlags:一种通过位运算记录开关的方式。mViewFlags一个32位的int值,用每一位的0或1记录属性。比如第1位的0和1记录focusable(是否可以获取焦点)

/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
// 如果这个View设置了触摸监听onTouchListener并且View是可用的,并且onTouch返回的是true
// 那么事件就消费掉了,传递结束。
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
// 否则,交给onTouchEvent处理(View中没有onInterceptTouchEvent方法,因为它没有
// child了,不需要有拦截方法)
return onTouchEvent(event);
}

// 注意看谷歌工程师的注释,它们称消费事件的View为target view,如果这里的dispatch或者说
// onTouchEvent返回true,那么这个View就是target View了。先记下来


继续看onTouchEvent

/**
* Implement this method to handle touch screen motion events.
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
// 如果View不可用,但它却是可点击的(clickable属性),那么仍然消费这个事件(但是不执行任何
// 操作,也就是不会响应)。也就是说,只要有clickable属性,那么这个点击事件就必然被消费掉。
if ((viewFlags & ENABLED_MASK) == DISABLED) {
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}

// 这个是触摸代理,就是点击另一个View,这个view会响应点击事件。默认是null,开发者可以通过
// setTouchDelegate设置。详情请自行查看TouchDelegate
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}

// 如果View是可点击的,那么消费掉这个事件,否则返回给上层处理(parent)
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
//...... 此处省略N行代码,没有return语句,我们pass。
return true;
}
return false;
}


好了,View处理事件的源码就这么点。一会就看完了,挺简单的。

从上面这两段代码可以得出的结论:

结论1:onTouch优先于onTouchEvent执行,并且onTouch消费掉事件后,onTouEvent不会再执行。

结论2:如果View是可点击的(clickable),那么事件一定会被消费掉,不会再继续传递。

我们来验证一下结论,用的还是 二、事件分发的基本过程 的那个项目。

我们给MyView设置touchListener并return true,然后点击蓝色区域看看log输出

activity代码:

public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

MyView view = (MyView) findViewById(R.id.view);
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("Event","===View=============onTouch====================Down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("Event","===View=============onTouch====================Move");
break;
case MotionEvent.ACTION_UP:
Log.e("Event","===View=============onTouch====================Up");
break;
}
return true;
}
});
}
}




结论1:onTouchEvent没有执行,验证正确。

将MainActivity设置侦听的代码注释掉,给View设置clickable属性,测试结论3

public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

MyView view = (MyView) findViewById(R.id.view);
view.setClickable(true);
//        view.setOnTouchListener(new View.OnTouchListener() {
//            @Override
//            public boolean onTouch(View v, MotionEvent event) {
//                switch (event.getAction()) {
//                    case MotionEvent.ACTION_DOWN:
//                        Log.e("Event","===View=============onTouch====================Down");
//                        break;
//                    case MotionEvent.ACTION_MOVE:
//                        Log.e("Event","===View=============onTouch====================Move");
//                        break;
//                    case MotionEvent.ACTION_UP:
//                        Log.e("Event","===View=============onTouch====================Up");
//                        break;
//                }
//                return true;
//            }
//        });
}
}




View执行了onTouchEvent,消费掉了事件,不再传递,结论3验证正确

请各位网友理解了View的事件传递的结论消化之后再继续往下看,因为ViewGroup中多次调用到super.dispatchTouchEvent, 其实也就是调用View的dispatchTouchEvent,因为View就是ViewGroup的父类。

四、ViewGroup源码走读

因为注释比较多,所以有点影响阅读性,最好可以配合没有注释的源码(api-8 Android2.2)一起阅读。同时思考一下我的分析是否和你的一样,有什么错误也可以在评论中指出。

我们只需要看dispatch的源码就可以了,onInterceptTouchEvent默认都是return false,没有onTouchEvent。

和View一样,也是使用注释的方式来说明。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 获取事件的类型和触摸坐标
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
// mTempRect初始是null的,这个Rect对象主要是用来记录child的可点击范围。
final Rect frame = mTempRect;

// mGroupFlags和mViewFlags一样是记录当前控件的状态。这里记录的是“是否允许拦截”这个属性。
// 可以通过requestDisallowInterceptTouchEvent这个方法进行设置,如果设置了这个属性,
// 那么ViewGroup就不会拦截child的事件了。
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

// 如果是down事件,就遍历child进行分发。
if (action == MotionEvent.ACTION_DOWN) {

// 两位谷歌工程师在聊天么=。=,大概意思就是:
// 为什么一个View响应完down事件后还没有消失,继续响应了第二次down事件。
// xxx: 我们可能应该发送一个up事件,而不是down……

// target:在之前也说过了,响应了down事件的那个View就是target,而在up或者cancel
// 事件执行后,这个target应该会被重置为null。
// 但是这里居然不是空的。博主我也不知道什么回事=。=

// 如果不是null,那么就让它重置为null。
if (mMotionTarget != null) {
// this is weird, we got a pen down, but we thought it was
// already down!
// XXX: We should probably send an ACTION_UP to the current
// target.
mMotionTarget = null;
}

// If we're disallowing intercept or if we're allowing and we didn't
// intercept
// 这里判断disallowIntercept有点多余,因为执行up或者cancel之后,这个属性就会被重
// 置为false(往下找)。而requestDisallowInterceptTouchEvent方法一般在child的
// dispatchTouchEvent中调用,但是事件还没有传到chid,那么这个方法也就不会执行,
// disallowIntercept肯定也只能是false。

// 我们无视掉这里的disallowIntercept,这里判断是否拦截child的事件。
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// reset this event's action (just to protect ourselves)
// 重置这个事件为donw事件,只是为了保证健壮性。上面那个disallowIntercept也是
// 为了健壮性么=。=
ev.setAction(MotionEvent.ACTION_DOWN);
// We know we want to dispatch the event down, find a child
// who can handle it, start with the front-most child.
// 不得不说谷歌工程师的注释也是很详细的=。=
// 我们想要分发这个down事件,遍历子View看谁可以持有它,从最上层的子View开始。
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
// 这里开始遍历所有child
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
// 如果child是可见的,或者child正在进行动画(动画中的View这个我没细看
// ,无视掉好了)。
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
// 获得child的有效点击范围,判断点击事件是否点在此child中。
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
// offset the event to the view's coordinate system
// 获取到事件的坐标是屏幕的绝对坐标,要转成child的相对坐标。
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);

// 如果点击事件在此child中,重置属性:CANCEL_NEXT_UP_EVENT
// 该属性的描述:Indicates whether the view is temporarily
// detached。
// 标记哪一个View是暂时和parent分离,就是和当前这个ViewGroup分
// 离。
// 有什么应用场景我也不知道,不过abslistView中是用到了,那边的源码

// PS:位运算没忘吧=。=,比如CANCEL_NEXT_UP_EVENT = 0000 1000
// 取反 1111 0111。  与上这个数就是取消 CANCEL_NEXT_UP_EVENT
// 这个属性……

child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;

// 然后这里调用child的事件分发
// 这里会出现几种情况
// 1:child是View,那么回想一下View的源码吧(三、View源码走读)
// 如果child消费了此down事件,那么这个child就是target了。
// 如果不消费,那么返回给当前的ViewGroup的onTouchEvent消费。

// 2:child是ViewGroup,那么继续调用child的dispatch,
// 继续遍历child的child(噗,孙子),直到有响应这个事件的。
// 如果最底层的child也是ViewGroup,那么请直接跳过这个遍历,
// 往下看…… if(target == null)那里。
// 调用super.dispatchEvent。
// 也就是说,如果最底层的child是ViewGroup,那么将它作为View处理。
if (child.dispatchTouchEvent(ev))  {
// Event handled, we have a target now.
// 如果响应了down事件,那么这个child就是target
mMotionTarget = child;
return true;
}
// The event didn't get handled, try the next view.
// Don't reset the event's location, it's not
// necessary here.
}
}
}
}
}

// 思考:dispatch中调用child的dispatch方法,这看起来是不是像递归遍历。直到有child响应down事件,
// 否则将所有child遍历完后这个事件流产。

// 注意:child有可能是View,也有可能是ViewGroup。它们两个的dispatch方法是不同的。如果是View,
// 就代表这个事件分发已经到了最底下的View了。

//===================================================================================
// 判断事件是否是up或者cancel
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);

// 如果是up或者cancel事件,将FLAG_DISALLOW_INTERCEPT(disallowIntercept是通过
// 这个值计算的)这个属性移除。
// 就是说,既然是up事件和cancel事件已经接收到,那么就代表这在target上的事件结束了,
// 重置这个属性,不然会影响到下一个事件。
if (isUpOrCancel) {
// Note, we've already copied the previous state to our local
// variable, so this takes effect on the next event
// 我们已经复制了上一个的状态到变量,所以这里就影响了下一个事件。
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}

// The event wasn't an ACTION_DOWN, dispatch it to our target if
// we have one.
// 如果traget存在,并且这个事件不是down,那么就将事件交给target处理
// (谁响应了down事件,后续事件就交给谁处理)
final View target = mMotionTarget;

if (target == null) {
// We don't have a target, this means we're handling the
// event as a regular view.
// 如果target为null,那么代表down事件没有被响应(也可能是target的后续事件被拦截,
// 那么target也会被重置为null)
ev.setLocation(xf, yf);
// 如果当前的View或者说ViewGroup已经从parent中分离,那么将事件改为cancel
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
// 这里关键了,如果这个ViewGroup没有child,那么它就会走到这里,然后调用View的事件
// 分发, 将自己作为一个View处理。
// 当返回dispatch返回true之后,这个ViewGroup本身就是target了,不过这个target引用
// 是在parent中,不是当前的=。=
return super.dispatchTouchEvent(ev);
}

// 代码能走到这里的话就代表,这不是一个down事件。

// if have a target, see if we're allowed to and want to intercept its
// events
// 如果我们有一个target,我们就会将后续的事件都交给target处理(跳过这个if判断直接往
// 下看)。
// 结合前面分析,从这里看出,如果某个child请求了requestDisallow,那么child就肯定能
// 接收到move和up事件,前提是没有拦截down事件。

// 假设我们拦截了后续事件(不拦截child的down事件,但是拦截了child的其他事件)
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
// 将事件改成cancel,然后交给target的dispatch处理。(这里可以看出,如果有
// target响应的down事件,但是target后续事件被拦截,那么就会传一个cancel给target)

// 不管它有没有响应,我们都将target置为空,就是说,后续事件当做没有target来处理。
if (!target.dispatchTouchEvent(ev)) {
// target didn't handle ACTION_CANCEL. not much we can do
// but they should have.
}
// clear the target
mMotionTarget = null;
// Don't dispatch this event to our own view, because we already
// saw it when intercepting; we just want to give the following
// event to the normal onTouchEvent().
return true;
}

// 如果事件是up或者cancel,那么将target重置为null。因为响应down事件的就是target,
// 既然已经抬起手指了,那target就没了。
if (isUpOrCancel) {
mMotionTarget = null;
}

// finally offset the event to the target's coordinate system and
// dispatch the event.
// 将坐标转成target的相对坐标。
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);

// 如果target已经被分离出去(相当于remove),那么将事件改成cancel。
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}

// 将所有后续事件都交给target处理。
return target.dispatchTouchEvent(ev);
}


注释太多看起来有点乱,不过没关系,大家配合无注释的代码一起看就行了。我只是将每一行的代码的作用注释出来,各种结论还是需要推测。

五、结论

现在我们来总结一下分析源码得出的一些结论:

结论1:onTouch优先于onTouchEvent执行,并且onTouch消费掉事件后,onTouEvent不会再执行。

结论2:如果View是可点击的(clickable),那么事件一定会被消费掉,不会再继续传递。

结论3: 如果ViewGroup在onInterceptTouchEvent中拦截了child的事件,那么这个事件会交给ViewGroup的onTouchEvent处理。

结论4:onInterceptTouchEvent方法在一次事件序列(down到up或者cancel的过程)中,只要返回true就不会再调用,或者说只要拦截过一次之后就不会再调用,直到下一次down事件开始前。

结论5:响应了down事件的View被称为target,Android会将后续事件都交给target处理。

结论6:在结论5的基础上,如果onInterceptTouchEvent中拦截了target的其他事件,比如move或up,那么target就会接受到一个cancel事件,并且将target置为null,后续的事件交给target的parent处理。(比如,我可以让child响应down事件,然后只拦截它的move事件,那么我就可以接受到除了down的所有后续事件了,而child则会接收到一个cancel事件,这样就可以解决滑动事件和按钮冲突的问题了。)

结论7:在结论6的基础上,child可以通过requestDisallowInterceptTouchEvent请求parent不拦截他的事件,前提是child能响应到down事件。(例如:parent在onIntercept中拦截了事件,child就没机会请求了。再例如:parent不拦截down事件,但是拦截了move和up事件,这时候requestDisallowInterceptTouchEvent就派上用场了)

结论8: 如果一个View在处理一个事件序列(down到up或者cancel的过程)的时候,parent将他remove掉了,那么这个View会接收到一个cancel事件。

结论的验证过程都很简单,因为文章篇幅已经太长了不打算贴出来,请自行验证结论的正确性,也可惜选择相信我的验证结果。

六、写在后面

这博客真的很难写,花了两天多的时间。不知道应该怎么写才能更加简洁易懂,最后干脆以注释的方式了。

虽然写了七八个结论,但是这并不是全部,我也很难将所有结论一个一个列出来。

分析源码,读懂源码的好处就是,当你发现事件冲突的时候,不会像无头苍蝇一样在百度胡乱搜方案,最后还搞得一头雾水。而是让你自己有能力解决冲突,能找到冲突的源头。

至于解决事件冲突的一些案例,我有空的话可能会整理几个出来,但是写着博客有种身心疲惫的感觉
_(:з」∠)_
,休息一段时间再说,就这样,下次见。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: