9.4. 自定义事件
Vue
如何处理原生的Dom
事件基本流程已经讲完,然而针对事件还有一个重要的概念不可忽略,那就是组件的自定义事件。我们知道父子组件可以利用事件进行通信,子组件通过vm.$emit
向父组件分发事件,父组件通过v-on:(event)
接收信息并处理回调。因此针对自定义事件在源码中自然有不同的处理逻辑。我们先通过简单的例子展开。
<script>
var child = {
template: `<div @click="emitToParent">点击传递信息给父组件</div>`,
methods: {
emitToParent() {
this.$emit('myevent', 1)
}
}
}
new Vue({
el: '#app',
components: {
child
},
template: `<div id="app"><child @myevent="myevent" @click.native="nativeClick"></child></div>`,
methods: {
myevent(num) {
console.log(num)
},
nativeClick() {
console.log('nativeClick')
}
}
})
</script>
从例子中可以看出,普通节点只能使用原生DOM
事件,而组件上却可以使用自定义的事件和原生的DOM
事件,并且通过native
修饰符区分,有了原生DOM
对于事件处理的基础,接下来我们看看自定义事件有什么特别之处。
9.4.1 模板编译
回过头来看看事件的模板编译,在生成AST
树阶段,之前分析说过addHandler
方法会对事件的修饰符做不同的处理,当遇到native
修饰符时,事件相关属性方法会添加到nativeEvents
属性中。下图是child
生成的AST
树:
9.4.2 代码生成
不管是组件还是普通标签,事件处理代码都在genData
的过程中,和之前分析原生事件一致,genHandlers
用来处理事件对象并拼接成字符串。
function genData() {
···
if (el.events) {
data += (genHandlers(el.events, false)) + ",";
}
if (el.nativeEvents) {
data += (genHandlers(el.nativeEvents, true)) + ",";
}
}
getHandlers
的逻辑前面已经讲过,处理组件原生事件和自定义事件的区别在isNative
选项上,我们看最终生成的代码为:
with(this){return _c('div',{attrs:{"id":"app"}},[_c('child',{on:{"myevent":myevent},nativeOn:{"click":function($event){return nativeClick($event)}}})],1)}
有了render
函数接下来会根据它创建Vnode
实例,其中遇到组件占位符节点时会创建子组件Vnode
, 此时为on,nativeOn
做了一层特殊的转换,将nativeOn
赋值给on
,这样后续的处理方式和普通节点一致。另外,将on
赋值给listeners
,在创建VNode
时以组件配置componentOptions
传入。
// 创建子组件过程
function createComponent (){
···
var listeners = data.on;
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn;
···
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 },
asyncFactory
);
return vnode
}
9.4.3 子组件实例
接下来是通过Vnode
生成真实节点的过程,这个过程遇到子Vnode
会实例化子组件实例。实例化子类构造器的过程又回到之前文章分析的初始化选项配置的过程,在系列最开始的时候分析Vue.prototype.init
的过程,跳过了组件初始化的流程,其中针对自定义事件的处理的关键如下
Vue.prototype._init = function(options) {
···
// 针对子组件的事件处理逻辑
if (options && options._isComponent) {
// 初始化内部组件
initInternalComponent(vm, options);
} else {
// 选项合并,将合并后的选项赋值给实例的$options属性
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
// 初始化事件处理
initEvents(vm);
}
function initInternalComponent (vm, options) {
var opts = vm.$options = Object.create(vm.constructor.options);
···
opts._parentListeners = vnodeComponentOptions.listeners;
···
}
此时,子组件拿到了父占位符节点定义的@myevent="myevent"
事件。接下来进行子组件的初始化事件处理,此时vm.$options._parentListeners
会拿到父组件自定义的事件。而带有自定义事件的组件会执行updateComponentListeners
函数。
function initEvents (vm) {
vm._events = Object.create(null);
vm._hasHookEvent = false;
// init parent attached events
var listeners = vm.$options._parentListeners;
if (listeners) {
// 带有自定义事件属性的实例
updateComponentListeners(vm, listeners);
}
}
之后又回到了之前分析的updateListeners
过程,和原生DOM
事件不同的是,自定义事件的添加移除的方法不同。
var target = vm;
function add (event, fn) {
target.$on(event, fn);
}
function remove$1 (event, fn) {
target.$off(event, fn);
}
function updateComponentListeners (vm,listeners,oldListeners) {
target = vm;
updateListeners(listeners, oldListeners || {}, add, remove$1, createOnceHandler, vm);
target = undefined;
}
9.4.4 事件API
我们回头来看看Vue
在引入阶段对事件的处理还做了哪些初始化操作。Vue
在实例上用一个_events
属性存贮管理事件的派发和更新,暴露出$on, $once, $off, $emit
方法给外部管理事件和派发执行事件。
eventsMixin(Vue); // 定义事件相关函数
function eventsMixin (Vue) {
var hookRE = /^hook:/;
// $on方法用来监听事件,执行回调
Vue.prototype.$on = function (event, fn) {
var vm = this;
// event支持数组形式。
if (Array.isArray(event)) {
for (var i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn);
}
} else {
// _events数组中记录需要监听的事件以及事件触发的回调
(vm._events[event] || (vm._events[event] = [])).push(fn);
if (hookRE.test(event)) {
vm._hasHookEvent = true;
}
}
return vm
};
// $once方法用来监听一次事件,执行回调
Vue.prototype.$once = function (event, fn) {
var vm = this;
// 对fn做一层包装,先解除绑定再执行fn回调
function on () {
vm.$off(event, on);
fn.apply(vm, arguments);
}
on.fn = fn;
vm.$on(event, on);
return vm
};
// $off方法用来解除事件监听
Vue.prototype.$off = function (event, fn) {
var vm = this;
// 如果$off方法没有传递任何参数时,将_events属性清空。
if (!arguments.length) {
vm._events = Object.create(null);
return vm
}
// 数组处理
if (Array.isArray(event)) {
for (var i$1 = 0, l = event.length; i$1 < l; i$1++) {
vm.$off(event[i$1], fn);
}
return vm
}
var cbs = vm._events[event];
if (!cbs) {
return vm
}
if (!fn) {
vm._events[event] = null;
return vm
}
// specific handler
var cb;
var i = cbs.length;
while (i--) {
cb = cbs[i];
if (cb === fn || cb.fn === fn) {
// 将监听的事件回调移除
cbs.splice(i, 1);
break
}
}
return vm
};
// $emit方法用来触发事件,执行回调
Vue.prototype.$emit = function (event) {
var vm = this;
{
var lowerCaseEvent = event.toLowerCase();
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
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];
// 找到已经监听事件的回调,执行
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs;
var args = toArray(arguments, 1);
var info = "event handler for \"" + event + "\"";
for (var i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info);
}
}
return vm
};
}
有了这些事件api,自定义事件的添加移除理解起来也简单很多。组件通过this.$emit
在组件实例中派发了事件,而在这之前,组件已经将需要监听的事件以及回调添加到实例的_events
属性中,触发事件时便可以直接执行监听事件的回调。
最后,我们换一个角度理解父子组件通信,组件自定义事件的触发和监听本质上都是在当前的组件实例中进行,之所以能产生父子组件通信的效果是因为事件监听的回调函数写在了父组件中。