问题
前两天在掘金大模型子站遇到一个问题,使用 $0.click()
模拟点击弹窗的关闭按钮时,出现报错 click is not a function
且弹窗没有关闭,原因是关闭按钮使用的 svg
元素没有 click
方法。
随后使用 $0.dispatchEvent(new MouseEvent('click'))
,代码没报错但是弹窗没有关闭。
解决
经过调试分析发现,将 new MouseEvent('click')
的第二个参数设置为 {bubbles: true}
就能关闭弹窗,成功代码: $0.dispatchEvent(new MouseEvent('click',{bubbles: true}))
。
原因
- 1、
HTMLElement.click()
失败原因:因为svg
元素没有click
函数 - 2、
EventTarget.dispatchEvent(new MouseEvent('click'))
失败原因:掘金大模型子站使用React
编写,模拟点击事件没有冒泡,未能触发React
onClick
事件React
的合成事件区分捕获与冒泡,onClick
只在冒泡阶段被触发,捕获阶段不会触发EventTarget.dispatchEvent(new MouseEvent('click'))
分发的事件没有冒泡阶段,所以没有触发React
onClick
事件- 代码修改为
EventTarget.dispatchEvent(new MouseEvent('click',{bubbles:true}))
,分发的事件既有捕获阶段又有冒泡阶段,可以触发React
onClick
事件
分析要点
搭建最简复现环境
为了解决调试不便和影响因素过多等问题,在 codesandbox
搭建了一个最简复现环境,关键代码如下:
// App.js
export default function App() {
function clickHandler() {
console.log("onClick");
}
return (
<div className="App" onClick={clickHandler}>
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
对比分析
在上述环境中,HTMLElement.click()
可以正常调用 clickHandler
函数,而 EventTarget.dispatchEvent(new MouseEvent('click'))
没有调用 clickHandler
函数。
如果嗅觉足够灵敏,再搭建一个类似的原生
JavaScript
复现环境,很快就能发现前面这两种方式都能调用click
事件绑定的函数,只是事件对象属性略有不同,也能很快发现问题出在new MouseEvent
的第二个参数上。
分析调用栈
为了排查失败的真正原因,可以在 clickHandler
函数中添加断点,对调用栈中的函数逐个排查。
关键源码分析
事件队列不同
经过排查,在 React
代码 react-dom.development.js
文件中的 dispatchEventsForPlugins
函数发现了端倪:
- 使用
$0.click()
触发点击事件,该函数被执行两次- 第一次函数中的变量
dispatchQueue
为空数组,不会调用clickHandler
函数 - 第二次函数中的变量
dispatchQueue
不为空,会调用clickHandler
函数
- 第一次函数中的变量
- 使用
$0.dispatchEvent(new MouseEvent('click'))
触发点击事件,该函数被执行一次,函数中的dispatchQueue
为空数组,不会调用clickHandler
函数
获取监听器的名称与结果不同
从上图可以看到 dispatchQueue[0].listeners[0].listener
的值就是 App.js
中的 clickHandler
函数。这里的 dispatchQueue
可以简单理解为即将执行的事件监听器组成的队列,队列不为空时其中的监听器会逐一执行。
接下来的关键点是搞清楚队列 dispatchQueue
如何被填充,排查发现是被 extractEvents$5
函数所修改与填充,队列中元素的 listener
是通过 getListener
函数获得:
- 使用
$0.click()
触发点击事件,getListener
函数被执行两次- 第一次函数的入参
registrationName
值为onClickCapture
,返回结果为undefined
- 第二次函数的入参
registrationName
值为onClick
,返回结果为App.js
中的clickHandler
函数
- 第一次函数的入参
- 使用
$0.dispatchEvent(new MouseEvent('click'))
触发点击事件,getListener
函数被执行一次,函数的入参registrationName
值为onClickCapture
,返回结果为undefined
通过以上分析可以推断出问题的根本原因。
总结
- 模拟点击事件常用两种方式:
HTMLElement.click()
和EventTarget.dispatchEvent()
- 使用
EventTarget.dispatchEvent()
时,记得给事件加上冒泡避免踩坑 React
的合成事件区分捕获与冒泡,onClickCapture
在捕获阶段触发,onClick
在冒泡阶段触发,其他事件类似
EventTarget.dispatchEvent(new MouseEvent('click'))
在Vue
和Angular
中可以正常调用
相关资料
- Event Flow:https://www.w3.org/TR/DOM-Level-3-Events/#event-flow
- React capture:https://zh-hans.react.dev/learn/responding-to-events#capture-phase-events
- React events:https://zh-hans.legacy.reactjs.org/docs/events.html#supported-events
- React 源码
dispatchEventsForPlugins
: https://github.com/facebook/react/blob/8039e6d0b93b642a4bd1ee7dbe108d2153fd2981/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js#L290 - React 源码
getListener
: https://github.com/facebook/react/blob/8039e6d0b93b642a4bd1ee7dbe108d2153fd2981/packages/react-dom-bindings/src/events/getListener.js#L51