Vue之event(事件)

目标:1、了解event的实现原理。2、了解Dom事件和自定义事件的区别

        平时开发过程中,组件间通讯原生交互都离不开事件,对于一个组件元素,我们可以绑定原生JS事件(@click),也可以绑定自定义事件(@emit),非常灵活和方便。我们接下来会从源码角度看它实现原理。

例子为一对父子组件,child组件定义了原生事件@click和自定义事件@select,child组件的button标签定义了原生事件@click。

首先是编译

编译-parser:

       标签属性attrsList做处理,判断如果是指令,先解析出修饰符,判断如果是事件的指令,执行addHandler(el, name, value, modifiers, false, warn)。 

        parser建立AST节点树后,在节点处理过程中会执行processAttrs方法,它会对节点上的标签属性attrsList做处理processAttrs,遍历节点上的attrsList,拿到name属性,先判断name是否匹配模版指令的正则表达式(比如v-,:),如果匹配到,给节点的hasBindings属性设为true(标志是动态节点)。

        解析修饰符,接下来对name做parseModifiers(name)操作得到modifier。如果有modifer,把name中modifer相关字段去掉。解析事件指令,对name中'v-on'字段检测,满足的话把这个字段也去掉,name就变成click,调用addHandler1、根据modifier修饰符对事件名作修改。如果name为click且modifer中有 .right ,把name变成'contextmenu';如果name为click且modifer中有 .middle ,把name变成'mouseup'。2、根据modifer.native判断原生事件还是普通事件。接下来构造event对象,如果有 .native ,构造nativeEvents对象,否则构造events对象。3、按照name对事件做归类,并把回调函数的字符串保留到对应的事件中。接着构造newHandler对象{value: 事件名,如果modifer存在,把其中键值对写入}。接下来,把events[name]赋值给handlers,把newHandler赋值给events[name]。最终生成的节点有event或者nativeEvent对象(以事件名为key,值可能为newHandler,或者多个newHandler构成的数组),

        parseModifiers(src/compiler/helpers.js)对属性名上的修饰符做处理(如.natice,.prevent),得到modifier对象,包含了一个个 修饰符名:true 键值对。

        addHandler,给AST节点添加event属性,并根据modifer解析出来的标记给name上打标记(比如.once存在,name='~'+name)。

上面这个阶段,例子中我们得到的结果是:

child组件的click原生事件和select自定义事件解析出来的结果
子组件的 button 节点的原生click事件解析出来的结果

编译-codegen

        genData函数中根据AST元素节点上的events和nativeEvents生成data数据,它的定义在src/compiler/codegen/index.js中:

genData

        对于这两个属性,会调用 genHandlers 函数(src/compiler/codegen/events.js)。目标是生成和事件相关的代码

        genHandlers 根据isNative判断res的值为'nativeOn:{' 或者 'on:{'。遍历events对象,拼接genHandler会生成handler的代码,拼接出一个JSON代码对象。,返回值用","分割。最终返回形式是[xxx,xxx],res拼接得到{name:[function() {...},function() {...}],name1:[function() {...},function() {...}]...}

        genHandlers方法遍历事件对象 events,遍历events对每个事件调用 genHandler(name, events[name]) 方法,拼接结果(res+=`${name} : ${genHandler(name, events[name])}`)

        genHandler方法,如果events[name]是数组,map遍历它递归调用genHandler方法。1、正则去匹配event[name].value,判断它是一个函数的调用路径还是一个函数表达式。2、如果handler的modifers修饰符不存在,return `function($event) {${handler.value}}`;存在的话,遍历modifers对象key,根据不同的key做不同的逻辑操作,对于命中的key通过modiferCode(modiferCode是对不同修饰符key生成不同的代码)生成的代码临时储存在genModifierCode中,一步步把代码拼接起来。

这一阶段在我们的例子中得到的结果是:

child组件生成的data串
child组件中button标签生成的data串

        整个编译过程实际上就是对整个模版做解析,解析过程中生成的代码,完整描述了事件的定义,为最终去运行做准备

        那么到这里,编译部分完了,接下来我们来看一下运行时部分是如何实现的。其实 Vue 的事件有 2 种,一种是原生 DOM 事件,一种是用户自定义事件,我们分别来看。

运行部分如何实现

        vue的事件有两种,原生DOM事件和用户自定义事件,分别来看。

DOM原生事件:

        还记得我们之前在 patch 的时候执行各种 module 的钩子函数吗,当时这部分是略过的,我们之前只分析了 DOM 是如何渲染的,而 DOM 元素相关的属性、样式、事件等都是通过这些 module 的钩子函数完成设置的

        所有和 web 相关的 module 都定义在 src/platforms/web/runtime/modules 目录下,我们这次只关注目录下的 events.js 即可。

        creatPatchFunction方法,创建patch方法。它先拿到modules(各个某块,由baseModules和platformModules合并而来)和nodeOps(和平台相关操作方法)。接下来遍历hooks(一个数组['creat','active','update'...]),对于每个hook,hook做为key,[]空数组做为值储存在cbs对象中。接下来遍历modules,查找modules中有没有定义这个hook,有的话,往前面的空数组[]内push, module对应的hook方法。(比如事件对应的cbs为{'creat': creatFun, 'update': updateFun })。什么时候执行cbs中hooks方法呢?

       在createElm方法和creatComponent方法和更新节点patchVnode时会调用invokeCreateHooks方法,它会去遍历cbs中钩子函数进行执行。我们要看的是create钩子函数其中event相关方法,即updateDOMListener(oldVnode,newVnode)方法。

        updateDOMListener,先去判断oldVnode.data和newVnode.data是否都有on属性(on属性其实就是我们前面编译阶段执行genHandlers时得到的,其值为代码JSON对象)。都没有的话直接return,有的话去取出存在on,oldOn两个变量中,再去取target=vnode.elm真实dom节点(因为我们需要在dom上添加事件)。接着执行normalizeEvents(on)(和v-model相关,先不管)。再执行updateListeners(on, oldOn, add, remove, vnode.context)方法。

        updateListeners,它是被单独拿出来的文件,因为原生事件和自定义事件创建都会用到它。首次创建事件,之后更新事件。遍历on对象,拿到当前事件值cur和旧事件值old,对事件名执行normalizeEvent方法(前面对不同事件修饰符在name上做了标记,如‘~’,现在需要把它们作为Boolean返回并从name去掉)。

        如果old未定义,cur=on[name]=createFnInvoker(cur),即把on[name]指向createFnInvoker返回的值。接下来执行add(event.name,cur,event.once,event.capture,event.passive,event.params)方法。它就是通过addEventListener在真实dom上绑定事件了

                createFnInvoker,最终会返回一个invoker函数(最终添加事件的函数)。invoker函数,先拿到传进来的on[name]赋值给fns。如果它是一个数组,遍历它依次去执行其内定义的函数,否则直接执行fns,通过它创建了一个回调函数

        如果old定义了且old和cur不相同,直接把old.fns指向cur,同时把on[name]指向old,我们只要把invoker.fns改变即可,不需要重新创建事件。(事件创建不用再重复执行)

        了解了 updateListeners 的实现后,我们来看一下在原生 DOM 事件中真正添加回调和移除回调函数的实现( src/platforms/web/runtime/modules/event.js)

原生 DOM 事件添加回调和移除回调函数的实现

        实际上就是调用原生 addEventListener 和 removeEventListener,并根据参数传递一些配置,注意这里的 hanlder 会用 withMacroTask(hanlder) 包裹一下(src/core/util/next-tick.js)

withMacroTask

        实际上就是强制在 DOM 事件的回调函数执行期间如果修改了数据,那么这些数据更改推入的队列会被当做 macroTask 在 nextTick 后执行。

自定义事件:

        除了原生 DOM 事件,Vue 还支持了自定义事件,并且自定义事件只能作用在组件上,如果在组件上使用原生事件,需要加 .native 修饰符,普通元素上使用 .native 修饰符无效,接下来我们就来分析它的实现。

        自定义事件只能作用在组件中,我们回顾一下组件vnode创建过程(子组件在父组件中占位符vnode的创建过程)createComponent。在创建组件vnode之前,会对事件做处理,会把data.on(自定义事件)赋值给listeners,listeners在组件实例化成VNode时会作为参数传入。

createComponent,得到组件vnode的过程

        我们只关注事件相关的逻辑,可以看到,它把 data.on 赋值给了 listeners,把 data.nativeOn赋值给了 data.on,这样所有的原生 DOM 事件处理跟我们刚才介绍的一样,它是在当前组件环境中处理的(即子组件的原生dom事件在子组件占位符节点所在的父组件实例中处理)。而对于自定义事件,我们把 listeners 作为 vnode 的 componentOptions 传入,它是在子组件初始化阶段中处理的,所以它的处理环境是子组件(即子组件的自定义事件在子组件本身实例环境中处理)

        然后在子组件的初始化合并options的时候,会执行 initInternalComponent 方法(src/core/instance/init.js)

子组件的初始化

        这里拿到了父组件传入的 listeners,然后在执行 initEvents 的过程中,会处理这个 listeners(src/core/instance/events.js)

initEvents

拿到 listeners 后,执行 updateComponentListeners(vm, listeners) 方法:

updateComponentListeners

        updateListeners 我们之前介绍过,所以对于自定义事件和原生 DOM 事件处理的差异就在事件添加和删除的实现上,来看一下自定义事件 add 和 remove 的实现:

自定义事件 add 和 remove 的实现

实际上是利用 Vue 定义的事件中心,简单分析一下它的实现:

        非常经典的事件中心的实现,把所有的事件用 vm._events 存储起来,当执行 vm.$on(event,fn) 的时候,按事件的名称 event 把回调函数 fn 存储起来 vm._events[event].push(fn)。当执行 vm.$emit(event) 的时候,根据事件名 event 找到所有的回调函数 let cbs = vm._events[event],然后遍历执行所有的回调函数。当执行 vm.$off(event,fn) 的时候会移除指定事件名 event 和指定的 fn 。当执行 vm.$once(event,fn) 的时候,内部就是执行 vm.$on,并且当回调函数执行一次后再通过 vm.$off 移除事件的回调,这样就确保了回调函数只执行一次

        所以对于用户自定义的事件添加和删除就是利用了这几个事件中心的 API。需要注意的事一点,vm.$emit 是给当前的 vm 上派发的实例,之所以我们常用它做父子组件通讯,是因为它的回调函数的定义是在父组件中,对于我们这个例子而言,当子组件的 button 被点击了,它通过 this.$emit('select') 派发事件,那么子组件的实例就监听到了这个 select 事件,并执行它的回调函数——定义在父组件中的 selectHandler 方法,这样就相当于完成了一次父子组件的通讯

$on
$once
$off
$emit
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容