vue中的事件:原生事件与自定义事件

4,995 阅读5分钟

模板编译 processAttrs

对于ast attributes处理(v-on/@)

利用onRE与dirRE来捕获事件

这里最重要的就是dynamic的判断,vue中可以用动态参数来命名事件名称,如@[prop],prop为data中的值。不过通常都是一个静态的事件名称如 @click

另一个核心方法就是addHandler

addHandler 往AST上添加events属性

1. 这里会对动态事件名称进行一些处理,也会对right,middle修饰符处理,另外对capture, passive, once的事件名称做了标记。分别添加 !, ~, # 符号。然后删除了对应属性值

2. 参数value为@click="handler"例子中的handler,在此处还在编译阶段handler本质上是一个string。然后对value做了处理,记录了模板解析event时候start和end的位置。

3. 对于单个事件有多回调函数绑定的情况,添加了一个important参数,以此来提前触发当前回调函数的执行

4. 另外,这里对鼠标的right和middle做了处理,在处理只有删除了对应属性值。对于键盘的事件没有做处理

AST -> code

修饰符:modifierCode

上面已经提到过了,本质上模板编译的时候会利用正则处理各种修饰符,然后根据对应关键词的生成代码。如常用的stop,prevent。

核心方法就是src/compiler/codegen/events.js的genHandler方法,以下是分析:

1. genHandlers

    该方法就是简单的遍历events对象的键值对然后,对有无native修饰符与是否为dynamic事件做一个处理,其核心方法就是调用genHandler

genHandler

     一些参数判断

事件函数的多种写法

在官方文档中演示了事件回调函数的多种写法,这些写法都在模板编译过程中进行了识别

cn.vuejs.org/v2/guide/ev…

下方三个正则表达式是模板编译时对event写法的判断依据,下面会有更详细的注释

1. 路径类写法

2. 箭头函数,匿名函数

3. 表达式 handler($event), a = 1

在vue中通常通过@click="handle(event)"来获取event对象,其实这里是vue在模板中做了一层包裹,将function(event)"来获取event对象,其实这里是vue在模板中做了一层包裹,将function(event){}套在handle($event)外部。

有修饰符的情况

修饰符在ast生成的过程中就已经捕获了,vue中对event事件的修饰符处理如下

先对修饰符做一个处理

最后座一层包裹,因为这里对键盘事件也做了处理,因此一定要拿到event对象

genCode

在处理完这些之后会生成字符串on:"...."/nativeOn:"...",最后生成render函数

组件初始化

在前几篇文章中写了渲染watcher的内容:juejin.cn/post/685750…

组件初始化简单地说就是先对options做各种处理,最后执行渲染watcher,生成页面。

对native events的处理:

platforms/web/runtime/patch.js中有 

const patch = createPatchFunction({nodeOps, modules})

modules源自:web/runtime/modules/index,导出含生命周期的对象**(非created周期)**

createPatchFunction

createPatchFunction往hooks内添加了updateDOMListeners,hooks为**并非是**组件的生命周期函数。在组件create与updated的时候就会触发updateDOMListeners函数。注册事件。

注意点: 这里create并非是组件created的**周期函数。**是在真实节点创建之后才会触发钩子,因此是可以拿到真实节点的。

updateDOMListeners

在调用modules.create的hook的时候触发了updateDOMListeners

其作用就是给用addEventListeners与removeEventListeners方法给真实dom节点添加事件。

updateListeners

将新旧事件进行对比更新。这里的add和remove可以给真实dom注册事件,也可以给组价注册事件。

占位符$vnode的真实dom事件:createComponent(部分)

对于组件而言,data.on赋值给listeners,把data.nativeOn赋值给data.on

data.on里面存放着的都是原生dom事件,组件内部的listeners都是用户自定义的事件。

因此,在组件patch过程中,创建组件的根节点的时候,就会把data.on内部的原生dom事件注册在dom上。

因此如果在h5元素使用native如 <button @click.native="handler">,vue就会报错。这正是因为在这里做了处理,只有占位符vnode才可以有data.nativeOn的属性。是h5标签的节点不会调用createComponent方法,其data.on在创建节点的时候会绑定到节点上。

自定义事件(只针对组件间)

由createComponent函数可知,listeners存放了自定义事件。

在父子通讯的时候父组件只要v-on/@eventName,就可以监听到子组件emit出来的事件。

_init: initInternalComponent

创建子组件的时候会把占位符vnodevnode的listeners传递给子组件的$options**(此时data.on已经是data.nativeOn,原始的data.on赋值给变量listeners)**

_init: initEvents

调用updateComponentListeners方法,最后还是调用了updateListeners方法(见前面文章注释)。

但是这里不同的是add和remove方法并非是document.addEventListener和document.removeEventListener

add/remove(vue中的发布订阅模式):

其实发布订阅模式比较简单,就不详细说明了,主要是add/remove方法。

小结

其实本质上,还是将父组件注册的回调函数传给了子组件的_events对象(让该函数存在于子组件中),但是看起来像是子组件调用了父组件的方法

eventBus

对于跨组件的组件通信,利用了vue实例可以有自身的_events对象,因此在Vue原型上创建一个空的vue实例,然后将vue所有组件上的函数都注册在这个实例对象的_events对象上,一次达到跨组件通信的目的

Vue.prototype.$bus=new Vue()

第三种情况:v-on="$listeners"

cn.vuejs.org/v2/guide/re…

listeners就是vnode.data.on的别称,因此通过listeners就是vnode.data.on的别称,因此通过listeners就可以拿到父组件注册的非native事件。在爷孙组件通信的是可以使用$listeners通过父级组件将爷孙组件关联。

grand组件:

father组件:  // 此处是grand的事件

son组件:this.emit("test")//fatheremit("test") // 将father的listeners传入son,而father的$listeners包含grand的事件,因此就将grand的事件传入了son中。

AST解析

在模板解析的时候会用正则匹配v-on,对于v-on="$listeners",vue将这种写法视为指令(directive),有不同的策略

on指令

_g

这里将$listeners对象与data.on进行合并,通过v-on指令我们可以一次性对组件注册多个事件。

最后

vue的事件基本上就是这样,其实这里面牵扯到了最核心的组件初始化和更新流程,关于此部分在本文中并没有明确说明,只是大概提了一下,一是要单纯靠文字说明费时费力(代码含有大量递归),而是本文重点是关注vue中的事件,在后面的更深入了解整个机制后会尝试说明写一篇vue组件初始化及更新的文章。