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

寫文章不容易,點個讚唄兄弟


專一 Vue 源碼分享,文章分爲白話版和 源碼版,白話版助於理解工做原理,源碼版助於瞭解內部詳情,讓咱們一塊兒學習吧
研究基於 Vue版本 【2.5.17】

若是你以爲排版難看,請點擊 下面連接 或者 拉到 下面關注公衆號也能夠吧html

【Vue原理】Compile - 源碼版 之 generate 拼接綁定的事件 vue

今天咱們來探索事件的拼接啦,上一篇咱們已經講過了generate 階段 節點數據的拼接api

事件也屬於其中一部份內容,可是因爲內容實在太多太多太多太多,因此打算單獨拿出來說啦數組

這就是 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

}

沒錯,事件,分爲 組件原生事件 和 事件spa

組件原生事件

顧名思義,就是給組件綁定的原生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

由於這個鍵名,你能夠自定義

自定義鍵名

公衆號

具體能夠看官方文檔

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 出現的函數 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

公衆號

官方文檔地址:https://cn.vuejs.org/v2/guide...

在個人理解是,取消組合鍵的意思

好比你綁定了 按鍵 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


最後

鑑於本人能力有限,不免會有疏漏錯誤的地方,請你們多多包涵,若是有任何描述不當的地方,歡迎後臺聯繫本人,領取紅包

公衆號

相關文章
相關標籤/搜索