资讯动态

Android点击事件分发机制剖析:从MotionEvent到onClick的完整链路

发布时间:2026/10/14 12:24:04 来源:尧图企业网站定制
很多人写业务代码时会把点击事件当成一个理所当然的东西控件上设一个onClick用户点到了回调就触发了。但真遇到点不了、点偏了、滑动冲突、子 View 抢事件这类问题如果对点击事件的整套分发链路没有一个完整认知排查起来会非常痛苦。这篇文章是 Android 点击事件深度梳理的第一篇目标是把一条点击事件从物理屏幕出发最终变成onClick回调这个过程里的每一件关键事都讲透。后续系列会继续拆手势冲突、长按、多点触控这些进阶话题这篇先把地基打好。1. 点击事件从物理屏幕到View的第一公里输入系统链路1.1 触摸屏采样一次按压如何变成MotionEvent很多 Android 开发者对事件分发的理解是从ViewGroup.dispatchTouchEvent开始的但一次真实点击在到达你的自定义 View 之前已经走完了非常漫长的一段路。用户的指腹按在屏幕上触摸屏控制器会通过电容变化感知到触摸位置把模拟信号转成数字信号上报给驱动。Linux 内核的 input 子系统拿到这些原始上报后会把它包装成input_event结构体写入设备节点。这一阶段没有 Android 的概念只有硬件和内核。再往上系统里的InputManagerService才是真正意义上的Android 输入管家。它内部维护了两个线程InputReader和InputDispatcher。InputReader负责从内核读取原始事件做一系列加工比如坐标转换、按键映射、对事件做去抖动合并最终生成一个标准的MotionEvent对象。InputDispatcher则负责把这个MotionEvent投递给当前应该接收事件的窗口。所以一个MotionEvent本质上不是某个 View 突然蹦出来的它是系统对原始触摸数据做了完整加工后的产物。里面携带的关键信息包括action当前动作类型ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_CANCEL等x / y相对于当前 View 的坐标rawX / rawY相对于屏幕的绝对坐标eventTime事件发生的时间戳pointerId和pointerIndex用于分辨多点触控中的哪根手指pressure、size这类辅助信息压感和触摸面积部分设备上并不精确理解MotionEvent的诞生过程对排查点击延迟偶尔丢失点击这类问题特别关键。有些情况下事件根本没有送到应用进程你在 View 层怎么打日志都是看不到的。1.2 跨进程投递InputDispatcher与窗口输入通道InputDispatcher和应用进程不在同一个进程里它属于系统进程。要让事件从系统进程安全地到达 App 进程中间靠的是一套基于 socket 的跨进程通道这套通道在 Android 里叫InputChannel。每个窗口在添加时都会通过ViewRootImpl.setView向WindowManagerService注册自己的InputChannel。InputDispatcher内部维护着当前屏幕上所有窗口的输入通道信息它会根据触摸坐标、窗口层级、焦点状态来决定把事件投递给哪个窗口。这里有一个很重要的概念触摸事件的分发目标和键盘焦点并不完全一样。键盘事件只看焦点窗口而触摸事件是先根据坐标找到落在哪个窗口区域再结合窗口的 touchable 属性来确定目标。这也是为什么屏幕上两个窗口重叠时只有上层可触摸窗口能收到点击。InputDispatcher一旦确定了目标窗口会把MotionEvent封装好通过 socket 写入应用进程对应的InputChannel。应用进程侧ViewRootImpl创建了一个WindowInputEventReceiver它持续监听这个通道收到事件后会做一次排队和同步然后交给DecorView。这里还涉及到一个finishInputEvent的回执机制应用处理完事件后必须通知InputDispatcher这样系统才知道这个事件已经处理完了可以继续派发下一个。如果应用处理得太慢InputDispatcher 会认为窗口无响应从而可能触发 ANR 或者输入延迟。1.3 应用侧接收ViewRootImpl到DecorView的交接事件进入应用进程后第一站是ViewRootImpl内部的WindowInputEventReceiver它会调用deliverInputEvent最终走到DecorView.dispatchTouchEvent。但注意DecorView.dispatchTouchEvent不会立刻开始做ViewGroup的那套分发逻辑它要先经过Activity这层中转。准确地说事件流向是这样的WindowInputEventReceiver - DecorView.dispatchTouchEvent - Activity.dispatchTouchEvent - PhoneWindow.superDispatchTouchEvent - DecorView.superDispatchTouchEvent - ViewGroup.dispatchTouchEvent也就是说View 层的事件分发实际上是Activity给了Window也就是PhoneWindow一次处理机会PhoneWindow再调回DecorView。如果你在Activity里重写了dispatchTouchEvent并且返回true那么窗口和 View 层都不会再收到这个事件。这种给 Activity 先看一眼的设计是为了让顶层界面有机会拦截全局手势比如某些全局侧滑返回就是这么做的。这一段链路平时写业务确实用不太到但一旦你开始做启动优化、性能分析或者遇到为什么我自定义 View 里收不到某些事件这种诡异问题就需要回头检查是不是在更靠前的环节就被拦截了。我从实际排障经验里给一个建议排查点击问题时先想清楚事件到底有没有进入我的 View 树再想进入之后被哪个方法拦住了。很多问题卡了半天最后其实是前一层就已经断了。2. 事件分发的三个核心方法它们到底在说什么2.1 dispatch / intercept / onTouch 的分工进入 View 树之后就来到了所有 Android 开发者最熟悉的三个方法dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。理解这三者的关系是理解整个点击事件机制的基石。可以用一个小型公司的审批流程来打比方。一份文件送进来先到部门经理手里经理看一眼判断这个文件应不应该往下传给员工如果不传经理自己处理如果传文件会发给负责这个子任务的员工。员工拿到文件后可以处理并签字消费也可以说自己处理不了把文件退还回去。对应到 Android 的代码体系dispatchTouchEvent是总入口负责调度。不管最终事件被谁消费所有事件都会先经过这个方法。onInterceptTouchEvent是经理的拦截决策环节。它只存在于ViewGroup普通View没有这个方法。它负责回答一个问题这个事件要不要继续往下发给子 View请注意它只是决定拦截它本身不消费事件也不处理事件。onTouchEvent是最终执行环节。事件如果到了某个 View 手上且前面的监听器没有消费就会走到这里由它决定消费还是返回false上抛。一个容易混淆的地方是onInterceptTouchEvent和onTouchEvent虽然都带Touch但语义完全不同。拦截是切断向下传递的路径消费是吃掉这个事件不再向上反馈。拦截完之后事件会转给当前ViewGroup自己的onTouchEvent去处理而不是凭空消失。2.2 责任链怎么走一段可读的伪代码View和ViewGroup的dispatchTouchEvent行为不一样。先看最简单的Viewpublic boolean dispatchTouchEvent(MotionEvent event) { boolean result false; // 1. 先给 OnTouchListener 一个机会 if (mOnTouchListener ! null isEnabled() mOnTouchListener.onTouch(this, event)) { result true; } // 2. 监听器没消费才轮到 onTouchEvent if (!result onTouchEvent(event)) { result true; } return result; }而ViewGroup的dispatchTouchEvent要复杂得多因为中间多了往下分发这个动作。简化后的决策逻辑大概是public boolean dispatchTouchEvent(MotionEvent ev) { // 先给自己一个拦截决策的机会 boolean intercepted onInterceptTouchEvent(ev); // 如果没有拦截尝试找到能接收事件的子 View并分发下去 if (!intercepted) { for (子 View in 子View列表) { if (坐标命中了子View 子View.dispatchTouchEvent(ev) true) { mFirstTouchTarget 命中子View; return true; } } } // 没有子 View 接收或者被拦截了自己处理 return super.dispatchTouchEvent(ev); }实际源码当然比这复杂得多比如DOWN事件才允许重新寻找目标、MOVE和UP事件一般只会往已确定的 target 上发这些我下一节展开。但先记住这条责任链的感觉事件永远是从根部ViewGroup逐层向下问越界了再逐层向上抛。向下用dispatchTouchEvent拦截用onInterceptTouchEvent处理用onTouchEvent。2.3 true和false的真正含义以及两个常见误解很多人刚接触事件分发时会走入一个误区认为onTouchEvent返回true就是把这个事件消费了返回false就是把这个事件丢了。这个理解大体是对的但不完整。更准确的说法是返回值表示我这个层级是否还需要对这个事件负责。dispatchTouchEvent返回true表示整个事件分发链认为这件事已经被处理完了不再往上抛。返回false则表示向上传递最终可能到Activity的onTouchEvent。如果连Activity都返回false这个事件就真的没人管了。再纠两个非常常见的误解第一个误解onInterceptTouchEvent返回true代表我的 ViewGroup 消费了事件。不是的它只是把事件拦下来了接下来系统会把事件转交给这个ViewGroup自己的onTouchEvent。至于onTouchEvent消费不消费那是另一回事。第二个误解onTouchEvent返回false的事件就不会再往父 View 传了。这个说法需要分情况。如果是最开始的那个DOWN事件返回false那后续整个事件序列都不会再传给这个 View。但如果是事件序列中间某个MOVE返回false系统的处理方式会有细微差别这同样关系到触摸事件是怎么认领的放到后面事件序列部分细讲。3. ViewGroup怎么裁决一个事件拦截与下发的完整决策路径3.1 DOWN事件的签约目标子View是怎么被选中的ViewGroup每次收到一个完整手势的第一个ACTION_DOWN时会执行一套寻找目标的逻辑。这个逻辑的本质是从所有直接子 View 里找到坐标命中、可见、没被禁用、并且愿意接收这个事件的那一个。我把这个动作叫做签约。一个事件序列从DOWN开始一旦某个子 View 接收了DOWN并返回了true它就相当于和这个事件序列签了合同。后续同一次手势里的MOVE、UP只要不发生意外都会直接发给这个签约的子 View不会再像DOWN那样去找其他人。所以DOWN事件在整个分发机制里地位非常特殊。它是事件序列的登记时刻。系统在收到DOWN时会先清除之前的mFirstTouchTarget链然后才开始找新目标。mFirstTouchTarget就是那个合同上的名字它记录着当前事件序列的接收者。需要注意的是ViewGroup在找目标时是从后往前遍历子 View 的也就是最上层显示的子 View 会先被检查。如果两个子 View 区域重叠默认上层那个会先拿到事件。这也是为什么有时候你明明给底层 View 设了点击事件却死活点不到——上面还有一层透明 View 把事件吸走了。3.2 后续事件的分发沿着mFirstTouchTarget一路走DOWN结束后手指移动产生的MOVE和最后抬手的UP会沿着mFirstTouchTarget这条链路直接分发给签约的 View不会再走一遍从后往前找子 View的流程。这样做既省性能也保证了一个手势从始至终由同一个 View 接收避免出现手指还没抬起来事件突然换了个对象的诡异现象。不过直接分发不代表ViewGroup完全放手。它在分发每个事件前照样会先执行自己的onInterceptTouchEvent。如果该方法返回trueViewGroup就会把事件从子 View 手里抢回来具体怎么抢核心是补发一个ACTION_CANCEL。当前事件序列的归属权本质上只有DOWN那一下才做最终决定后续事件的分发路径是顺着合同走但随时可以被父 View 撕毁合同。我再补充一点MOVE和UP事件在分发时如果子 View 返回了false系统不会像DOWN那样重新找目标而是把当前的 target 从链表里移除。如果整个 target 链变成了空父 View 才有机会重新接管后续事件。这也是很多嵌套滑动冲突问题的根源。3.3 FLAG_DISALLOW_INTERCEPT子View的反向控制父 View 拥有拦截权但子 View 也不是完全没有反制手段。Android 提供了一个requestDisallowInterceptTouchEvent方法子 View 可以通过它请求父 View 这一串事件里你暂时不要拦截。这个机制在横向滑动和纵向滑动嵌套时特别有用。典型的场景是一个纵向滚动的列表里嵌了一个横向滑动的轮播图。手指横着滑动时我们希望轮播图自己处理不触发列表的纵向滚动这时轮播图内部会在收到DOWN后调用getParent().requestDisallowInterceptTouchEvent(true)父 View 在后续MOVE中就不会再拦截事件。但这里有几个细节要特别注意DOWN事件发生时父 View 依然有权力做首次拦截决策因为此时事件序列还没有建立FLAG_DISALLOW_INTERCEPT标志在事件序列开始时会失效。一旦父 View 真的把事件转发给子 View子 View 才能调用requestDisallowInterceptTouchEvent(true)阻止后续拦截。当事件序列结束或者子 View 收到CANCEL时这个标志会被系统自动清除。我自己做自定义滑动控件时就经常用这个标志来避免父容器和子控件互相抢事件。但有一点要提醒它只对父 View 的拦截有效。如果父 View 在dispatchTouchEvent里直接把事件扣下了子 View 再怎么请求都拦不住。3.4 中途拦截之后系统为什么补发一个CANCEL假设一个事件序列已经签给了子 View父 View 在某个MOVE事件时突然决定拦截这时系统会做一件非常重要的事给子 View 补发一个ACTION_CANCEL。为什么需要CANCEL因为子 View 已经从DOWN开始维护自己的状态了。比如一个按钮在DOWN时进入了按压态背景高亮长按计时器也在走。如果父 View 在某个瞬间不声不响地停掉事件传递子 View 会一直以为自己还在按压中出现按钮卡在高亮状态怎么点都点不掉这种 bug。ACTION_CANCEL就是用来告诉子 View这个事件序列不归你了请立刻重置你的状态。所以自定义控件做手势识别时对ACTION_CANCEL的处理要和ACTION_UP一样严谨该清理的按压状态、该取消的计数器都要处理否则很容易留下各种幽灵状态。4. 事件到达View之后的走向从onTouchEvent到onClick的诞生4.1 默认的onTouchEvent都做了哪些事事件经过层层分发最终落到某个 View 时会进入它的onTouchEvent。普通 View 的onTouchEvent都做了些什么我们先说结论它的大部分行为都围绕着一个核心字段——clickable也就是这个 View 是否可点击。一个 View 默认情况下clickable是false此时onTouchEvent会在DOWN事件时直接返回false表示自己不消费事件。但如果你给这个 View 设置了OnClickListener、OnLongClickListener或者OnTouchListener系统的逻辑会自动把它变成可点击状态clickable变成true。当一个 View 是可点击的onTouchEvent处理逻辑就丰富起来了ACTION_DOWN记录按压状态触发View进入按下的 pressed 状态同时启动一个长按检测的定时任务。如果你设置了按压背景这时候开始显示。ACTION_MOVE检查手指是否还在 View 的边界内。如果移出了边界且没有重新进入会取消按压状态如果移回了边界会恢复按压状态。ACTION_UP表示手势结束。如果手指是在这个 View 区域内抬起的就会触发点击动作也就是performClick。同时取消长按检测清除按压状态。这四个动作连起来看你会发现onClick并不是一个独立事件它是在ACTION_UP时由onTouchEvent根据整个手势过程推算出来的。4.2 onClick是被凑出来的点击识别与performClick那performClick内部是怎么工作的其实非常简单public boolean performClick() { final boolean result; final ListenerInfo li mListenerInfo; if (li ! null li.mOnClickListener ! null) { li.mOnClickListener.onClick(this); result true; } else { result false; } return result; }就是调用了你注册的OnClickListener的onClick方法。但真正关键的是它在什么时机被调用。前面说了onTouchEvent在ACTION_UP时判断到手指在 View 区域范围内抬起就会走performClick。所以一次真正意义上的点击需要满足几个条件事件序列的DOWN是分发给当前 View 的且DOWN被消费了整个手势过程中没有收到ACTION_CANCEL手指在抬起时依然位于该 View 的边界内如果手指按下去之后滑出了 View 再抬起是不会触发点击的。这符合物理直觉你按了一个按钮手指滑走了按钮不该被点亮。有一个长期被忽略的细节performClick这个名字其实是在强调一件事——你手动调用performClick也能产生onClick回调。这在做无障碍兼容时特别重要。Android 系统在辅助功能模式下来自键盘、无障碍服务的点击操作很多都是直接调用performClick触发的。所以官方规范一直建议自定义 View 里如果有点击语义不要在onTouchEvent里直接写业务逻辑而是调用performClick()让监听器被触发。这样无障碍系统才能正确识别你的手势。长按的机制也依赖这个体系。Android 在DOWN时会启动一个延迟任务默认500ms。如果500ms内没有发生ACTION_UP、没有滑动移出边界且 View 设置了OnLongClickListener就会触发长按回调。长按触发后通常意味着这个手势已经被长按消费了系统会用一个内部标志让后续的UP不再触发普通点击。4.3 onTouch、onTouchEvent、onClick的执行顺序一个 View 收到事件后可能的处理路径有OnTouchListener、onTouchEvent、OnClickListener三层它们的执行顺序非常关键实践中的很多 bug 都出在这里。顺序是固定的OnTouchListener最先执行如果它返回了trueonTouchEvent不再执行onClick也一定不会触发。如果OnTouchListener返回false或者没有设置事件才轮到onTouchEvent处理onClick是onTouchEvent在合适的时机里调出来的。用代码表示就是if (mOnTouchListener ! null mOnTouchListener.onTouch(this, event)) { // 事件被 OnTouchListener 消费了 return true; } if (onTouchEvent(event)) { // onTouchEvent 内部按需调用了 performClick() return true; }我见过不少同学在实现一个既能监听触摸轨迹又要响应点击的控件时直接在onTouch里处理了一堆逻辑并随手return true结果onClick莫名不触发。这不是系统 bug而是你侵犯了事件分配的顺序。5. 一个事件序列的完整生命周期从DOWN的签约到CANCEL的违约5.1 DOWN决定归属事件序列的合同关系所谓事件序列指的是手指按下、移动、抬起这完整的一套动作对应ACTION_DOWN到中间若干个ACTION_MOVE再到ACTION_UP。Android 的事件分发机制在设计上把整个序列当成一个整体来管理。这个管理规则简单说就是DOWN事件决定了这个序列归谁管。只要DOWN被某个 View 消费了后续所有MOVE和UP都优先发给它哪怕是手指已经滑到别的 View 上方了。你在界面上看到的现象就是手指按下去之后即使滑出了控件区域这个控件依然能收到MOVE事件有的控件会在滑出边界后显示取消按压态那是onTouchEvent自己根据坐标做的状态处理不是系统切换了事件归属。反过来如果DOWN事件一路上抛最终没有被任何 View 消费系统会怎么处理这个序列后续的所有事件都会被视为无人认领它们不会再进入子 View 的分发队列而是直接向上抛给Activity。如果Activity也没处理那这个手势就彻底没有接收者了。很多新手自定义 View 时不消费DOWN却期待后续收到UP这是不可能的。5.2 MOVE阶段的中途变节拦截、回抛与责任移交前面说过DOWN签约之后父 View 依然有权在MOVE阶段拦截。一旦拦截发生子 View 会收到CANCEL事件序列的归属重新回到父 View 手上。这种情况在嵌套滑动场景里再常见不过手指在子列表上滑动滑到一定距离后父容器觉得这个滚动应该归我管了于是中途拦截子列表被迫退出。另一种不太为人注意的情况是子 View 把DOWN消费了但后续某个MOVE返回了false。这时系统的处理是把这个子 View 从事件序列的 target 链里摘除。如果原本有多个 target事件可能移交给链上的下一个如果 target 链空了事件重新归父 View 处理。这个移交过程不会补发CANCEL因为在这个场景中退回事件的是子 View 自己它已经显式表态我不想继续管了。但有一个例外要记住对于ACTION_UP如果当前 target 消费了它即使返回false系统也不会再向上回抛一次UP。原因很好理解整个手势已经结束了没有继续分发和交接的必要。所以判断一个UP是否被消费要结合前面整个序列来看单独看UP的返回值没有太大意义。5.3 CANCEL事件哪些场景会收到收到后该做什么ACTION_CANCEL是最容易被新手忽略的一种事件类型。它不是手指抬起而是系统宣布这个事件序列被中断了你不用再管了。常见的触发场景包括父 View 在事件序列中途决定拦截给子 View 补发CANCEL当前窗口失去焦点比如用户按了 Home 键、来电弹窗遮盖了屏幕View 在事件序列进行中从窗口树上移除或者窗口本身被销毁系统输入通道被重置收到CANCEL时View 应该做和UP类似的收尾处理清除按压状态、取消长按定时器、终止正在进行的动画或手势识别。如果一个控件只处理UP不处理CANCEL很容易出现按压效果卡住手势状态残留这类问题。我还想提醒一个细节CANCEL事件和UP事件虽然都是结束但语义上完全不同。UP是一个正常的结束手势表示手指抬起CANCEL是一个被外部打断的结束表示这个手势被人为终止。处理时务必区分对待。很多开源手势框架里CANCEL分支都会要求你重置所有状态不能像UP那样去做完成动作。6. 这些坑我踩过事件分发的常见误区和排错方法6.1 onTouch返回true之后onClick不触发到底是谁的锅这个问题在社区里被问过无数遍。场景是我给一个按钮同时设置了OnTouchListener和OnClickListener然后在onTouch里做了一些自定义判断结果发现onClick不再触发了。原因我在 4.3 已经讲透了OnTouchListener在onTouchEvent之前执行返回true表示消费了事件onTouchEvent根本没机会运行onClick自然不存在。这不是 Android 的 bug而是你无形中把事件的责任签给了OnTouchListener。解决办法有两种。如果你只是想在处理触摸事件的同时不影响点击那onTouch里处理完自己的逻辑后要返回false把事件继续交还给onTouchEvent。如果你确有必须消费触摸事件的需求那就不要依赖OnClickListener而是在你自己的逻辑里显式调用performClick()保证点击语义不丢这也是官方推荐的兼容无障碍的做法。6.2 子View消费了DOWN父View的onTouchEvent却还是被调用了还有一种情况让人非常困惑我明明确认子 View 在DOWN时返回了true为什么父 View 的onTouchEvent还是收到了后续事件这通常发生在父 View 中途拦截之后。父 View 在某个MOVE中调用onInterceptTouchEvent返回了true子 View 收到CANCEL退出接下来这个手势的后续MOVE和UP就会由父 View 自己处理。如果你只在DOWN阶段打日志自然看不到这个全貌。还有一种实现子 View 在DOWN时消费了但后续MOVE的某个时刻返回了false。前面说过这会触发 target 链的移除与责任移交最终父 View 的onTouchEvent可以收到后续事件。这种移交尤其容易出现在一些只关心按下、不关心滑动的控件上——它在DOWN抢到了事件滑两下又撒手了父容器被迫接盘。如果需要验证最直接的办法是在父子 View 的重写方法里把所有事件类型打上日志配合MotionEvent.actionToString看清楚整个序列不要只看单个事件。6.3 调试事件分发时的正确姿势日志怎么打才有效很多人调事件分发只在onTouchEvent里打一个Log看到没触发就一脸蒙。我的建议是排查这类问题时在关键的dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent里都打日志并且打全事件类型和坐标。日志格式可以参考这样Override public boolean dispatchTouchEvent(MotionEvent ev) { boolean result super.dispatchTouchEvent(ev); Log.d(TouchFlow, String.format( dispatch | %s | (%s, %s) | result%s, MotionEvent.actionToString(ev.getActionMasked()), ev.getRawX(), ev.getRawY(), result)); return result; }在ViewGroup里重写onInterceptTouchEvent时同样把返回结果打出来。这样一次手势下来你就能在 Logcat 里完整看到事件从父到子的流经路径以及每个环节的决策结果。很多看似玄学的点击问题用这个方式几分钟就能定位到是哪一层拦截或消费出了问题。如果事件根本没有到达应用进程比如日志里一个都没打那问题很可能出在输入系统层面。可以结合dumpsys input查看窗口焦点、输入通道状态排查是不是有系统级窗口遮挡住了你的应用。这类问题光看 View 层代码是找不到答案的。6.4 自定义控件处理点击事件的最佳实践最后总结几条我自己在项目里踩过之后沉淀下来的实践原则。第一条不要在dispatchTouchEvent里做业务处理。它负责的是事件调度你在里面写业务逻辑会破坏父子 View 之间的责任边界。自定义控件真正处理触摸业务的地方应该是onTouchEvent。第二条凡是语义上属于点击的操作都通过performClick()触发不要直接在onTouchEvent的UP分支里塞业务代码。这样既保证无障碍兼容也方便测试时通过代码模拟点击。第三条处理CANCEL时要有一种保险思维。哪怕你当前控件很简单也要把CANCEL当成一个正式分支来处理清空按压、取消定时器。很多看似偶发的小 bug都是因为CANCEL分支被随手忽略了。第四条遇到复杂手势场景优先画状态机。把DOWN到UP的每种可能路径列出来标出哪些节点会收到CANCEL哪些地方允许父 View 拦截。想清楚状态状态之间的流转再动笔写代码比边写边猜靠谱得多。点击事件分发这套东西初看好像就是几个方法来回调用但真正吃透它需要在真实的项目里反复验证。我在自己梳理完这条链路之后最大的感受是很多之前看起来莫名其妙的点击 bug其实都是因为对事件序列的归属和CANCEL语义理解不够深入。希望这篇梳理也能帮你把这些概念串起来后面我们再往下拆具体场景。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑