Vue定义了四种添加事件监听的方法:
例如$on
,可通过vm.$on(event,callback)
添加监听。
事件监听
$on
Vue.prototype.$on = function (event, fn) {
var this$1 = this;
var vm = this;
//如果传参event是数组,递归调用$on
if (Array.isArray(event)) {
for (var i = 0, l = event.length; i < l; i++) {
this$1.$on(event[i], fn);
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn);
// 这里在注册事件的时候标记bool值也就是个标志位来表明存在钩子,而不需要通过哈希表的方法来查找是否有钩子,这样做可以减少不必要的开销,优化性能。
if (hookRE.test(event)) {
vm._hasHookEvent = true;
}
}
return vm
};
$once
$once监听只能触发一次的事件,触发以后会自动移除该事件。
Vue.prototype.$once = function (event, fn) {
var vm = this;
function on () {
//在第一次执行的时候将该事件销毁
vm.$off(event, on);
//执行注册的方法
fn.apply(vm, arguments);
}
on.fn = fn;
vm.$on(event, on);
return vm
};
$off
$off用来移除自定义事件。
Vue.prototype.$off = function (event, fn) {
var this$1 = this;
var vm = this;
// 如果没有参数,关闭全部事件监听器
if (!arguments.length) {
vm._events = Object.create(null);
return vm
}
// 关闭数组中的事件监听器
if (Array.isArray(event)) {
for (var i = 0, l = event.length; i < l; i++) {
this$1.$off(event[i], fn);
}
return vm
}
// 具体的某个事件
var cbs = vm._events[event];
if (!cbs) {
return vm
}
// fn回调函数不存在,将事件监听器变为null,返回vm
if (!fn) {
vm._events[event] = null;
return vm
}
// 回调函数存在
if (fn) {
// specific handler
var cb;
var i$1 = cbs.length;
while (i$1--) {
cb = cbs[i$1];
if (cb === fn || cb.fn === fn) {
// 移除 fn 这个事件监听器
cbs.splice(i$1, 1);
break
}
}
}
return vm
};
$emit
$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);
for (var i = 0, l = cbs.length; i < l; i++) {
try {
//触发当前实例上的事件,附加参数都会传给监听器回调。
cbs[i].apply(vm, args);
} catch (e) {
handleError(e, vm, ("event handler for \"" + event + "\""));
}
}
}
return vm
};
HTML元素上点击事件
先来研究发生在原生dom元素上的事件。
用v-on指令监听DOM事件,并在触发时运行一些JS代码,也可以通过@简写的方式,原因:
Vue定义了这样的正则表达式var onRE = /^@|^v-on:/;
,在processAttrs()解析属性时通过onRE.test(name)
判断来添加属性。
html的编写如下:
<body>
<div id="app">
<button @click="test01">点我01号</button>
</div>
<script>
new Vue({
'el':'#app',
methods:{
test01:function(){
alert('01号被点了!');
}
}
});
</script>
</body>
Vue初始化时,调用initEvents(vm)对事件进行初始化:
function initEvents (vm) {
vm._events = Object.create(null);
//_hasHookEvent标志位表明是否存在钩子,而不需要通过哈希表来查找是否有钩子,这样做可以减少不必要的开销,优化性能。
vm._hasHookEvent = false;
//初始化父组件attach的事件
var listeners = vm.$options._parentListeners;
if (listeners) {
updateComponentListeners(vm, listeners);
}
}
initEvents方法在vm上创建一个_events对象,用来存放事件。
接下来就调用initState()方法初始化事件,相关代码段:if (opts.methods) { initMethods(vm, opts.methods); }
这个例子methods内有方法test01,因此会执行initMethods。
vm[key] = methods[key] == null ? noop : bind(methods[key], vm);
initMethods遍历定义的methods,通过调用bind改变函数的this指向,修饰了事件的回调函数,组件上挂载的事件都是在父作用域中的。
function nativeBind (fn, ctx) {
//fn的this指向ctx
return fn.bind(ctx)
}
var bind = Function.prototype.bind
? nativeBind
: polyfillBind;
这里bind用来改变函数中this的指向。(关于call,apply,bind的学习来自于https://www.cnblogs.com/xljzl...)
接下来Vue将html解析成ast,解析后的render如图所示
解析过程参考https://segmentfault.com/a/11...
接下来调用add$1通过addEventListener将事件绑定到target上。
function add$1 (event,handler,once$$1,apture,passive) {
handler = withMacroTask(handler);
if (once$$1) { handler = createOnceHandler(handler, event, capture); }
//绑定点击事件
target$1.addEventListener(
event,
handler,
supportsPassive
? { capture: capture, passive: passive }
: capture
);
}
component上点击事件
<body>
<div id="app">
<child-test :test-message="mess" v-on:click.native="test01" v-on:componenton="test02"></child-test>
</div>
<template id="tplt01">
<button>{{testMessage}}</button>
</template>
<script>
new Vue({
'el':'#app',
data:{mess:'组件点击001'},
methods:{
test01:function(){
alert('01号被点了!');
},
test02:function(){
alert('01号被点了!');
}
},
components:{
'childTest':{
template:"#tplt01",
props:['testMessage'],
methods:{
test02:function(){
alert('001号被点了!');
}
},
}
}
});
</script>
</body>
<child-test>标签定义了click事件,若click事件没加修饰符.native,点击按钮不出发任何事件,
若添加了.native修饰符,点击按钮执行的就是test01,alert('01号被点了!');
。.native的作用就是在原生dom上绑定事件。
不添加.native的的事件解析过程与上文相同,而添加了.native之后,v-on的事件会被放到nativeOn数组中,解析后的render如图所示:
在事件初始化,调用genHandlers的时候,会先判断该事件是否为native,如果是,解析的事件字符串就会用'nativeOn{}'包裹。
function genHandlers (
events,
isNative,
warn
) {
var res = isNative ? 'nativeOn:{' : 'on:{';
for (var name in events) {
res += "\"" + name + "\":" + (genHandler(name, events[name])) + ",";
}
return res.slice(0, -1) + '}'
}
html解析成vnode之后会调用createComponent进行处理。
function createComponent (
Ctor,
data,
context,
children,
tag
) {
if (isUndef(Ctor)) {
return
}
var baseCtor = context.$options._base;
/*其他代码省略*/
//缓存data.on的函数,这些需要作为子组件监听器而不是DOM监听器来处理。就是componenton事件
var listeners = data.on;
//data.on被native修饰符的事件所替换
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
}
createComponent将.native修饰符的事件放在data.on上面。接下来data.on上的事件(本文中的alert('001号被点了!');)会按普通的html事件往下走。
function updateDOMListeners (oldVnode, vnode) {
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return
}
var on = vnode.data.on || {};
var oldOn = oldVnode.data.on || {};
target$1 = vnode.elm;
normalizeEvents(on);
updateListeners(on, oldOn, add$1, remove$2, vnode.context);
target$1 = undefined;
}
最终通过target$1.addEventListener添加事件监听。
而标签内没有.native的修饰符调用的是$on方法。
function updateComponentListeners (
vm,
listeners,
oldListeners
) {
target = vm;
updateListeners(listeners, oldListeners || {}, add, remove$1, vm);
target = undefined;
}
updateListeners又调用了add方法
function add (event, fn, once) {
if (once) {
target.$once(event, fn);
} else {
target.$on(event, fn);
}
}
也就是说对于普通html元素和在组件标签内添加了.native修饰符的事件,都通过target$1.addEventListener()来挂载事件。而定义在组件上的事件会调用原型上的$on等方法。
事件修饰符及其他
关于事件的使用官网已经说得很清楚啦,这里就不赘述啦。
https://cn.vuejs.org/v2/guide...
<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>
<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>
<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div>
<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。