摘要:写文章不容易,点个赞呗兄弟专注源码分享,文章分为白话版和源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于版本如果你觉得排版难看,请点击下面链接或者拉到下面关注公众号也可以吧原理源码版之拼接绑定的事件今天我们
写文章不容易,点个赞呗兄弟
专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧
研究基于 Vue版本 【2.5.17】
如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧
【Vue原理】Compile - 源码版 之 generate 拼接绑定的事件
今天我们来探索事件的拼接啦,上一篇我们已经讲过了generate 阶段 节点数据的拼接
事件也属于其中一部分内容,但是由于内容实在太多太多太多太多,所以打算多带带拿出来讲啦
这就是 Compile 的最后一篇文章了!终于终于发完了....真的发恶心了,估计网上找不着我这么详细的 compile 文章了(找到当我没说)
本文章很长,一万多字,但是其实都是代码和例子,帮助我们理解的,没有什么,一路看下来绝壁没有压力,源码也是简化过的
好吧,开始正文
从上篇文章我们知道了,节点数据拼接,调用的是 genData$2 方法
现在我们就来回顾下这个方法(只保留了 事件 相关)
function genData$2(el, state) { var data = "{"; .....已省略其他属性拼接 // 事件 if(el.events) { data += genHandlers(el.events, false) + ","; } // 原生事件 if(el.nativeEvents) { data += genHandlers(el.nativeEvents, true) + ","; } data = data.replace(/,$/, "") + "}"; return data }
没错,事件,分为 组件原生事件 和 事件
组件原生事件顾名思义,就是给组件绑定的原生DOM事件,像这样
事件而这个呢,包含范围很广,包括 组件上的自定义事件,以及 标签上的原生事件,如下
他们调用的方法都是 genHandlers,都是把结果值直接拼接上 data ,那有什么不同呢?
不同就在于给 genHandlers 传的最后一个参数,现在我们把视角切到 genHandlers 方法上
genHandlersfunction genHandlers( events, isNative ) { var res = isNative ? "nativeOn:{": "on:{"; var handler = events[name] for (var name in events) { res += ` ${name} : ${ genHandler(name,handler )} ,` } return res.slice(0, -1) + "}" }
这里非常明显看到
当你传入 isNative=true 的时候,该事件即为 组件原生DOM 事件,需要拼接在 nativeOn:{} 字符串上
否则,就是事件,拼接在 on:{} 字符串上
先给个简单的例子来熟悉下
拼接后的字符串是这样的
`_c("div", { on: { "click": aa } },[ _c("test", { on: { "test": cc }, nativeOn: { "input":$event=>{ return bb($event) } } }) ])`
好的,大家大概有个了解了,下面我们将会探索事件内部复杂的拼接了
因为需要涉及到各种许多的 modifiers
这次之前,先给大家介绍 Vue 内部已经设置了的按键值
按键内部配置Vue 把几个常用的按键给配置了,当然了,内部设置的键名,你都是可以覆盖配置的
很简单,过下眼,没啥好说的,不过下面会用到
记住,keyCodes 和 keyName 是 Vue 内部配置键盘的变量
// 键盘和 code 映射 var keyCodes = { esc: 27, tab: 9, enter: 13, space: 32, up: 38, left: 37, right: 39, down: 40, delete: [8, 46] }; // 键盘名映射 var keyNames = { esc: "Escape", tab: "Tab", enter: "Enter", space: " ", // IE11 使用没有 Arrow 前缀的键名 up: ["Up", "ArrowUp"], left: ["Left", "ArrowLeft"], right: ["Right", "ArrowRight"], down: ["Down", "ArrowDown"], delete: ["Backspace", "Delete"] };
看完了 Vue 的按键配置,我们来看 Vue 内部配置的修饰符
修饰符内部配置相信大家在项目中,都有用到过修饰符吧,很方便对不对,就是下面这些 stop prevent,很方便对不对
简直不要太方便,其实 Vue 也没做什么太多东西,不过是帮我们自动组装上了几个语句
来看看 Vue 配置的修饰符
var modifierCode = { stop: "$event.stopPropagation();", prevent: "$event.preventDefault();", ctrl: genGuard("!$event.ctrlKey"), shift: genGuard("!$event.shiftKey"), alt: genGuard("!$event.altKey"), meta: genGuard("!$event.metaKey"), self: genGuard("$event.target !== $event.currentTarget"), left: genGuard(""button" in $event && $event.button !== 0"), middle: genGuard(""button" in $event && $event.button !== 1"), right: genGuard(""button" in $event && $event.button !== 2") };
像 stop,prevent 直接就可以拼接在回调中
比如你绑定的事件名是 aaa 吼,并且使用了修饰符 stop,就会这么拼接
"function($event ){ " + "$event.stopPropagation();" + " return "+ aaa +"($event);" + "}"
你肯定看到了其他修饰符使用了一个函数 genGuard ,马上看下
var genGuard = function(condition) { return ` if ( ${ condition } ) return null ` }
这个函数炒鸡简单,就是给你的事件回调拼接执行条件(当你使用了相关修饰符)
比如使用了 ctrl ,那么就会拼接上
if( !$event.ctrlKey ) return null
也就是说,当你按的不是 ctrl 的时候,直接 return,不执行下面
在比如一个 middle,鼠标中间,会拼接上
if( ""button" in $event && $event.button !== 1" ) return null
当鼠标点击的按键不是 1 的时候,直接 return,不执行下面(下面就会执行你绑定的方法)
键盘修饰符现在再来看看 Vue 是怎么给你拼接上按键的修饰符的吧,来看一个方法
这个方法,后面会用到,这里预言,记住哦
function genKeyFilter(keys) { var key = keys.map(genFilterCode).join("&&") return ` if( !("button" in $event) && ${ key } ) return null ` }
这个方法跟前面的方法,拼接的内容差不多,只是条件不一样,条件是这样
` if( !("button" in $event) && ${ key } ) return null `
这个 key 肯定是一个很重要的条件,怎么得到呢,两个东西
keys,genFilterCode
现在一个个来说一下
1参数 keys是一个数组,保存的是你添加的按键修饰符,可以是数字,可以是键名,比如
于是 keys = [ "enter" , 86 ],其中 enter 就是 enter 键,Vue 内部定义过,86 就是 键名 v 的值
然后会遍历 keys,逐个调用 genFilterCode 去处理每个键值
现在看下它的源码
2出现的函数 genFilterCodefunction genFilterCode(key) { // 绑定的 modifier 是数字 var keyVal = parseInt(key); if (keyVal) { return "$event.keyCode!==" + keyVal } // 绑定的 modifier 是键名 var keyCode = keyCodes[key]; var keyName = keyNames[key]; return ` _k( $event.keyCode , ${ key } , ${ keyCode }, ${ $event.key } , ${ keyName } ) ` }如果参数 key 是数字
这就简单了,直接 返回字符串
"$event.keyCode!==" + keyVal
于是回调就可以拼接上一个执行条件,比如key 是数字 86
if( !("button in $event) && $event.keyCode!== 86 ) return null
这句话的意思就是当你按下的键不是 86 的时候,就return,因为你监听的是 86 嘛,按其他键肯定是不执行的啦
如果参数 key 是键名如果你的 key 是个名字,经过 parseInt 就变成 NAN,于是就走到下面一步了
哦吼,好像直接返回了呢,并且还从 keyName 和 keyCode 两个对象中去获取键值和键名
keyName 和 keyCode 前面已经列出来了,就是两个大对象,忘记可以翻上去看,Vue 内部的变量,这里获取会得到什么呢
比如你的键名是 enter, 获取之后
那么 keyName = "Enter" , keyCode = 13
但是,这个键名不一定要存在 Vue 自己定义的 keyName 和 keyCode 中,所以你从这两个变量中获取也可能是 undefined
因为这个键名,你可以自定义
自定义键名具体可以看官方文档
https://cn.vuejs.org/v2/api/#...
3genFilterCode 返回值我们现在来看看他的返回值
`_k( $event.keyCode , ${ key } , ${ keyCode }, $event.key , ${ keyName } )`
看着上面的字符串,我们一个个讲
1 其中参数五个参数,key 就是你绑定的键名或键值,keyName 和 keyCode 上面讲过了
剩下两个参数
$event.keyCode / $event.key
$event 是事件对象,不用说了
$event.keyCode 是你按下的键的值
$event.key 是你按下的键的名
比如你按下 v,事件对象就会包括这个键的信息
2 方法 _k 是什么返回 _k 函数的话,比如按下 v,事件回调的执行条件就变成
if( !("button" in $event) && _k( $event.keyCode , "v" , undefined, $event.key , undefined ) ) return null
也就是说,每次按下键盘,都会去调用一遍 _k 去检查按键
_k 的本体其实是函数 checkKeyCodes,源码如下
function checkKeyCodes( eventKeyCode, key, keyCode, eventKeyName, keyName ) { // 比如 key 传入的是自定义名字 aaaa // keyCode 从Vue 定义的 keyNames 获取 aaaa 的实际数字 // keyName 从 Vue 定义的 keyCode 获取 aaaa 的别名 // 并且以用户定义的为准,可以覆盖Vue 内部定义的 var mappedKeyCode = config.keyCodes[key] ||keyCode; // 该键只在 Vue 内部定义的 keyCode 中 if (keyName && eventKeyName && !config.keyCodes[key]) { return isKeyNotMatch(keyName, eventKeyName) } // 该键只在 用户自定义配置的 keyCode 中 else if (mappedKeyCode) { return isKeyNotMatch(mappedKeyCode, eventKeyCode) } // 原始键名 else if (eventKeyName) { return hyphenate(eventKeyName) !== key } }
乍一看,好像很复杂?其实这个函数作用,就是检查 key
比如你绑定 v,当你按下 v 的时候,这个函数就返回 false
如果按下其他键,则返回 true,回调执行条件为 真,所以就直接 return null,不会执行下面绑定的回调
番外:"button" in $event
当鼠标按下的时候,这个表达式就为 true
而键盘按下的时候,事件对象不存在 button 这个值
所以 【!"button" in $event】 表示按键时必须没有鼠标按下
但是这个条件只有在【 非内置修饰符 】才会存在,就是说 ctrl 那些键是没有这个条件的
所以你可以绑定 click + ctrl 事件,比如
这么绑定时候,直接点击是无效的,必须按下 ctrl 再点击
但是你不能绑定 click + 普通按键,比如 click+v
就算你这么绑定了,也是没用的,直接点击也会触发 而不用 v
下面现在我们继续分析上面的 checkKeyCodes
1 config.keyCodes这个就是你自己配置的键值键名表,下面是我的配置
2 函数 isKeyNotMatch检查按下的键,是否【不和】 配置的键值对 匹配
function isKeyNotMatch( expect, actual ) { if (Array.isArray(expect)) { return expect.indexOf(actual) === -1 } else { return expect !== actual } }
注意里面的匹配符是 ===-1 和 !== ,意思就是不匹配才返回 true,匹配则返回 false
比如你按下的键是 enter,Vue 内部配置了 { enter:"Enter" }
而 参数 actual 是调用 isKeyNotMatch 传入的事件对象获取的键名,也是 "Enter"
于是调用这个函数
isKeyNotMatch( "Enter" , "Enter" )
匹配成功,返回 false,也就是 checkKeyCode(_k) 返回false,回调执行过滤条件为假,不会 return null,这样才会执行下面
配置数组的话也是同理
3 出现的函数 hyphenatevar hyphenate = function(str) { return str.replace(/B([A-Z])/g, "-$1").toLowerCase() }
把驼峰改成 - 命名
Vue 官方文档说了,不支持驼峰修饰符
4 checkKeyCodes 的 匹配顺序1、先匹配 Vue 内部配置的键值对
但是要保证这个键不存在用户重定义中,因为必须要用户自定义为主
2、匹配用户自定义的键值对
3、匹配原始键值对
也就是说,你直接写键盘上的键名也是ok 的
像这样,就绑定了 键 v 和 键 b
哎哟,我们已经讲了这么多啊,下面准备开始我们的主菜了
在一开始的 genHandlers 中出现的女猪脚 genHandler,用于逐个处理 修饰符的 她
genHandler这个函数有点长,用于处理修饰符,但是其实内容并不复杂,逻辑也很清晰,但是第一眼肯定看得烦,虽然我已经极大简化,你可以跳到后面的解析,对照着看一下
function genHandler(name,handler) { // 没有绑定回调,返回一个空函数 if (!handler) { return "function(){}" } // 如果绑定的是数组,则逐个递归一遍 if (Array.isArray(handler)) { return "[" + handler.map(handler) => { return genHandler(name, handler); }).join(",") + "]" } // 开始解析单个回调 var isMethodPath = simplePathRE.test(handler.value); var isFunctionExpression = fnExpRE.test(handler.value); // 没有 modifier if (!handler.modifiers) { // 是一个方法,或者有 function 关键字,可以直接作为回调 if (isMethodPath || isFunctionExpression) { return handler.value } // 内联语句,需要包裹一层 return "function($event){" + handler.value + ";}" } else { var code = ""; // 保存按键修饰符 var genModifierCode = ""; // 保存内部修饰符 var keys = []; for (var key in handler.modifiers) { if (modifierCode[key]) { ....被抽出,放在后面 } // 精确系统修饰符 else if (key === "exact") { ....被抽出,放在后面 } // 普通按键 else { keys.push(key); } } // 开始拼接事件回调!!!! // 拼接 Vue 定义外的按键修饰符 if (keys.length) { code += genKeyFilter(keys); } // 把 prevent 和 stop 这样的修饰符在 按键过滤之后执行 if (genModifierCode) { code += genModifierCode; } // 事件回调主体 var handlerCode = isMethodPath // 执行你绑定的函数 ? "return " + handler.value + "($event)" : ( isFunctionExpression // 如果回调包含 function 关键字,同样执行这个函数 ? "return " + handler.value + "($event)" : handler.value; ) return ` function($event) { ${ code + handlerCode } } ` } }
下面开始一点点解析上面的代码
1出现的正则// 包含function 关键字 var fnExpRE = /^([w$_]+|([^)]*?))s*=>|^functions*(/; // 普通方法名 var simplePathRE = /^[A-Za-z_$][w$]*(?:.[A-Za-z_$][w$]*|["[^"]*?"]|["[^"]*?"]|[d+]|[[A-Za-z_$][w$]*])*$/;
不解释这么多,直接看例子
fnExpRE 专门匹配有函数体的
simplePathRE 专门匹配函数名
所以其中的 isMethodPath 表示事件绑定的值 是否是函数名
isFunctionExpression 表示绑定值是否是 函数体
2绑定的 handler.modifiers在 parse 阶段,会把绑定的事件解析过,所有的 modifiers 都解析成一个对象
比如
所以 handler.modifiers 可以直接遍历而取到每一个 修饰符
3 收集内部修饰符这是被抽取的代码
if (modifierCode[key]) { // 拼接内部的的事件修饰符 genModifierCode += modifierCode[key]; }
先判断这个修饰符是否存在 Vue 内部的修饰符中
关于 modifierCode 可以看上面,已经记录过啦
比如吼
那么拼接修饰符吼,genModifierCode 就是
` $event.stopPropagation(); if( "button" in $event && $event.button !== 1" ) return null `4收集系统修饰键 exact
或许你不知道什么是系统修饰键,我一开始也不知道,看了源码才知道有这个东西,如下的 exact
官方文档地址:https://cn.vuejs.org/v2/guide...
在我的理解是,取消组合键的意思
比如你绑定了 按键 v,触发的条件有几个?
1、直接 按 v 键
2、ctrl + v 、 shift +v 、 alt +v 等等组合键
如果你添加了 exact 这个修饰符的话
那么触发条件,只有你只按下这个键的时候才会触发,也就是说一起按其他键无效
来看下从 genHandler 抽出的源码
else if (key === "exact") { var modifiers = handler.modifiers; genModifierCode += genGuard( ["ctrl", "shift", "alt", "meta"] // 过滤拿到没写 modifiers 的按键 .filter(keyModifier = >{ return ! modifiers[keyModifier]; }) .map(keyModifier = >{ return "$event." + keyModifier + "Key" }) .join("||") ); }
看源码中,匹配的组合键有四个
ctrl , shift , alt ,meta
看源码中,就是筛选出你没有绑定的功能键
然后筛选出来的键被按下时,直接 return null
比如你绑定了 ctrl,并使用了 exact
那么只有你【只按下】ctrl 的时候,才会触发回调
看下拼接结果
` _c("input", { on: { "keyup" $event =>{ if (!$event.ctrlKey) return null; if ( $event.shiftKey || $event.altKey || $event.metaKey) return null; return testMethod($event) } } }) `
1、没有按下 ctrl 的时候
2、按下了 ctrl的时候,但是也按下了 shfit 等其他功能键
同理,对于其他普通键也是一样的,当你不需要使用到组合键的时候,添加 exact 就好了
5收集其他按键else{ keys.push(key); }
这是最普通的,当你添加的修饰符,不在Vue内部定义的修饰符中,也没有 exact
那么就直接数组收集你添加的按键
比如
那么 keys = [ "v" , "b" ,"c" ]
6拼接事件回调下面开始本函数最重要的部分了,打起精神嘞
拼接事件回调的三个重点
1、拼接按键修饰符
2、拼接内置修饰符
3、拼接事件回调
1 拼接按键修饰符if (keys.length) { code += genKeyFilter(keys); }
genKeyFilter 上面已经讲过了,忘记的可以回头看看,这里给个结果就好了
比如
那么 keys = [ "v" , "b" ,"c" ]
最后拼接得到的 code 是
` if( !("button" in $event)&& _k($event.keyCode,"v",undefined,$event.key,undefined)&& _k($event.keyCode,"b",undefined,$event.key,undefined)&& _k($event.keyCode,"c",undefined,$event.key,undefined) ) return null; `
调用了三个 _k 函数
2 拼接内部修饰符if (genModifierCode) { code += genModifierCode; }
genModifierCode 就是收集的 内部修饰符 和 带有 exact 的修饰符,上面有说明
拼接结果是 code
` $event.stopPropagation(); if(!$event.shiftKey) return null; `3 拼接事件回调主体
这里开始涉及到你绑定的回调了,你可以先尝试看下源码
var handlerCode = isMethodPath // 执行你绑定的函数 ? "return " + handler.value + "($event)" : ( isFunctionExpression // 如果回调包含 function 关键字,同样执行这个函数 ? "return (" + handler.value + "($event))" : handler.value; )
1、如果你绑定的是方法名
比如你绑定的方法名是
那么 handlerCode 是
"return aaa($event)"`
2、如果有函数体
比如
那么 handlerCode 是
"return (function(){}($event))"
3、如果只是内联语句
比如
那么 handlerCode 是
aa = 33
直接把这条语句当做 回调的一部分就好了
走流程不知不觉,我们已经讲了很多内容,最后我们用一个例子去玩一下流程
收集内置修饰符
1、拼接按键修饰符
2、拼接内置修饰符
3、拼接事件回调
4、包装事件回调
比如下面的模板,我们来看看怎么拼接事件
1、开始遍历修饰符 2、碰到 ctrl , 是内置修饰符,收集起来genModifierCode = ` if( !$event.ctrlKey ) return null `3、继续遍历,碰到 v,不是内置,收集到 keys
keys.push("v")4、遍历结束,开始拼接按键修饰符
调用 genKeyFilter ,传入 keys,得到 code
code = ` if( !("button" in $event)&& _k($event.keyCode,"v",undefined,$event.key,undefined) ) return null; `5、把内置修饰符放到 按键修饰符后面
code = code + genModifierCode6、开始拼接事件回调,是个方法名
handlerCode = ` return test($event) `7、ok,最后一步,开始组装
` funciton($event){ if( !$event.ctrlKey ) return null; if( !("button" in $event)&& _k($event.keyCode,"v",undefined,$event.key,undefined) ) return null; return test($event) }`
然后事件拼接就完成了!!!!!!!!!!
哟,别忘了,我们是要拼接到 render,赶快往上翻到 genHandlers
上面讲的只是事件回调,如果要拼接到 render 回调,我们还要做下操作
加上事件名,拼接 on 对象字符串里面,像这样,具体看上面的 genHandlers
`,on:{ keyup: ...上面我们拼接的回调 } `
谢谢大家观看,辛苦了
最后鉴于本人能力有限,难免会有疏漏错误的地方,请大家多多包涵,如果有任何描述不当的地方,欢迎后台联系本人,领取红包
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/106725.html
摘要:写文章不容易,点个赞呗兄弟专注源码分享,文章分为白话版和源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于版本如果你觉得排版难看,请点击下面链接或者拉到下面关注公众号也可以吧原理源码版之节点数据拼接上一篇我们 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究...
摘要:还原的难度就在于变成模板了,因为其他的什么等是原封不动的哈哈,可是直接照抄最后鉴于本人能力有限,难免会有疏漏错误的地方,请大家多多包涵,如果有任何描述不当的地方,欢迎后台联系本人,有重谢 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得排版...
摘要:写文章不容易,点个赞呗兄弟专注源码分享,文章分为白话版和源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于版本如果你觉得排版难看,请点击下面链接或者拉到下面关注公众号也可以吧原理白话版终于到了要讲白话的时候了 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究...
摘要:一旦我们检测到这些子树,我们可以把它们变成常数,这样我们就不需要了在每次重新渲染时为它们创建新的节点在修补过程中完全跳过它们。否则,吊装费用将会增加好处大于好处,最好总是保持新鲜。 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得排版难看,...
摘要:首先,兄弟,容我先说几句涉及源码很多,篇幅很长,我都已经分了上下三篇了,依然这么长,但是其实内容都差不多一样,但是我还是毫无保留地给你了。 写文章不容易,点个赞呗兄弟专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也...
阅读 3197·2023-04-26 01:30
阅读 665·2021-11-08 13:15
阅读 1774·2021-09-24 10:35
阅读 997·2021-09-22 15:41
阅读 1929·2019-08-30 15:44
阅读 592·2019-08-30 13:22
阅读 1004·2019-08-30 13:06
阅读 1196·2019-08-29 13:22