模拟点击,踩到React事件的坑

问题

前两天在掘金大模型子站遇到一个问题,使用 $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 搭建了一个最简复现环境,关键代码如下:

javascript 复制代码
// 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 中可以正常调用

相关资料

相关推荐
anew___8 分钟前
《从零手写操作系统 (20):ELF加载器——让OS读懂现代编译器》
java·服务器·前端
ss2739 分钟前
AI全栈实战 | 2.3-01 组件库实战:Element Plus 怎么搭出专业界面,过度封装为什么成维护负担
前端·javascript·elementui·vue
对空六课15 分钟前
动态按钮ID变化时的稳定埋点标识设计
前端·数据分析
福兮说18 分钟前
URL 编码的七个坑:c++ 传到后端变空格、%25 套娃、截断 emoji 直接报错
开发语言·前端·javascript·node.js·url
吴声子夜歌24 分钟前
Nginx应用与运维——Nginx Web服务应用实战(Python网站的搭建)
运维·前端·nginx
小的时候可菜了42 分钟前
JS 异步的本质:从单线程到 thenable(一条进化链)
开发语言·前端·javascript
liangshanbo12151 小时前
Webpack vs Vite 底层原理高频追问的问题
前端·webpack·node.js
可乐鸡翅yeah_1 小时前
hls.js 配置 loadStartPosition,点播起始播放位置踩坑
开发语言·javascript·网络协议·ecmascript·m3u8在线
web打印社区1 小时前
Windows 网页静默打印设置步骤:客户端、防火墙与联调清单
开发语言·前端·javascript·chrome·pdf·ecmascript
恋猫de小郭1 小时前
KMP 又改了编译流程, Separate Compilation 禁止了 `commonMain` 的依赖穿透
android·前端·flutter