我们在开发组件时有时需要和父组件沟通,此时可以用自定义事件来实现

组件的事件分为自定义事件和原生事件,前者用于子组件给父组件发送消息的,后者用于在组件的根元素上直接监听一个原生事件,区别就是绑定原生事件需要加一个.native修饰符。

子组件里通过过this.$emit()将自定义事件以及需要发出的数据通过以下代码发送出去,第一个参数是自定义事件的名称,后面的参数是依次想要发送出去的数据,例如:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
<title>Document</title>
</head>
<body>
<div id="d"><com @myclick="MyClick" @mouseenter.native="Enter"></com></div>
<script>
Vue.config.productionTip=false;
Vue.config.devtools=false;
Vue.component('com',{
template:'<button @click="childclick">Click</button>',
methods:{
childclick:function(){this.$emit('myclick','gege','123')} //子组件的事件,通过this.$emit触发父组件的myclick事件
}
})
debugger
var app = new Vue({
el:'#d',
methods:{
MyClick:function(){console.log('parent MyClick method:',arguments)}, //响应子组件的事件函数
Enter:function(){console.log("MouseEnter")} //子组件的原生DOM事件
}
})
</script>
</body>
</html>

子组件就是一个按钮,渲染如下:

我们给整个组件绑定了两个事件,一个DOM原生的mouseenter事件和自定义的MyClick组件事件,当鼠标移动到按钮上时,打印出:MouseEnter,如下:

当点击按钮时输出子组件传递过来的信息,如下:

自定义事件其实是存储在组件实例的_events属性上的,我们在控制台输入console.log(app.$children[0]["_events"])就可以打印出来,如下:

myclick就是我们自定义的事件对象

writer by:大沙漠 QQ:22969969

源码分析


父组件在解析模板时会执行processAttrs()函数,会在AST对象上增加一个events和nativeevents属性,如下

function processAttrs (el) {      //第9526行 对属性进行解析
var list = el.attrsList;
var i, l, name, rawName, value, modifiers, isProp;
for (i = 0, l = list.length; i < l; i++) { //遍历每个属性名
name = rawName = list[i].name;
value = list[i].value;
if (dirRE.test(name)) {
// mark element as dynamic
el.hasBindings = true;
// modifiers
modifiers = parseModifiers(name);
if (modifiers) {
name = name.replace(modifierRE, '');
}
if (bindRE.test(name)) { // v-bind
/*略*/
} else if (onRE.test(name)) { // v-on //如果name以@或v-on:开头,表示绑定了事件
name = name.replace(onRE, '');
addHandler(el, name, value, modifiers, false, warn$2); 调用addHandler()函数将事件相关信息保存到el.events或nativeEvents里面
} else { // normal directives
/*略*/
}
} else {
/*略*/
}
}
} function addHandler ( //第6573行 给el这个AST对象增加event或nativeEvents
el,
name,
value,
modifiers,
important,
warn
) {
modifiers = modifiers || emptyObject;
/*略*/ var events;
if (modifiers.native) { //如果存在native修饰符,则保存到el.nativeEvents里面
delete modifiers.native;
events = el.nativeEvents || (el.nativeEvents = {});
} else { //否则保存到el.events里面
events = el.events || (el.events = {});
} /*略*/
var handlers = events[name]; //尝试获取已经存在的该事件对象
/* istanbul ignore if */
if (Array.isArray(handlers)) { //如果是数组,表示已经插入了两次了,则再把newHandler添加进去
important ? handlers.unshift(newHandler) : handlers.push(newHandler);
} else if (handlers) { //如果handlers存在且不是数组,则表示只插入过一次,则把events[name]变为数组
events[name] = important ? [newHandler, handlers] : [handlers, newHandler];
} else { //否则表示是第一次新增该事件,则值为对应的newHandler
events[name] = newHandler;
} el.plain = false;
}

例子里执行完后AST对象里对应的信息如下:(AST可以这样认为:Vue把模板通过正则解析后以对象的形式表现出来)

接下来在generate生成rendre函数的时候会调用genHandlers函数根据不同修饰符等生成对应的属性(作为_c函数的第二个data参数一部分),如下:

function genData$2 (el, state) {  //第10274行  拼凑data值
var data = '{'; /*略*/
// event handlers
if (el.events) { //如果el有绑定事件(没有native修饰符时)
data += (genHandlers(el.events, false, state.warn)) + ",";
}
if (el.nativeEvents) { //如果el有绑定事件(native修饰符时)
data += (genHandlers(el.nativeEvents, true, state.warn)) + ",";
}
/*略*/
return data
}

genHandlers会根据参数2的值将事件存储在nativeOn或on属性里,如下:

function genHandlers (      //第9992行 拼凑事件的data函数
events,
isNative,
warn
) {
var res = isNative ? 'nativeOn:{' : 'on:{'; //如果参数isNative为true则设置res为:nativeOn:{,否则为:on:{
for (var name in events) {
res += "\"" + name + "\":" + (genHandler(name, events[name])) + ",";
}
return res.slice(0, -1) + '}'
}

例子里执行到这里时等于:

_render将rendre函数转换为VNode时候会调用createComponent()函数创建组件占位符VNode,此时会有

function createComponent (  //第4182行
Ctor,
data,
context,
children,
tag
) {
/*略*/
var listeners = data.on; //对自定义事件(没有native修饰符)的处理,则保存到listeners里面,一会儿存到占位符VNode的配置信息里
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn; //对原生DOM事件,则保存到data.on里面,这样等该DOM渲染成功后会执行event模块的初始化,就会绑定对应的函数了 /*略*/
  var name = Ctor.options.name || tag;
  var vnode = new VNode(
    ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
    data, undefined, undefined, undefined, context,
    { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },        //自定义事件作为listeners属性存储在组件Vnode的配置参数里了
    asyncFactory
  );   // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  return vnode
}

原生事件存储在on属性上,后面介绍v-on指令时再详细介绍,对于自定义事件存储在组件Vnode配置参数的listeners属性里了。

当组件实例化的时候执行_init()时首先执行initInternalComponent()函数,该函数会获取listeners属性,如下:

function initInternalComponent (vm, options) {        //第4632行  初始化子组件
var opts = vm.$options = Object.create(vm.constructor.options);
// doing this because it's faster than dynamic enumeration.
var parentVnode = options._parentVnode; //该组件的占位符VNode
opts.parent = options.parent;
opts._parentVnode = parentVnode;
opts._parentElm = options._parentElm;
opts._refElm = options._refElm; var vnodeComponentOptions = parentVnode.componentOptions; //占位符VNode初始化传入的配置信息
opts.propsData = vnodeComponentOptions.propsData;
opts._parentListeners = vnodeComponentOptions.listeners; //将组件的自定义事件保存到_parentListeners属性里面
opts._renderChildren = vnodeComponentOptions.children;
opts._componentTag = vnodeComponentOptions.tag; if (options.render) {
opts.render = options.render;
opts.staticRenderFns = options.staticRenderFns;
}
}

回到_init函数,接着执行initEvents()函数,该函数会初始化组件的自定义事件,如下:

function initEvents (vm) {      //第2412行 初始化自定义事件
vm._events = Object.create(null);
vm._hasHookEvent = false;
// init parent attached events
var listeners = vm.$options._parentListeners; //获取占位符VNode上的自定义事件
if (listeners) {
updateComponentListeners(vm, listeners); //执行updateComponentListeners()新增事件
}
}

updateComponentListeners函数用于新增/更新组件的事件,如下:

function add (event, fn, once) {      //第2424行
if (once) {
target.$once(event, fn); //自定义事件最终调用$once绑定事件的
} else {
target.$on(event, fn);
}
} function remove$1 (event, fn) {
target.$off(event, fn);
} function updateComponentListeners ( //第2436行
vm,
listeners,
oldListeners
) {
target = vm;
updateListeners(listeners, oldListeners || {}, add, remove$1, vm); //调用updateListeners()更新DOM事件,传入add函数
target = undefined;
}

updateListeners内部会调用add()函数,这里用了一个优化措施,实际上我们绑定的是Vue内部的createFnInvoker函数,该函数会遍历传给updateListeners的函数,依次执行。

add()最终执行的是$on()函数,该函数定义如下:

  Vue.prototype.$on = function (event, fn) {  //第2448行 自定义事件的新增  event:函数名 fn:对应的函数
var this$1 = this; var vm = this;
if (Array.isArray(event)) { //如果event是一个数组
for (var i = 0, l = event.length; i < l; i++) { //则遍历该数组
this$1.$on(event[i], fn); //依次调用this$1.$on
}
} else { //如果不是数组
(vm._events[event] || (vm._events[event] = [])).push(fn); //则将事件保存到ev._event上
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) { //如果事件名以hook:开头
vm._hasHookEvent = true; //则设置vm._hasHookEvent为true,这样生命周期函数执行时也会执行这些函数
}
}
return vm
};

从这里可以看到自定义事件其实是保存到组件实例的_events属性上的

当子组件通过$emit触发当前实例上的事件时,会从_events上拿到对应的自定义事件并执行,如下:

  Vue.prototype.$emit = function (event) {  //第2518行  子组件内部通过$emit()函数执行到这里
var vm = this;
{
var lowerCaseEvent = event.toLowerCase(); //先将事件名转换为小写
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) { //如果lowerCaseEvent不等于event则报错(即事件名只能是小写)
tip(
"Event \"" + lowerCaseEvent + "\" is emitted in component " +
(formatComponentName(vm)) + " but the handler is registered for \"" + event + "\". " +
"Note that HTML attributes are case-insensitive and you cannot use " +
"v-on to listen to camelCase events when using in-DOM templates. " +
"You should probably use \"" + (hyphenate(event)) + "\" instead of \"" + event + "\"."
);
}
}
var cbs = vm._events[event]; //从_events属性里获取对应的函数数组
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs; //获取所有函数
var args = toArray(arguments, 1); //去掉第一个参数,后面的都作为事件的参数
for (var i = 0, l = cbs.length; i < l; i++) { //遍历cbs
try {
cbs[i].apply(vm, args); //依次执行每个函数,值为子组件的vm实例
} catch (e) {
handleError(e, vm, ("event handler for \"" + event + "\""));
}
}
}
return vm
};

大致流程跑完了,有点繁琐,多调试一下就好了。

Vue.js 源码分析(十四) 基础篇 组件 自定义事件详解的更多相关文章

  1. Vue.js 源码分析(十二) 基础篇 组件详解

    组件是可复用的Vue实例,一个组件本质上是一个拥有预定义选项的一个Vue实例,组件和组件之间通过一些属性进行联系. 组件有两种注册方式,分别是全局注册和局部注册,前者通过Vue.component() ...

  2. Vue.js 源码分析(十八) 指令篇 v-for 指令详解

    我们可以用 v-for 指令基于一个数组or对象来渲染一个列表,有五种使用方法,如下: <!DOCTYPE html> <html lang="en"> & ...

  3. Vue.js 源码分析(十九) 指令篇 v-html和v-text指令详解

    双大括号会将数据解释为普通文本,而非 HTML 代码.为了输出真正的 HTML,你需要使用 v-html 指令,例如: <!DOCTYPE html> <html lang=&quo ...

  4. Vue.js 源码分析(十六) 指令篇 v-on指令详解

    可以用 v-on 指令监听 DOM 事件,并在触发时运行一些 JavaScript 代码,例如: <!DOCTYPE html> <html lang="en"& ...

  5. Vue.js 源码分析(十五) 指令篇 v-bind指令详解

    指令是Vue.js模板中最常用的一项功能,它带有前缀v-,比如上面说的v-if.v-html.v-pre等.指令的主要职责就是当其表达式的值改变时,相应的将某些行为应用到DOM上,先介绍v-bind指 ...

  6. Vue.js 源码分析(十) 基础篇 ref属性详解

    ref 被用来给元素或子组件注册引用信息.引用信息将会注册在父组件的 $refs 对象上.如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素:如果用在子组件上,引用就指向组件实例,例如: ...

  7. Vue.js 源码分析(二十三) 指令篇 v-show指令详解

    v-show的作用是将表达式值转换为布尔值,根据该布尔值的真假来显示/隐藏切换元素,它是通过切换元素的display这个css属性值来实现的,例如: <!DOCTYPE html> < ...

  8. Vue.js 源码分析(二十一) 指令篇 v-pre指令详解

    该指令会跳过所在元素和它的子元素的编译过程,也就是把这个节点及其子节点当作一个静态节点来处理,例如: <!DOCTYPE html> <html lang="en" ...

  9. jQuery 源码分析(十二) 数据操作模块 html特性 详解

    jQuery的属性操作模块总共有4个部分,本篇说一下第1个部分:HTML特性部分,html特性部分是对原生方法getAttribute()和setAttribute()的封装,用于修改DOM元素的特性 ...

随机推荐

  1. python numba讲解

    目录 一:什么是numba 二:如何使用numba   由于python有动态解释性语言的特性,跑起代码来相比java.c++要慢很多,尤其在做科学计算的时候,十亿百亿级别的运算,让python的这种 ...

  2. 工作笔记--adb命令篇

    1.抓log方法 (bat文件) mkdir D:\logcatset /p miaoshu=请描述操作:adb logcat -v threadtime > D:\logcat\%miaosh ...

  3. 进程调度算法spf,fpf,时间片轮转算法实现

    调度的基本概念:从就绪队列中按照一定的算法选择一个进程并将处理机分配给它运行,以实现进程并发地执行. 进程信息 struct node { string name;//进程名称 int id;//进程 ...

  4. java 图书馆初级编写

    import java.util.Scanner; import java.util.Arrays; public class book { public static void main(Strin ...

  5. [Caliburn.Micro专题][1]快速入门

    目录 1. 什么是Caliburn.Micro? 2. 我是否需要学习CM框架? 3. 如何下手? 3.1 需要理解以下几个概念: 3.2 工程概览 3.3 示例代码 开场白:本系列为个人学习记录,才 ...

  6. 【原创】Talend 配置SSL支持gitlab

    背景 talend的源代码控制用的是gitlab,以前都是http方式的,但是最近突然改了https,所以talend登录失败,必须要SSL方式才能获取到分支等数据,才能提交代码. 证书导入 1.ta ...

  7. The listener supports no services oracle注册监听

    问题登场: [oracle@my-e450 ~]$ lsnrctl status …… The listener supports no servicesThe command completed s ...

  8. 使用ES对中文文章进行分词,并进行词频统计排序

    前言:首先有这样一个需求,需要统计一篇10000字的文章,需要统计里面哪些词出现的频率比较高,这里面比较重要的是如何对文章中的一段话进行分词,例如“北京是×××的首都”,“北京”,“×××”,“中华” ...

  9. mysql_innodb存储引擎的优化

    采用innodb作为存储引擎时的优化 innodb_buffer_pool_size 如果用 Innodb,那么这是一个重要变量.相对于 MyISAM 来说,Innodb对于 buffer size ...

  10. Rust中的字符串处理

    一路看过来,怕是我知道的所有语言当,处理最复杂吧. 当然,如果能正确处理,也是能理解最到位的. 这,就是我为什么要学Rust的原因. 暂无用武之地,但逻辑体系和知识点够复杂,才能应对更多事务~ fn ...