ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vue事件对象详解:掌握$event与修饰符,彻底搞懂交互细节

Vue事件对象详解:掌握$event与修饰符,彻底搞懂交互细节 写Vue事件处理时有一个东西绕不开事件对象Event Object。你写过clickhandleClick然后在方法handleClick(e)里拿到的那个e就是它。这个对象里装着与本次交互有关的全部现场信息——你点的是哪个元素、点击坐标是多少、有没有按住Shift、哪个键被按下了。标题里把它叫“现场报告”很贴切事件发生的那一刻浏览器把所有与本次交互相关的信息打包成一个对象随回调一起交给你。这篇博文我会重点讲清楚Vue.js 2.x里事件对象的使用方式什么场景会自动传、什么场景要用$event、事件修饰符和事件对象之间是什么关系、组件通信里事件对象又该怎么理解最后给出一套可以直接跑起来的实操示例和常见问题排查经验。适合刚把Vue基础语法过完、正在写列表、表单和组件交互的读者也适合那些被$event和.native搞晕过的人。1. 事件对象到底是什么先把它当“现场报告”看待1.1 为什么需要事件对象事件处理器的“第三只手”很多人一开始写点击事件都是这样methods: { handleClick() { // 只知道自己被点了其他一概不知 } }然后很快就会发现需求变得复杂要记录用户点中了哪个列表项、要判断点击是否发生在某个区域内部、要在回车键按下时提交表单、要在右键时弹出自定义菜单……这些信息如果全靠“手动维护状态”去猜代码很快就会变成一团乱麻。事件对象就是为此而生的。可以把它理解成一份“快递面单”事件是快递员按门铃这件事面单上则写着寄件人、收件人、物品类型、签收时间、派送位置。事件对象就是那张面单浏览器在触发事件的瞬间把target目标元素、clientX屏幕坐标、key按键值、type事件类型这些信息全部塞进去再交给你的处理函数。没有这张面单你就只能隔着门喊话什么有效信息都拿不到。这个设计不是Vue独有的。Vue的底层依赖浏览器原生的DOM事件体系事件对象本身来自Web API。理解这一点很重要哪怕你的页面里全是Vue组件事件对象依然是浏览器给你的一份“原生现场记录”Vue只是帮你把这个对象传得更顺手而已。1.2 浏览器Web API里的原生事件对象核心成员一览原生事件对象有很多类型鼠标点击产生的是MouseEvent键盘按下产生的是KeyboardEvent输入框内容变化产生的是InputEvent元素获得焦点产生的是FocusEvent。它们都继承自基类Event同时各自带有专有属性。实际开发中最常用的成员我用一张表帮你梳理成员所属类型作用典型场景typeEvent事件类型如click、keydown同一函数处理多种事件时做分支targetEvent实际触发事件的元素判断用户到底点中了谁currentTargetEvent当前监听器所在元素区分“事件源”和“监听者”clientX/clientYMouseEvent相对浏览器可视区域的坐标弹层定位、拖拽起始点pageX/pageYMouseEvent相对整个文档的坐标结合滚动条计算真实位置offsetX/offsetYMouseEvent相对目标元素内容区的坐标精确定位图片热点keyKeyboardEvent按键的字符值如Enter判断按了哪个键codeKeyboardEvent物理键盘位置如KeyA区分左右ShiftdetailMouseEvent连续点击次数判断单击还是双击preventDefault()Event阻止默认行为阻止表单提交、阻止链接跳转stopPropagation()Event阻止事件冒泡子元素点击不触发父元素逻辑stopImmediatePropagation()Event阻止冒泡且阻止后续监听器同一元素上多个监听器时的强隔离提示target和currentTarget是最容易混淆的一对。以冒泡为例事件从最内层元素一路向外传播target始终指向最内层的那个元素而currentTarget在每一层都不一样——哪一层正在执行监听器它就指向哪一层。判断“用户点的是不是我”用target判断“监听器挂在谁身上”用currentTarget。Vue中用的$event就是这些原生事件对象中的某一个。它不是Vue重新发明的对象而是Vue透传给模板的浏览器事件对象。搞清楚了这一点后面所有“为什么Vue里的event有这些属性”“为什么键盘事件里有key没有keyCode”之类的疑问就都有了解释方向。2. Vue.js中获取事件对象自动传递与显式$event2.1 不带自定义参数时事件对象自动“送上门”Vue的v-on指令缩写在设计上做了很多“隐身”优化。当你在模板中写clickhandleClick没有给方法加括号、没有传参数时Vue会默认把事件对象作为第一个参数传入handleClick方法button clickhandleClick点我/buttonmethods: { handleClick(event) { // 这里的 event 就是原生事件对象 console.log(event.type); // click console.log(event.target); // button console.log(event.clientX); // 鼠标点击的 x 坐标 } }这个设计非常贴心因为大部分时候你想用的第一个东西就是事件对象本身。Vue在模板编译阶段会生成类似on: { click: function($event) { return handleClick($event) } }的渲染代码相当于替你完成了“把现场报告递进方法里”这个动作。我建议新手一开始养成一个好习惯事件处理方法的第一个参数固定命名为event或简写为e。虽然叫$event是模板里的写法但在JavaScript方法内部它就是一个普通参数命名随你保持一致的命名风格代码会更好读。2.2 带参数时用$event显式传递需求一复杂方法就要带自己的业务参数了。最常见的写法是button clickhandleClick(delete, 42)删除/button这时你如果在方法里只声明了两个形参是拿不到事件对象的methods: { handleClick(action, id) { console.log(action); // delete console.log(id); // 42 // 这里没有 event } }原因很简单Vue不会聪明到自动帮你“猜”你要把事件对象放在哪个位置。带了自定义参数后模板中就必须显式地用$event把事件对象“递”进去button clickhandleClick(delete, 42, $event)删除/buttonmethods: { handleClick(action, id, event) { console.log(action); // delete console.log(id); // 42 console.log(event.clientX); // 有现场报告了 } }$event在Vue模板中是一个特殊变量只在事件绑定表达式中有意义它的值就是当前触发事件的原生事件对象。你也可以把它想象成一个“现场报告专用通道”业务参数走普通通道现场报告走$event通道两者互不干扰。这个写法在列表渲染中非常常用比如v-for循环里既要传item又要判断点击位置li v-foritem in list :keyitem.id clickselectItem(item, $event) {{ item.name }} /li2.3 模板内联表达式里直接访问event还有一种情况容易被忽略在模板内联表达式里你实际上可以直接写event不带$。比如p clickshowTooltip !showTooltip; lastClick event.clientX点我试试/p这里event不是Vue的语法而是浏览器在事件处理函数作用域中暴露出来的window.event。Chrome等主流浏览器对它做了兼容性支持所以这样写通常也能拿到事件对象。但我强烈不建议你在生产代码里依赖它window.event是历史遗留的非标准实现不是所有环境都可靠。模板内联表达式里塞一堆逻辑可读性会很快崩。一旦将来要重构把内联代码移进方法时很容易漏掉这个隐式依赖。正确的替代方案很简单内联表达式中想用事件对象就明确写$event。Vue编译器会自动把它注入到表达式参数中不依赖window.event行为在所有浏览器上都一致p clicklastClick $event.clientX点我试试/p3. 用事件修饰符精准控制交互顺序与底层逻辑3.1 修饰符链的写法与执行顺序Vue为事件绑定内置了一套修饰符系统你用.号把它们串联在事件名后面。比如a click.stop.preventhandleLink跳转/a这句的意思点击链接时先调用event.stopPropagation()阻止冒泡再调用event.preventDefault()阻止默认跳转然后执行handleLink方法。修饰符从左到右依次生效它们本质上就是帮你把事件对象上的常见操作封装成了声明式语法。为什么用stop和prevent这样的单词而不是让你自己在方法里写因为很多交互细节和业务逻辑无关比如阻止冒泡、阻止表单提交、限制触发一次。这些行为如果混在业务方法里每个方法都要重复写一遍还要小心别写错顺序。Vue把它收敛到模板层面让方法里的代码更干净也让“这个事件对应哪些约束”一目了然。3.2 高频修饰符逐个拆解常用修饰符和它们对应的底层操作我整理成一张对照表修饰符底层操作使用场景.stopevent.stopPropagation()子元素按钮点击不希望父级容器的点击逻辑也触发.preventevent.preventDefault()表单submit不刷新页面链接不跳转.self只有event.target event.currentTarget才触发点击容器本身才响应点容器里的子元素不响应.capture用捕获阶段监听需要在事件到达目标之前拦截.once事件只触发一次之后自动解绑首次点击后无限制的引导提示.passive告诉浏览器“我不会调用preventDefault”滚动监听提升移动端滚动流畅度这里要重点说.passive。移动端touchmove滚动卡顿的一个常见原因就是浏览器不确定滚动监听器会不会调用preventDefault所以要等监听器执行完再决定要不要滚动。加了.passive相当于提前告诉浏览器“我保证不阻止你”浏览器就能放心地立刻滚动体验会顺滑很多。反过来说.passive和.prevent不能同时使用——一个保证不阻止一个要阻止逻辑直接冲突浏览器会警告。.self这个修饰符也值得留意。它判断的是target和currentTarget是否相等。也就是说只有你点击的那个元素恰好就是监听器挂载的元素时处理器才执行。这在做“点击遮罩层关闭弹窗”之类的效果时非常有用div classmodal-mask click.selfcloseModal div classmodal-content弹窗内容/div /div点遮罩层空白处关闭点弹窗内部不关闭不用在方法里再写判断。3.3 按键修饰符与系统修饰键键盘事件对象里最常用的属性是keyVue允许你直接用按键名作为修饰符input keyup.entersubmit / input keyup.esccancel /这比在方法里写if (event.key Enter)简洁多了而且语义清楚。Vue提供了一组内置按键别名.enter、.tab、.delete、.esc、.space、.up、.down、.left、.right。注意.delete同时覆盖了Delete和Backspace两个键这在做输入框清除操作时要小心。还可以自己扩展别名// 把 F2 命名为 .f2 Vue.config.keyCodes.f2 113;这样模板里就能写keydown.f2edit了。自定义别名在项目里要统一维护否则团队里每个人定义一套代码就乱了。系统修饰键是另一类.ctrl、.alt、.shift、.metaMac上是Cmd键Windows上是Win键。它们可以和普通按键组合textarea keydown.ctrl.entersendMessage发送/textarea这句的含义按下Ctrl不松手再按下Enter才触发sendMessage。实际动作是“组合键提交”比单纯监听回车键更精确。还有一个容易踩坑的点keyup.ctrl单独使用几乎很难触发。因为keyup事件触发的时候Ctrl键已经松开了事件对象里的ctrlKey为falseVue检查不通过。如果你确实要监听“松开Ctrl”这个动作需要keyup.ctrl配合keyup.ctrl判断但更常见的做法是监听keydown而不是keyup。我在实际项目里通常会优先选keydown因为keydown的可预测性更好尤其在做快捷键时。3.4 一个差点坑哭我的顺序问题prevent和self连写时顺序不一样结果会差很多。这是初学者最容易忽略的细节之一。!-- 方式A先 prevent 再 self -- a click.prevent.selfhandler链接/a !-- 方式B先 self 再 prevent -- a click.self.preventhandler链接/a方式A的含义先阻止所有点击的默认行为然后再判断target是不是当前元素。注意“所有点击”包括从子元素冒泡上来的点击——也就是说子元素上发生的点击默认行为一样会被阻止。方式B的含义先判断target是否等于currentTarget只有点击当前元素本身时才去阻止默认行为。如果点击的是子元素就不会阻止默认行为。我在一个导航菜单组件里就踩过这个坑。我在每个菜单项上写了方式A结果子级菜单上的链接全部失效——明明应该由子菜单自己处理跳转父级的prevent却把默认行为提前拦掉了。排查了很久才发现是修饰符顺序问题。记住这个口诀需要“无条件拦截”就用prevent放前面需要“先确认是自己的事再拦截”就用self放前面。修饰符虽然看着像小细节一旦组合起来顺序就是行为契约。4. 在组件边界上传递“现场报告”事件对象在组件通信中的角色4.1 自定义事件里的事件对象不是你以为的那个前面说的$event都是原生DOM事件里的。但Vue中还有另一类事件子组件通过$emit主动抛出的自定义事件。父组件监听这类事件时拿到的$event不是原生事件对象而是子组件$emit时传出来的数据。// 子组件 methods: { onChange() { this.$emit(change, hello from child); } }!-- 父组件 -- my-component changehandleChildChange /methods: { handleChildChange(payload) { console.log(payload); // hello from child } }这里的payload可以是字符串、对象、数组甚至可以是一个原生事件对象如果子组件有意识地把它传出来但它并不是自动变成原生的那条$event。我见过好几个人在父组件里写changehandleChildChange($event.target.value)以为拿到的是表单输入事件——实际上子组件如果没有把原生事件对象透传出来这里的$event根本就不是一个事件实例取target自然就是undefined。理解这个区别的关键是原生事件与自定义事件是两个独立通道。原生事件发生在DOM元素上事件对象由浏览器创建自定义事件发生在组件实例上事件数据由组件开发者自己决定。Vue在这两套体系里都叫$event是命名上的巧合和统一底层却是两套逻辑。4.2 监听组件内部原生事件.native与$listeners在Vue 2中直接在组件标签上写click默认监听的是“自定义事件”而不是组件根元素上的原生点击。如果你这样写my-button clickhandleClick按钮/my-button你的本意可能是“点击按钮组件触发handleClick”但my-button内部如果从来没有$emit(click)那handleClick永远不执行。你听到的点击声只是组件根元素上自然发生的原生click事件Vue并不会自动帮你把它转发为自定义事件。想要直接监听组件内部的原生事件需要加.native修饰符my-button click.nativehandleClick按钮/my-button加了.nativeVue就会把这个监听器挂到组件根元素的原生事件上。这在封装的第三方组件、UI库组件里非常常见直接用click.nativehandleClick监听库组件根元素的点击。.native有一个容易出问题的场景如果组件根元素被v-if临时切换成另一个元素或者组件结构里包了一层transition.native监听可能会失效因为监听器是在“某个根元素”上根元素变了监听可能就丢了。更稳妥的做法是利用$listeners在子组件内部显式透传原生事件监听器!-- 子组件模板 -- div v-on$listeners.../div这样父组件写的所有事件监听器包括不带.native的都会落到子组件模板内指定的元素上行为更可控也不会因为根节点变化而丢失。我在封装基础组件时基本都是这个思路。4.3 自定义v-model与事件对象的联动v-model本质上也是事件机制的语法糖。以表单元素为例v-modelquery大致等价于input :valuequery inputquery $event.target.value /这里的$event是原生InputEvent$event.target.value就是从输入框里取出的最新文本。你可以看到Vue在v-model背后默默地帮你处理了一次“事件对象取值”的动作。如果你在自定义组件上实现v-model就要自己掌控事件数据和事件对象的关系// 子组件 props: [value], methods: { selectValue(val) { // 注意这里 emit 出去的是业务数据不是事件对象 this.$emit(input, val); } }这时父组件v-modelselected能拿到的是val不是事件对象。如果你确实需要事件对象里的坐标、按键信息就要把两者一起传出去this.$emit(input, val, event);父组件对v-model内部帮你生成的事件处理器是无感知的这种做法一般只在特殊定制场景用日常业务里不推荐混着传增加理解成本。事件对象是用于交互细节的业务数据是用于状态的两者最好各走各的道。5. 实操过程构建一个“事件现场”调试台5.1 搭建一个最小可运行的环境为了让事件对象的各个属性更直观我通常建议用“事件现场记录台”的方式练手页面上一个区域负责产生各种事件另一个区域负责把事件对象的关键信息渲染出来。不需要安装任何复杂脚手架一个HTML文件加Vue的CDN就能跑!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleVue Event Object 调试台/title script srchttps://cdn.jsdelivr.net/npm/vue2.6.14/dist/vue.js/script style .stage { width: 400px; height: 200px; background: #f5f7fa; border: 1px dashed #aaa; } ul li { margin: 4px 0; font-size: 13px; } /style /head body div idapp !-- 产生事件的舞台 -- div classstage clickrecordClick dblclickrecordDblClick 点我、双击我、在上面按键盘 /div !-- 记录展示区 -- ul li v-foritem in logs :keyitem.id{{ item.msg }}/li /ul !-- 键盘事件区 -- input classinput placeholder按 Enter 提交按 CtrlEnter 发送 keydown.entersubmitByEnter keydown.ctrl.entersendByCombo / /div script new Vue({ el: #app, data: { logs: [] }, methods: { recordClick(e) { this.logs.unshift({ id: Date.now(), msg: ${e.type} - 目标:${e.target.tagName}坐标:(${e.clientX}, ${e.clientY})detail:${e.detail} }); }, recordDblClick(e) { this.logs.unshift({ id: Date.now(), msg: 双击 - detail:${e.detail}相对元素:(${e.offsetX}, ${e.offsetY}) }); }, submitByEnter(e) { this.logs.unshift({ id: Date.now(), msg: 回车提交 - key:${e.key}code:${e.code} }); }, sendByCombo(e) { this.logs.unshift({ id: Date.now(), msg: 组合键发送 - ctrlKey:${e.ctrlKey}key:${e.key} }); } } }); /script /body /html页面打开后你在灰色区域点击、双击在输入框里按Enter或CtrlEnter右侧列表会实时显示出对应的事件类型、目标元素、坐标和按键信息。这个简单的调试台本质上就是事件对象的“可视化仪表盘”。5.2 实战示例坐标追踪和组合键提交基于上面的调试台我再加两个贴近业务的场景。第一个是“右键自定义菜单”。业务中常见需求在列表项上右键弹出一个浮层菜单。这个功能里最关键的事件对象属性就是clientX和clientY它们直接决定菜单出现的位置ul li v-foritem in items :keyitem.id contextmenu.preventshowMenu(item, $event) {{ item.name }} /li /ulmethods: { showMenu(item, e) { // 菜单定位用点击坐标 this.menuStyle { left: e.clientX px, top: e.clientY px }; this.currentItem item; } }这里必须写.prevent否则浏览器原生的右键菜单会一起弹出来。你仔细看这个功能里同时用到了三个能力业务参数item、事件对象$event、修饰符.prevent。这正是事件对象知识点的集成应用。第二个是“组合键提交消息”。聊天场景里用户习惯按Enter换行按CtrlEnter发送。如果只用keydown.enter用户按一下Enter就发了显然不符合预期。正确写法是textarea keydown.enter.shiftinsertNewLine keydown.ctrl.entersendMessage /textarea系统修饰键放前面普通键放后面顺序从左到右匹配。enter.shift表示“ShiftEnter”用它换行ctrl.enter表示“CtrlEnter”用它发送。这里不需要在方法里取key判断修饰符已经把判断做完了方法里直接写业务逻辑即可。5.3 借助Vue DevTools看事件监听器排查事件问题时除了在方法里console.log(event)还有两个定位手段值得掌握。第一个是Vue DevTools。在插件安装好之后打开Vue面板操作页面时会看到右侧的组件树。选中某个组件面板里能看到对应的事件相关数据。DevTools对$listeners等信息的展示并不算细致但它能帮你快速确认“这个监听器到底挂在哪个组件上”。我在排查自定义事件失效的问题时第一步永远是检查DevTools里组件树上有没有对应的监听器项以及在Elements面板里选中DOM元素看浏览器自带的Event Listeners部分。浏览器DevTools的Elements - 右侧Event Listeners面板会列出某个DOM元素上挂载的所有事件监听器包括Vue通过addEventListener挂上去的那些。第二个是直接在事件处理方法里打印完整事件对象methods: { handleClick(e) { console.log(e); console.log(e.target, e.currentTarget); } }这里有个小技巧直接console.log(e)不要用JSON.stringify(e)。事件对象是浏览器原生对象很多属性是不可枚举的还有原型链上的方法JSON.stringify会丢失大量信息。直接打出来在控制台里展开它的属性列表才能看到完整现场报告。6. 常见问题与排查技巧实录6.1 为什么我在方法里拿不到event对象这是出现频率最高的一个问题。场景基本都一样模板里带了业务参数方法里也只留了业务参数button clickremoveItem(3)删除/buttonmethods: { removeItem(id) { console.log(id); // 3 console.log($event); // 报错方法里没有 $event 这个变量 } }原因就是第2节说的没有自定义参数时Vue自动传事件对象带了自定义参数后必须手动用$event占位。正确写法是button clickremoveItem(3, $event)删除/buttonmethods: { removeItem(id, event) { console.log(id, event.clientX); } }另一种情况是使用了箭头函数封装把事件“吞掉了”。比如button click() handleClick()按钮/button这种方式完全绕过了事件传递事件对象自然就丢了。除非你有明确的过滤需求否则不要在模板里写这种多余的箭头函数直接clickhandleClick最干净。6.2 事件处理器里的this指向为什么变了Vue 2的methods里的方法会被绑定到组件实例上所以在模板里clickhandleClick这样调用方法内的this是组件实例可以正常访问this.xxx。但有两个常见破坏点第一在methods里用箭头函数methods: { handleClick: () { console.log(this); // undefined 或 window } }箭头函数没有自己的this它会继承外层作用域的this而Vue并不能把箭头函数的this“重定向”到组件实例上。所以methods里应该统一用普通函数声明。第二在created或mounted里手动addEventListener监听原生事件时回调函数里的this指向元素或window不再是组件实例。需要在回调外层包一层箭头函数或提前用bind(this)把方法绑定好mounted() { window.addEventListener(keydown, this.onKeyDown); }, beforeDestroy() { window.removeEventListener(keydown, this.onKeyDown); }, methods: { // 注意这里用的是普通函数不是箭头函数 onKeyDown(e) { console.log(this); // 组件实例 } }Vue会在methods初始化时把每个方法bind到实例上所以this.onKeyDown本身已经绑定了你把它传给addEventListener时this依然是组件实例。如果不放心可以在beforeDestroy里显式移除监听避免内存泄漏。6.3 .native被“吃掉”了怎么办.native修饰符在Vue 2中好用但也有失灵的时候。常见原因有两个组件根元素发生了变化比如组件内部最外层是transition包裹的或者根节点被v-if切换了。组件不是单根节点而是多根节点虽然Vue 2官方不推荐但某些场景下会出现。.native的本质是“监听组件根元素的原生事件”一旦根元素变得不确定监听自然不稳定。解决办法就是前面提到的$listeners透传在组件内部明确指定把事件监听器挂到具体哪个元素上!-- BaseButton.vue 组件的模板 -- button v-on$listeners slot/slot /button这样父组件里直接写click就能生效因为监听器已经被透传到真正的button元素上了。注意这种写法监听的还是原生click事件不是自定义事件所以不需要$emit(click)。6.4 从Vue 2迁移到Vue 3时的事件对象变化如果你项目将来要升级到Vue 3有两个变化要提前知道。第一.native修饰符被移除了。Vue 3中组件上的v-on会一律被视为原生事件监听器事件会作为组件的prop传入并通过$attrs携带。如果子组件里没有显式地v-on$attrs或inheritAttrs: false配合处理监听器不会自动落到某个元素上。封装组件时需要更明确地透传。第二自定义事件的$emit需要在组件定义里声明emits选项// Vue 3 export default { emits: [change], methods: { onChange() { this.$emit(change, payload); } } }没有声明emitsVue 3会告警。但$event本身的使用方式和Vue 2保持一致原生事件对象、自定义事件数据、$event显式传递这些核心概念都没变。你在Vue 2里学的事件对象知识迁移到Vue 3时绝大部分可以直接复用。我在实际项目中调试事件相关bug时最大的体会是永远不要只盯着业务代码逻辑先确认“我手里的这个$event到底是原生事件对象还是自定义事件数据”。这个定位一旦错了后面排查方向就会完全跑偏。把事件对象当成一份现场报告先看清楚报告内容再决定怎么处理交互代码就会变得清晰可控。最后再分享一个小技巧排查事件问题时在方法第一行写console.log(e)然后对着控制台把target、currentTarget、clientX、key、detail逐项看一遍比在脑内推测快得多。事件对象是调试交互bug最直接的入口用好它省下的时间是实实在在的。
返回列表