
先把Flex事件分发的底层逻辑掰碎了说,别被术语吓住
我敢说,80%的Flex事件问题,都是没搞懂“事件流”到底怎么流的。你就把Flex的界面想成一套俄罗斯套娃——最外面是Application容器,里面套着Layout容器,再里面是地图组件、按钮、输入框这些小套娃。当你点击一个按钮时,事件的“流动”分三步:
第一步是捕获阶段:事件从最外层的Application容器开始,顺着套娃的层级往下“钻”,路过每个容器都会问一句:“你要不要处理这个事件?”比如Application→Layout→Map→Button的父容器→Button本身,这一步是“从上到下”。
第二步是目标阶段:事件终于钻到你点击的那个按钮(目标元素),这时候按钮自己的事件处理函数会触发——比如你写的btnClick函数。
第三步是冒泡阶段:事件处理完目标元素,又会“往回跑”,从Button→父容器→Map→Layout→Application,这一步是“从下到上”,每个容器再问一遍:“你要不要再处理一次?”
是不是有点像快递员送快递:先从快递公司(Application)出发,逐层往下找收件人(Button)(捕获);找到人把快递递过去(目标);再逐层返回,问每层楼的住户“有没有要带的东西”(冒泡)。
那FlexViewer的事件为什么会“乱串”?我朋友的项目里,问题出在冒泡阶段:他给地图加了click事件(用来显示坐标),又给按钮加了click事件(用来刷新数据),结果点击按钮时,事件先触发了按钮的函数(目标阶段),然后往上冒泡到地图容器,触发了地图的click事件——所以一点按钮,地图坐标也跟着变,完全不对。
我再用表格把这三个阶段的关键信息列清楚,你对比着看就懂了(带实线边框,手机上也能看清):
阶段名称 | 执行顺序 | 常用处理方法 | 我踩过的坑 |
---|---|---|---|
捕获阶段 | 从上到下(父→子) | addEventListener时加useCapture=true | 别随便开useCapture,会覆盖子元素的事件 |
目标阶段 | 仅目标元素本身 | 直接给目标元素加事件监听 | 目标元素可能被父容器的事件“拦截” |
冒泡阶段 | 从下到上(子→父) | event.stopPropagation()阻止冒泡 | stopPropagation会影响父容器的事件,谨慎用 |
Adobe官方文档里其实早说过,Flex的事件模型是抄的网页前端的DOM Level 2标准(参考链接:https://helpx.adobe.com/cn/flex/using/events.html),所以你要是做过网页开发,其实这套逻辑特熟悉——只不过Flex把它包装成了自己的API。而FlexViewer的“坑”,恰恰是它帮你默认加了一堆事件监听(比如地图的缩放、侧边栏的展开),这些默认监听会“插队”到你的自定义事件前面,导致你写的代码“不好使”。
FlexViewer事件机制剥离的实操步骤,我踩过的坑你别再踩
光懂逻辑没用,得动手拆。我把去年帮朋友调试的流程整理成了4步实操,每一步都标了我踩过的坑,你跟着走,省得绕远路。
第一步:先找到“事件的发源地”,别瞎改代码
你要是连事件从哪来的都不知道,改代码就是瞎撞。我当时的办法特笨,但有效——在每个可能的容器里加trace语句,看输出的顺序。比如朋友的项目里,点击地图上的标注,应该触发弹出信息窗,但结果触发了侧边栏的折叠。我就给Application、Layout、Map、Sidebar、Marker这几个容器的click事件里都加了trace("触发了" + this.id + "的click事件")
,结果输出是:
触发了Application的click事件
→触发了Layout的click事件
→触发了Sidebar的click事件
→触发了Map的click事件
→触发了Marker的click事件
哦,原来Sidebar的click事件比Marker的先触发!因为Sidebar是Map的父容器,冒泡阶段是从子到父?不对啊,等一下——哦,我朋友给Sidebar加click事件时,用了useCapture=true
(捕获阶段),所以事件在捕获阶段就触发了Sidebar的函数,比目标阶段的Marker事件还早。坑点1:别随便给父容器加useCapture=true,会“截胡”子元素的事件。
你要是觉得trace麻烦,也可以用Flex Builder的调试工具(现在叫Flash Builder),在事件处理函数里打个断点,运行时看调用栈——栈顶的函数就是最先触发的,顺着栈往下走,就能理清事件的“流动路径”。
第二步:拆解“事件分发链路”,看谁在“插队”
找到发源地之后,下一步是理清哪些容器加了事件监听,优先级怎么样。我当时用了个表格(没错,又是表格,整理链路特好用),把每个容器的事件类型、监听函数、useCapture状态、优先级(priority参数)都列出来:
容器ID | 事件类型 | 监听函数 | useCapture | 优先级 |
---|---|---|---|---|
Application | click | appClickHandler | false | 0 |
Layout | click | layoutClickHandler | false | 0 |
Sidebar | click | sidebarClickHandler | true | 10 |
Map | click | mapClickHandler | false | 0 |
Marker | click | markerClickHandler | false | 0 |
一看就明白:Sidebar的click事件用了useCapture=true(捕获阶段触发),而且优先级是10(比默认的0高),所以不管点哪里,Sidebar的事件都会先触发——这就是为什么Marker的事件被“插队”了。坑点2:Flex的事件监听优先级(priority)越大,触发越早,默认是0,别乱设高优先级。
第三步:剥离冗余逻辑,把“控制权”抢回来
理清链路之后,就要“动手术”了。我当时给朋友提了三个方案,他选了最稳妥的那个:
event.stopImmediatePropagation()
,阻止后续的事件(包括同阶段的其他监听和冒泡); 朋友选了方案1,改完之后,事件的触发顺序变成了:
触发了Application的click事件(捕获)
→触发了Layout的click事件(捕获)
→触发了Marker的click事件(目标)
→触发了Map的click事件(冒泡)
→触发了Sidebar的click事件(冒泡)
→触发了Layout的click事件(冒泡)
→触发了Application的click事件(冒泡)
这下对了!Marker的事件终于先触发了,信息窗弹出来,之后才触发Sidebar的折叠——朋友当时拍着桌子说:“我之前怎么没想到改useCapture!”
坑点3:stopImmediatePropagation和stopPropagation的区别要搞清楚——stopPropagation是阻止冒泡,但不影响同阶段的其他事件;stopImmediatePropagation是连同阶段的其他事件都阻止,比如同一个按钮加了两个click监听,用这个方法,第二个就不会触发了。我之前就犯过这个错,用了stopImmediatePropagation,结果把自己加的另一个事件也拦了,排查了半天才找到原因。
第四步:验证效果,别改完就完事
改完代码一定要验证!我当时让朋友做了三个测试:
确认没问题,才敢上线。坑点4:改事件逻辑时,一定要测试所有关联功能,别只测目标场景——我之前改一个项目,只测了点击按钮,结果没测输入框的回车事件,上线后发现输入框回车没反应,又紧急回滚,特丢人。
你看,其实FlexViewer的事件机制剥离真的不是什么“黑魔法”——先掰碎逻辑,再找到链路,最后动手拆冗余。我当时帮朋友搞定之后,他的项目BUG率降了40%,用户反馈“地图终于听话了”。
如果你按我讲的步骤试了,不管成没成,都欢迎在评论区告诉我——我帮你看看哪里出问题。 踩过的坑,能让别人少踩一个,就值了。
Flex事件流的三个阶段具体是怎么“流”的?
你可以把Flex界面想成俄罗斯套娃,最外层是Application,里面一层层嵌套容器和组件。事件流分三步:首先是“捕获阶段”,事件从最外层Application开始往下钻,像快递员从快递公司出发逐层找收件人,比如Application→Layout→Map→按钮父容器→按钮,这一步是“从上到下”;然后是“目标阶段”,事件终于到你点击的按钮(目标元素),触发按钮自己的事件函数,比如你写的btnClick;最后是“冒泡阶段”,事件处理完目标元素往回跑,从按钮→父容器→Map→Layout→Application,像快递员送完件逐层返回。
举个例子,点击按钮时,捕获阶段是Application先“问”要不要处理,然后是Layout、Map,直到按钮父容器;目标阶段按钮自己的函数触发;冒泡阶段又从按钮往上“问”每个容器要不要再处理。
怎么快速找到FlexViewer事件的“发源地”?
我常用的笨办法是“加trace语句”——在可能的容器(比如Application、Layout、Map、按钮)的click事件里加trace("触发了" + this.id + "的click事件")
,运行后看输出顺序,就能知道事件先触发了哪个容器。比如朋友项目里点击标注,输出是Application→Layout→Sidebar→Map→Marker,就能发现Sidebar的事件比Marker早。
如果觉得trace麻烦,也能用Flash Builder的调试工具,在事件函数里打个断点,运行时看调用栈——栈顶的函数是最先触发的,顺着栈往下走就能理清事件路径。
给父容器加useCapture=true,为什么会“截胡”子元素的事件?
useCapture=true会让事件在“捕获阶段”就触发父容器的函数。比如父容器是Sidebar,子元素是Marker,当你点击Marker时,捕获阶段是从上到下,Sidebar作为父容器会比Marker更早接收到事件。如果Sidebar的useCapture设为true,它的事件函数会在捕获阶段就触发,比Marker的“目标阶段”还早,相当于“截胡”了子元素的事件。
就像快递员还没到Marker这个“收件人”,先被Sidebar这个“父容器”截住了快递,Marker自然收不到——这就是为什么朋友项目里Marker的事件没触发,因为Sidebar用了useCapture=true。
stopPropagation和stopImmediatePropagation有什么不一样?
简单说,stopPropagation是“阻止冒泡”,但不影响同一阶段的其他事件。比如按钮加了两个click函数,用stopPropagation只会阻止事件往父容器跑,两个函数还是会依次触发。而stopImmediatePropagation更“狠”,不仅阻止冒泡,连同一阶段的其他事件都不让触发——比如同一个按钮的两个click函数,用它的话,第一个函数触发后,第二个就不会执行了。
我之前犯过错,给按钮加了两个click函数,想用stopImmediatePropagation阻止冒泡,结果第二个函数也没触发,后来才搞清楚两者的区别——如果只是想阻止父容器接收到事件,用stopPropagation就够了。
改完FlexViewer事件逻辑后,为什么一定要全面验证?
因为事件逻辑是关联的,你改了一个容器的事件,可能影响其他功能。比如我之前改一个项目,只测了按钮点击没问题,结果没测输入框的回车事件,上线后发现输入框回车没反应,只能紧急回滚。
像朋友的项目,改完Sidebar的useCapture后,要测三个场景:点击Marker看信息窗是否先弹、点击Sidebar空白处看折叠是否正常、点击Map空白处看缩放是否正常。只有所有关联功能都没问题,才能确保改对了——毕竟“只测目标场景”很容易漏掉隐藏的BUG。