可以用 v-on 指令监听 DOM 事件,并在触发时运行一些 JavaScript 代码,例如:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="vue.js"></script>
</head>
<body>
<div id="app">
<button @click="show('click',$event)" @mouseenter="show('mouseenter',$event)">测试</button>
</div>
<script>
Vue.config.productionTip=false;
Vue.config.devtools=false;
var app = new Vue({
el:'#app',
methods:{ show(type,ev){console.log(type)} }
})
</script>
</body>
</html>

渲染结果为:

我们给测试按钮添加了一个mouseenter和click事件,鼠标移上去式控制台输出:

当点击时,输出为:

Vue的事件绑定有很多种写法,例如:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="vue.js"></script>
</head>
<body>
<div id="app">
<p>{{message}}</p>
<button @click="test1">Test1</button>     <!--事件可以对应一个方法-->
<button @click="test2('test2',$event)">Test2</button> <!--方法还可以传递参数,$event表示原始的DOM事件-->
<button @click="message='test3'">Test3</button>   <!--也可以是一个表达式-->
<button @click="function(){message='test4'}">Test4</button> <!--也可以是一个函数-->
<button @click="()=>{message='test5'}">Test5</button> <!--也可以是一个箭头函数-->
</div>
<script>
var App = new Vue({
el:'#app',
data(){
return {message:"Hello Vue"}
},
methods:{
test1(){console.log('test1');},
test2(text,ev){console.log(text);console.log(ev.type)}
}
})
</script>
</body>
</html>

可以看到v-on对应事件可以很多种格式的,可以是当前Vue实例的一个方法、一个表达式、一个函数,或者一个箭头函数

源码分析


writer by:大沙漠 QQ:22969969

以上面的第一个例子为例,Vue将DOM解析成AST对象时的时候执行到a节点时会执行processElement()函数,然后会执行processAttrs()函数,该函数会遍历每个属性,然后用判断是否以:或v-bind:开头,如下:

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)) { //如果该属性以v-、@或:开头,表示这是Vue内部指令
// mark element as dynamic
el.hasBindings = true;
// modifiers
modifiers = parseModifiers(name);
if (modifiers) {
name = name.replace(modifierRE, '');
}
if (bindRE.test(name)) { //bindRD等于/^:|^v-bind:/ ,即该属性是v-bind指令时 例如:<a :href="url">你好</a>
/*这里时v-bind指令对应的分支*/
} else if (onRE.test(name)) { //onRE等于/^@|^v-on:/,即该属性是v-on指令时
name = name.replace(onRE, ''); //获取绑定的事件类型 比如@click,此时name等于click v-on:click此时name也等于click
addHandler(el, name, value, modifiers, false, warn$2); //调用addHandler()函数将事件相关信息保存到el.events或nativeEvents里面
} else { // normal directives
/*自定义指令的分支*/
}
} else { //存储普通属性的分支
// literal attribute
{
var res = parseText(value, delimiters);
if (res) {
warn$2(
name + "=\"" + value + "\": " +
'Interpolation inside attributes has been removed. ' +
'Use v-bind or the colon shorthand instead. For example, ' +
'instead of <div id="{{ val }}">, use <div :id="val">.'
);
}
}
addAttr(el, name, JSON.stringify(value));
// #6887 firefox doesn't update muted state if set via attribute
// even immediately after element creation
if (!el.component &&
name === 'muted' &&
platformMustUseProp(el.tag, el.attrsMap.type, name)) {
addProp(el, name, 'true');
}
}
}
}

addHandler()函数用于给对应的AST对象增加一个events属性,保存事件对应的信息,如下:

function addHandler (     //第6573行  给el这个AST对象增加event或nativeEvents,用于记录事件的信息
el,
name,
value,
modifiers,
important,
warn
) {
modifiers = modifiers || emptyObject;
// warn prevent and passive modifier
/* istanbul ignore if */
if (
"development" !== 'production' && warn &&
modifiers.prevent && modifiers.passive
) {
warn(
'passive and prevent can\'t be used together. ' +
'Passive handler can\'t prevent default event.'
);
} // check capture modifier
if (modifiers.capture) {
delete modifiers.capture;
name = '!' + name; // mark the event as captured
}
if (modifiers.once) { //如果有once修饰符
delete modifiers.once;
name = '~' + name; // mark the event as once
}
/* istanbul ignore if */
if (modifiers.passive) {
delete modifiers.passive;
name = '&' + name; // mark the event as passive
} // normalize click.right and click.middle since they don't actually fire
// this is technically browser-specific, but at least for now browsers are
// the only target envs that have right/middle clicks.
if (name === 'click') { //鼠标按键修饰符:如果是click事件,则根据modiflers进行修正
if (modifiers.right) {
name = 'contextmenu';
delete modifiers.right;
} else if (modifiers.middle) {
name = 'mouseup';
}
} 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 newHandler = {
value: value.trim()
};
if (modifiers !== emptyObject) {
newHandler.modifiers = modifiers;
} 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 {
events[name] = newHandler; //否则表示是第一次新增该事件,则值为对应的newHandler
} el.plain = false;
}

例子里执行到这里这里后对应的AST等于:

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

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

genHandler会获取每个事件对应的代码,如下:

function genHandler (   //第10004行  name:事件名,比如:name handler:事件绑定的对象信息,比如:{value: "show", modifiers: {…}}
name,
handler
) {
if (!handler) {
return 'function(){}'
} if (Array.isArray(handler)) {
return ("[" + (handler.map(function (handler) { return genHandler(name, handler); }).join(',')) + "]")
} var isMethodPath = simplePathRE.test(handler.value); //是否为简单的表达式,比如show、show_d、show1等
var isFunctionExpression = fnExpRE.test(handler.value); //是否为函数表达式(箭头函数或function(){}格式的匿名函数) if (!handler.modifiers) { //如果该事件的修饰符为空
if (isMethodPath || isFunctionExpression) { //如果是简单表达式或者是函数表达式
return handler.value //则直接返回handler.value,比如:show
}
/* istanbul ignore if */
return ("function($event){" + (handler.value) + "}") // inline statement //否则返回带有一个$event变量的函数形式,比如:当value是个表达式时,例如:value=a+123,返回格式:function($event){a+123;}
} else { //如果还存在修饰符(解析模板时有些修饰符被过滤掉了)
var code = '';
var genModifierCode = '';
var keys = [];
for (var key in handler.modifiers) { //遍历每个修饰符,比如:prevent
if (modifierCode[key]) { //如果有在modifierCode里面定义 modifierCode是个数组,保存了一些内置修饰符对应的代码
genModifierCode += modifierCode[key]; //则拼凑到genModifierCode里面
// left/right
if (keyCodes[key]) {
keys.push(key);
}
} else if (key === 'exact') {
var modifiers = (handler.modifiers);
genModifierCode += genGuard(
['ctrl', 'shift', 'alt', 'meta']
.filter(function (keyModifier) { return !modifiers[keyModifier]; })
.map(function (keyModifier) { return ("$event." + keyModifier + "Key"); })
.join('||')
);
} else {
keys.push(key);
}
}
if (keys.length) { //如果有按键
code += genKeyFilter(keys); //则拼凑按键
}
// Make sure modifiers like prevent and stop get executed after key filtering
if (genModifierCode) {
code += genModifierCode;
}
var handlerCode = isMethodPath
? ("return " + (handler.value) + "($event)")
: isFunctionExpression
? ("return (" + (handler.value) + ")($event)")
: handler.value;
/* istanbul ignore if */
return ("function($event){" + code + handlerCode + "}")
}
}

例子里执行到这里后生成的render函数等于:

with(this){return _c('div',{attrs:{"id":"app"}},[_c('button',{on:{"click":function($event){show('click',$event)},"mouseenter":function($event){show('mouseenter',$event)}}},[_v("测试")])])}

其中和事件有关的如下:

on: {
"click": function($event) {
show('click', $event)
},
"mouseenter": function($event) {
show('mouseenter', $event)
}
}

最后在_watch渲染成真实的DOM节点后,就会调用events模块的updateDOMListeners钩子函数,该函数会获取该Vnode的on属性,依次遍历on对象里的每个元素,最后调用addEventListener去绑定对应的事件

function updateDOMListeners (oldVnode, vnode) {     //第7083行 DOMN事件相关
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return
}
var on = vnode.data.on || {}; //新Node上的事件 例如:{click: ƒ ($event){}}
var oldOn = oldVnode.data.on || {};
target$1 = vnode.elm; //DOM引用
normalizeEvents(on); //处理V-model的
updateListeners(on, oldOn, add$1, remove$2, vnode.context); //调用updateListeners做进一步处理
target$1 = undefined;
}

updateListeners()函数又会调用add$1函数去添加DOM事件,如下:

function updateListeners (      //第2036行 更新DOM事件
on,
oldOn,
add,
remove$$1,
vm
) {
var name, def, cur, old, event;
for (name in on) { //遍历on,此时name就是对应的事件类型,比如:click
def = cur = on[name];
old = oldOn[name];
event = normalizeEvent(name);
/* istanbul ignore if */
if (isUndef(cur)) {
"development" !== 'production' && warn(
"Invalid handler for event \"" + (event.name) + "\": got " + String(cur),
vm
);
} else if (isUndef(old)) { //如果old没有定义,则表示这是一个创建事件
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur);
}
add(event.name, cur, event.once, event.capture, event.passive, event.params); //调用add()绑定事件
} else if (cur !== old) {
old.fns = cur;
on[name] = old;
}
}
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name);
remove$$1(event.name, oldOn[name], event.capture);
}
}
}

updateListeners里的add函数,也就是全局的add$1函数才是最终的添加事件函数,如下:

function add$1 (    //第7052行  绑定事件  event:事件名 handler:事件的函数 once$$1:是否只执行一次 capture:是否采用捕获状态 passive:可用于移动端性能提升
event,
handler,
once$$1,
capture,
passive
) {
handler = withMacroTask(handler);
if (once$$1) { handler = createOnceHandler(handler, event, capture); } //如果有设置了once$$1,则继续使用createOnceHandler封装
target$1.addEventListener( //调用原生的DOM APIaddEventListener添加对应的事件,2017年DOM规范对addEventListener()的第三个参数做了修订,可以是一个对象
event,
handler,
supportsPassive
? { capture: capture, passive: passive }
: capture
);
}

我们看到Vue内部添加DOM事件最终也是通过addEventListener()来添加的,说到底,Vue只是把这些API进行了封装,使我们用起来更方便而已。

Vue.js 源码分析(十六) 指令篇 v-on指令详解的更多相关文章

  1. Vue.js 源码分析(十四) 基础篇 组件 自定义事件详解

    我们在开发组件时有时需要和父组件沟通,此时可以用自定义事件来实现 组件的事件分为自定义事件和原生事件,前者用于子组件给父组件发送消息的,后者用于在组件的根元素上直接监听一个原生事件,区别就是绑定原生事 ...

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

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

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

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

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

    我们可以用 v-for 指令基于一个数组or对象来渲染一个列表,有五种使用方法,如下: <!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. jQuery 源码分析(十二) 数据操作模块 html特性 详解

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

  8. Vue.js 源码分析(一) 代码结构

    关于Vue vue是一个兴起的前端js库,是一个精简的MVVM.MVVM模式是由经典的软件架构MVC衍生来的,当View(视图层)变化时,会自动更新到ViewModel(视图模型),反之亦然,View ...

  9. Vue.js 源码分析(二十六) 高级应用 作用域插槽 详解

    普通的插槽里面的数据是在父组件里定义的,而作用域插槽里的数据是在子组件定义的. 有时候作用域插槽很有用,比如使用Element-ui表格自定义模板时就用到了作用域插槽,Element-ui定义了每个单 ...

随机推荐

  1. ARM64 的 memcpy 优化与实现

    参考:https://www.byteisland.com/arm64-%E7%9A%84-memcpy-%E6%B1%87%E7%BC%96%E5%88%86%E6%9E%90/ libc/stri ...

  2. [03]使用 VS2019 创建 ASP.NET Core Web 程序

    使用 VS2019 创建 ASP.NET Core Web 程序 本文作者:梁桐铭- 微软最有价值专家(Microsoft MVP) 文章会随着版本进行更新,关注我获取最新版本 本文出自<从零开 ...

  3. CSV文件数据如何读取、导入、导出到新的CSV文件中以及CSV文件的创建

    CSV文件数据如何读取.导入.导出到新的CSV文件中以及CSV文件的创建 一.csv文件的创建 (1)新建一个文本文档: 打开新建文本文档,进行编辑. 注意:关键字与关键字之间用英文半角逗号隔开.第一 ...

  4. Linux 网络相关命令 Cheat Sheet

    以下漫画形式呈现的常用 Linux 网络相关命令速查表来自 twitter -

  5. 故事2:.net程序员成长经历

    啊,最近一段时间在学习asp.net mvc ,一直没有接着写了,加上白天工作很忙,每天都很辛苦的哈,那咱接着说上一个故事哈. 当时第二天开始复习java面试题,非常的期待,从来没有去过公司,不知道别 ...

  6. ASP.NET MVC AJAX 请求中加入 antiforgerytoken 解决“所需的防伪表单字段“__RequestVerificationToken”不存在”问题

    在ASP.NET mvc中如果在表中使用了@Html.AntiForgeryToken(),ajax post不会请求成功 解决方法是在ajax中加入__RequestVerificationToke ...

  7. Typescript基础(2)——函数

    前言 今天继续typescript的学习,开始函数的学习. 函数 函数的定义 和JavaScript一样,TypeScript函数可以创建有名字的函数和匿名函数. 你可以随意选择适合应用程序的方式,不 ...

  8. iOS音频与视频的开发(一)-使用AVAudioPlayer播放音乐、使用AVPlayerViewController播放视频

    iOS的多媒体支持非常强大,它提供了多套支持多媒体的API,无论是音频.视频的播放,还是录制,iOS都提供了多种API支持.借助于这些API的支持,iOS应用既可以查看.播放手机相册中的照片.视频,也 ...

  9. MBProgressHUD源码(上)

    本篇博文记录MBProgressHUD源码学习过程,从官方提供的Demo项目入手,一步步了解其代码结构,学习它使用的技术,体会作者的编程思想. 一.结构 我们先来看下MBProgressHUD的结构, ...

  10. Saltstack_使用指南08_远程执行-返回程序

    1. 主机规划 salt 版本 [root@salt100 ~]# salt --version salt (Oxygen) [root@salt100 ~]# salt-minion --versi ...