带你了解 Vue 3 事件绑定的具体细节

794 阅读7分钟

介绍

Vue 3 中通过 v-on 给元素绑定监听器,官网的介绍如下:

事件类型由参数来指定。表达式可以是一个方法名,一个内联声明,如果有修饰符则可省略。

当用于普通元素,只监听原生 DOM 事件。当用于自定义元素组件,则监听子组件触发的自定义事件。

当监听原生 DOM 事件时,方法接收原生事件作为唯一参数。如果使用内联声明,声明可以访问一个特殊的 event变量:von:click="handle(ok,event 变量:v-on:click="handle('ok', event)"。

v-on 还支持绑定不带参数的事件/监听器对的对象。请注意,当使用对象语法时,不支持任何修饰符。

Vue 3 整个事件系统分为两部分,首先通过编译器解析模版并生成一个渲染函数,渲染函数返回要渲染的虚拟 DOM,模版的 v-on 绑定的事件就会在这边部分转为虚拟 DOM 对象的 props 中的属性。

code.png

通过编译器解析生成一下 render 渲染函数:

code4.png

p 元素的虚拟 DOM 大概结果如下:

code1.png

事件作为属性放入 props 中,并且前面还加了 on。

编译器编译完了之后就要通过渲染器将虚拟 DOM 渲染成真实 DOM,同时绑定事件。

本文主要讲渲染器中绑定事件的部分,编译器解析v-on指令后期会单独写一篇。

渲染器

在上一篇 【带你了解 nextTick 所有细节】 中简单解析了 Vue 3 的响应式的原理,其中在 set 方法中做派发更新,而渲染器就是其中重要部分更新 DOM。

code2.png

我们将上面代码改写成一个简单的渲染虚拟 DOM 的渲染器。

code3.png

vnode 是我们要渲染的虚拟 DOM ,container 是要挂载的容器。

Vue 3 渲染器的目标是设计一个多平台的渲染器,因此要将上面代码浏览器 API 进行单独封装。

code5.png

这里我们可以用懒函数的方式来返回一个浏览器 API 的渲染器,就不用每次都要传 browserOptions。

code6.png

我们在将渲染器功能扩展下,添加卸载操作和更新操作。

code7.png

如果新 vnode 为空并且旧 vnode 不为空就进行卸载操作,新 vnode 不为空就进行挂载或更新操作,这里 processElement 函数就是挂载和更新操作,我们再来看 processElement 函数里面的代码:

code8.png

n1 为空进行挂载操作,n1 不为空进行更新操作,更新操作这里就不详细说明,在 diff 篇在详细说明。我们先来实现简单的挂载操作 mountElement 函数。

code9.png

完整代码:

code10.png

这里只是简单的实现了渲染器,Vue 3 源码中的渲染器要复杂很多。

这里不做过多解析,重点在于对事件处理的部分,我们知道 v-on 既可以监听原生DOM事件,也可以监听子组件触发的自定义事件。两种处理方式是不一样的。那怎么区分这两种方式呢?

原生 DOM 事件

前面提到事件被作为属性放入 props 中,并且前面加了 on,因此事件的处理部分就和属性解析一起封装到 patchProp 函数中。

code11.png

patchEvent 函数用于实现事件绑定,我们先简单实现下基本的事件绑定。

code12.png

这里有个问题是,每次更新事件都需要调用 removeEventListener 函数来移除上次绑定的事件,我们可以优化下封装一个事件处理函数 invoker ,真正的事件处理函数放入 invoker.value 属性值,直接执行 invoker.value ,更新事件时只要更新invoker.value 的值就行了。

code13.png

一个元素可能绑定多个事件,invoker.value 就会被覆盖掉,这里将 invoker 改为对象,并且 invoker.value 改为数组。

code14.png

事件冒泡

这种方式加上响应系统会导致事件冒泡出现问题。先举个简单例子:

code19.png

单击p元素打印结果如下:

code20.png

打印结果看出事件冒泡到父div元素执行了微任务中给 div 元素绑定的单击事件,因为事件触发是放入宏任务中,因此会先执行微任务中的函数,再冒泡触发父div元素的事件。

上面实现的事件绑定也有同样的问题:

code15.png

代码中声明了响应式数据 bol,bol 为 true 时 div 绑定单击事件 doThat ,子元素 p 绑定了单击事件 doThis 将 bol 设置为 ture,预期效果是第一次单击 p 元素是不会触发 doThat ,第二次单击才会触发。但实际效果是第一次就会触发 doThat。这是因为第一次单击触发 doThis 函数设置 bol 值连带触发了 bol 的 setter 方法,将副作用函数的执行放入微任务队列中,而事件冒泡触发的事件执行是在宏任务队列中,先执行渲染器,doThat 先绑定到 div 上,之后事件冒泡触发 div 的单击事件,因此第一次单击 p 元素也触发 doThat。

Vue 3 的解决方案的需要知道一个知识点才能理解:事件冒泡到父元素触发的事件的事件对象和当前元素触发事件的事件对象是一个对象。比如以下例子:

code17.png

单击 p 元素打印的结果是

code18.png

也就是在 p 元素的单击事件中给事件对象添加 _vts 属性为1,在事件冒泡到父元素 div 的事件中,能在事件对象上获取到 _vts。

但我在网上没有找到相关的资料说明。找到的同学可以在评论区留个地址么?

Vue 就是利用这点给事件对象添加 _vts 用于保存事件触发的时间戳,然后进行判断如果触发时间是早于绑定事件就不执行。

code16.png

修饰符

v-on 事件绑定的修饰符又是如何实现的呢?

修饰符大部分是关于事件对象的操作。我们在事件处理外在封装一个函数。

code25.png

编译器编译成 vnode 的时候,如果有修饰符,类似下面的结构:

code26.png

会先执行 withModifiers 返回一个新的包含对应修饰符处理逻辑的函数。

组件自定义事件

组件解析成 vnode 的 type 属性存储的时组件的选项对象。因此在渲染器中添加对 key 类型做不同处理。

code21.png

我们在将 type 的判断逻辑单独封装成 patch 函数

code22.png

我们在来看看 processComponent 函数的实现:

code23.png

自定义事件也是解析到 props 中,emit 触发对应的自定义事件,我们再来看看 emit 是如何触发的。

code24.png

在 mountComponent 函数中声明了 emit 函数,寻找 props 中对应的事件函数并执行,从 emit 函数中可以看到触发自定义事件时同步执行的。

修饰符 .native 的误区

网上很多说 Vue 的所有事件都是模拟事件,给出的理由是修饰符 .native 是绑定原生事件的。

甚至我在面试的时候,面试官也表达了在原生标签上绑定事件和组件绑定自定义事件是一样的,div 标签也可以看成是特殊组件这种观点。

在 Vue 3 中移除了.native 修饰符,官方给的解释是:

2.x 语法

默认情况下,传递给带有 v-on 的组件的事件监听器只能通过 this.$emit 触发。要将原生 DOM 监听器添加到子组件的根元素中,可以使用 .native 修饰符。

3.x 语法 v-on 的 .native 修饰符已被移除。同时,新增的 emits 选项允许子组件定义真正会被触发的事件。

因此,对于子组件中未被定义为组件触发的所有事件监听器,Vue 现在将把它们作为原生事件监听器添加到子组件的根元素中。

.native 修饰符的作用只是在组件中绑定原生事件用的,因为 Vue 2 的组件的事件绑定默认就是自定义事件。而原生标签的事件绑定并不是模拟事件,内部只是对事件做了一层代理,绑定的还是原生事件。