【Vue原理】Compile - 源码版 之 generate 拼接绑定的事件

写文章不容易,点个赞呗兄弟

专一 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工做原理,源码版助于了解内部详情,让咱们一块儿学习吧 研究基于 Vue版本 【2.5.17】html

若是你以为排版难看,请点击 下面连接 或者 拉到 下面关注公众号也能够吧vue

【Vue原理】Compile - 源码版 之 generate 拼接绑定的事件 api

今天咱们来探索事件的拼接啦,上一篇咱们已经讲过了generate 阶段 节点数据的拼接数组

事件也属于其中一部份内容,可是因为内容实在太多太多太多太多,因此打算单独拿出来说啦bash

这就是 Compile 的最后一篇文章了!终于终于发完了....真的发恶心了,估计网上找不着我这么详细的 compile 文章了(找到当我没说)app

公众号

本文章很长,一万多字,可是其实都是代码和例子,帮助咱们理解的,没有什么,一路看下来绝壁没有压力,源码也是简化过的ide

公众号

好吧,开始正文函数

从上篇文章咱们知道了,节点数据拼接,调用的是 genData$2 方法学习

如今咱们就来回顾下这个方法(只保留了 事件 相关)ui

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 方法上


genHandlers

function 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出现的函数 genFilterCode

function 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

由于这个键名,你能够自定义

自定义键名

公众号

具体能够看官方文档

cn.vuejs.org/v2/api/#key…

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 出现的函数 hyphenate

var hyphenate = function(str) {    
    return str.replace(/\B([A-Z])/g, '-$1').toLowerCase()
}
复制代码

把驼峰改为 - 命名

公众号

Vue 官方文档说了,不支持驼峰修饰符

公众号

4 checkKeyCodes 的 匹配顺序

一、先匹配 Vue 内部配置的键值对

可是要保证这个键不存在用户重定义中,由于必需要用户自定义为主

二、匹配用户自定义的键值对

三、匹配原始键值对

也就是说,你直接写键盘上的键名也是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*=>|^function\s*\(/;



// 普通方法名

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

公众号

官方文档地址:cn.vuejs.org/v2/guide/ev…

在个人理解是,取消组合键的意思

好比你绑定了 按键 v,触发的条件有几个?

一、直接 按 v 键

二、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)
        }
    }
}) `
复制代码

一、没有按下 ctrl 的时候

二、按下了 ctrl的时候,可是也按下了 shfit 等其余功能键

同理,对于其余普通键也是同样的,当你不须要使用到组合键的时候,添加 exact 就行了

5收集其余按键

else{
    keys.push(key);
}
复制代码

这是最普通的,当你添加的修饰符,不在Vue内部定义的修饰符中,也没有 exact

那么就直接数组收集你添加的按键

好比

公众号

那么 keys = [ 'v' , 'b' ,'c' ]

6拼接事件回调

下面开始本函数最重要的部分了,打起精神嘞

公众号

拼接事件回调的三个重点

一、拼接按键修饰符

二、拼接内置修饰符

三、拼接事件回调

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;
    )
复制代码

一、若是你绑定的是方法名

好比你绑定的方法名是

公众号

那么 handlerCode 是

"return aaa($event)"`
复制代码

二、若是有函数体

好比

公众号

那么 handlerCode 是

"return (function(){}($event))"
复制代码

三、若是只是内联语句

好比

公众号

那么 handlerCode 是

aa = 33
复制代码

直接把这条语句当作 回调的一部分就行了


走流程

不知不觉,咱们已经讲了不少内容,最后咱们用一个例子去玩一下流程

收集内置修饰符

一、拼接按键修饰符

二、拼接内置修饰符

三、拼接事件回调

四、包装事件回调

好比下面的模板,咱们来看看怎么拼接事件

公众号

一、开始遍历修饰符

公众号

二、碰到 ctrl , 是内置修饰符,收集起来

genModifierCode = ` 
    if( !$event.ctrlKey ) return null
 `
复制代码

三、继续遍历,碰到 v,不是内置,收集到 keys

keys.push('v')
复制代码

四、遍历结束,开始拼接按键修饰符

调用 genKeyFilter ,传入 keys,获得 code

code = `
    if(
        !('button' in $event)&&
        _k($event.keyCode,"v",undefined,$event.key,undefined)
    ) 
    return null;
`
复制代码

五、把内置修饰符放到 按键修饰符后面

code = code + genModifierCode
复制代码

六、开始拼接事件回调,是个方法名

handlerCode = ` 
    return test($event)
 `
复制代码

七、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: ...上面咱们拼接的回调  } 

`
复制代码

谢谢你们观看,辛苦了

image


最后

鉴于本人能力有限,不免会有疏漏错误的地方,请你们多多包涵,若是有任何描述不当的地方,欢迎后台联系本人,领取红包

公众号
相关文章
相关标签/搜索