模板编译 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
一些参数判断
事件函数的多种写法
在官方文档中演示了事件回调函数的多种写法,这些写法都在模板编译过程中进行了识别
下方三个正则表达式是模板编译时对event写法的判断依据,下面会有更详细的注释
1. 路径类写法
2. 箭头函数,匿名函数
3. 表达式 handler($event), a = 1
在vue中通常通过@click="handle($event)"来获取event对象,其实这里是vue在模板中做了一层包裹,将function($event){}套在handle($event)外部。
有修饰符的情况
修饰符在ast生成的过程中就已经捕获了,vue中对event事件的修饰符处理如下
先对修饰符做一个处理
最后座一层包裹,因为这里对键盘事件也做了处理,因此一定要拿到event对象
genCode
在处理完这些之后会生成字符串on:"…"/nativeOn:"…",最后生成render函数
组件初始化
组件初始化简单地说就是先对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如 ,vue就会报错。这正是因为在这里做了处理,只有占位符vnode才可以有data.nativeOn的属性。是h5标签的节点不会调用createComponent方法,其data.on在创建节点的时候会绑定到节点上。
自定义事件(只针对组件间)
由createComponent函数可知,listeners存放了自定义事件。
在父子通讯的时候父组件只要v-on/@eventName,就可以监听到子组件emit出来的事件。
_init: initInternalComponent
创建子组件的时候会把占位符$vnode的$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"
$listeners就是vnode.data.on的别称,因此通过$listeners就可以拿到父组件注册的非native事件。在爷孙组件通信的是可以使用$listeners通过父级组件将爷孙组件关联。
grand组件:
father组件: // 此处是grand的事件
son组件:this.$emit(“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组件初始化及更新的文章。
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
作者: Simplyme0823
原文链接:https://juejin.im/post/6861206075744452622