天天看點

jquery動态綁定事件的方法_Vue小Case 如何動态綁定多個事件(内附源碼解析)

編者按:本文作者高峰,360奇舞團前端工程師,W3C性能工作組/WOT工作組成員。

本文閱讀時間約為 16 分鐘,其中有一段苦澀的代碼,如懶得看的話,可直接跳至最後一部分查收總結。

最近遇到這樣一個需求,需要在抽象出來的元件上綁定使用者傳入的事件及其處理函數,并且事件名、數量不定,也就是動态綁定多個事件。印象中,文檔中沒有提到過類似的用法。是以 Google 一下。

然後就遇到了下面這樣一個可愛的故事。

一、“可愛”的故事

在搜尋的過程中,看到了這樣一條結果“初學 vue,請問怎麼在元素上綁定多個事件”[1],并且還是 Vue 的 Issue,那我當然得優先看看了。Issue 中具體的内容如下:

jquery動态綁定事件的方法_Vue小Case 如何動态綁定多個事件(内附源碼解析)

透過螢幕感受到了尤雨溪大佬的一絲絲嚴厲。心疼小哥 3 秒,不知道會不會是以想過放棄 Vue,放棄前端 ?。

不過大佬就是要這麼有威嚴不是嘛。嚴厲的同時還不忘給我們指一條“明路”。

我們可以按照圖中的方式試一下(示例 1[2]),會發現好像并不可行。這是為什麼呢?當然不是說大佬給我們“瞎指路”,這其實應該是某個版本疊代中支援的功能,隻不過在現在的版本中不支援了(示例中試了 1.0,2.0 好像也不行),現在的版本中會有新的寫法,具體内容下面會詳述。

好了,可愛的故事到此結束,下面我們一起讨論下如何實作動态綁定多個事件。

二、如何動态綁定多個事件

2.1 使用

vm.$on

實作

vm.$on

大家一定都用過,其用法如下:

vm.$on( event, callback )

,其中

event

參數不僅可以是個字元串,還可以是個事件名稱組成的數組。

是以借助

vm.$on

,我們可以通過如下的方式(示例 2[3])實作動态綁定多個事件。

new Vue({
el: '#container',
mounted: function() {
const eventMaps = {
'my-event1': this.eventHandler,
'my-event2': this.eventHandler,
    }

// 通過 forEach 周遊綁定多個事件
Object.keys(eventMaps).forEach((event) => {
this.$on(event, eventMaps[event])
    })

// vm.$on 傳遞數組,綁定多個事件
this.$on(['my-event3', 'my-event4'], this.eventHandler)

this.triggerEvents()
  },
methods: {
    eventHandler(eventName) {
console.log(eventName + ' 事件被觸發!')
    },
// 不同時間間隔觸發多個事件
    triggerEvents() {
      setTimeout(() => {
this.$emit('my-event1', 'my-event1')
      }, 1000)

      setTimeout(() => {
this.$emit('my-event2', 'my-event2')
      }, 2000)

      setTimeout(() => {
this.$emit('my-event3', 'my-event3')
this.$emit('my-event4', 'my-event4')
      }, 3000)
    }
  }
})
           

上述代碼中,我們可以通過

forEach

的方式循環周遊來綁定多個不同的事件及處理函數。

此外在 Vue 2.2.0+版本,還可以通過給

vm.$on

傳遞數組參數為多個不同的事件綁定同一個處理函數。注意, 這種方式有個限制,隻能綁定同一個處理函數。

運作上述代碼,會依次(1s/2s/3s)觸發

my-event1

my-event2

my-event3/my-event4

事件。

最後有一點需要注意,這一方式有一個局限,即該方式隻能用于綁定自定義事件,不支援原生的 DOM 事件。如果你想眼見為實的話,那就點一下試試吧(示例 3[4]),你會發現通過

this.$on(['click', 'mouseover'], this.eventHandler)

并不會被觸發。

文檔裡有提到

vm.$on

不支援原生事件,這主要是因為

$on/$off/$emit

這一套接口,是 Vue 本身實作的事件處理機制,隻能用來處理元件的自定義事件。第三部分我也會帶領大家看一下源碼中關于這一部分的實作。

2.2 使用

v-on

指令實作

如果隻是實作動态綁定事件,大家應該都知道,文檔[5]裡也有提到。從 Vue 2.6.0 開始,可以通過如下的方式

...

為一個動态的事件名綁定處理函數。

但是如果想要動态綁定多個事件及處理函數應該如何實作呢?

其實和

v-bind

綁定全部對象屬性類似(隻不過文檔裡沒提到,不知道是為啥),我們可以通過如下方式

v-on="{event1: callback, event2: callback, ...}"

同時綁定多個事件及處理函數(與第一部分提到的“明路”類似)。示例代碼如下(示例 4[6]):

HTML:

<div id="container" v-on="eventMaps">
  動态綁定多個事件
div>
           

JavaScript:

new Vue({
el: '#container',
computed: {
     eventMaps() {
return {
'click': this.clickHandler,
'mouseover': this.mouseoverHandler,
'my-event1': this.eventHandler,
       }
     }
  },
mounted: function() {
this.triggerEvents()
  },
methods: {
    clickHandler(eventName) {
console.log('原生 click 事件被觸發!')
    },
    eventHandler(eventName) {
console.log(eventName + ' 事件被觸發!')
    },
    mouseoverHandler(eventName) {
console.log('原生 mouseover 事件被觸發!')
    },
    triggerEvents() {
      setTimeout(() => {
console.log('主動觸發my-event1事件')
this.$emit('my-event1', 'my-event1')
      }, 5000)
    }
  }
})
           

運作一下,我們會發現兩個原生事件都會被監聽處理。而通過這種方式綁定了一個自定義事件,主動觸發事件後,事件并沒有被處理。通過這一現象,似乎可以得出結論通過

v-on={...}

綁定多個事件時,不支援元件自定義事件。但其實并不是這樣。

通過

v-on={...}

綁定多個事件時,如果是在 DOM 元素上綁定,則隻支援原生事件,不支援自定義事件;如果是在 Vue 元件上綁定,則隻支援自定義事件,不支援原生事件。如下所示(示例 5[7]),當是在自定義元件上綁定事件時,不支援原生事件。

到這裡就比較尴尬了,Vue 原生支援的兩種方式都不能很好地滿足需求,

vm.$on

不支援原生 DOM 事件,

v-on={...}

綁定多事件時,會因為宿主元素的不同有不同的限制。

此外

v-on={...}

這種用法綁定的時候是不可以使用修飾符,否則會有如下警告:

[Vue warn]: v-on without argument does not support modifiers.

。但是對于原生事件,我們有着一些很便捷的修飾符可以使用,這種情況下又該如何使用呢?

下面,我們通過 Vue 的源碼一起來分析下這些問題。

三、Vue 中

$on

v-on

的實作

3.1

$on

$emit

$off

以及

$once

的實作

如果你對于 Node 中 EventEmitter 或者其他事件機制的實作邏輯有過了解,那麼對于這四個執行個體方法的實作一定不會陌生。它們就是基于常見的釋出訂閱模式實作的。下面我們分别看下它們的實作。

3.1.1

$on

的實作

我們先來看 Vue 中

$on

的實作,部分代碼如下:

Vue.prototype.$on = function (event: string | Array, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
      vm.$on(event[i], fn)
    }
  } else {
    (vm._events[event] || (vm._events[event] = [])).push(fn)
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
      vm._hasHookEvent = true
    }
  }
return vm
}
           

可以看到

else

中的部分,vm 執行個體上有一個

_events

對象,其中的值為

$on

所監聽的事件及其處理函數數組。當事件對應的屬性不存在時,建立一個空數組,将新的處理函數推入;存在時,直接推入新的處理函數。

如果參數是數組,則遞歸一下。也就是說使用

$on

傳遞數組參數時,我們還可以傳多元數組,感興趣的同學可以自己試一下(示例 6[8])。

Tips:

$on

$emit

$off

以及

$once

傳回的都還是 vm 示例,是以還可以鍊式調用!

3.1.2

$emit

的實作

$emit

的部分代碼如下:

Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
// 其他非核心邏輯
let cbs = vm._events[event]
if (cbs) {
    cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
      invokeWithErrorHandling(cbs[i], vm, args, vm, info)
    }
  }
return vm
}
           

這一段代碼的核心邏輯就是擷取

$on

中事件所對應的處理函數數組,如果存在,則依次調用數組中的處理函數。

3.1.3

$off

的實作

$off

的部分代碼如下:

這段代碼較長,解釋請直接看代碼裡的注釋
Vue.prototype.$off = function (event?: string | Array, fn?: Function): Component {
const vm: Component = this

// 如果沒有提供參數,則移除所有的事件處理函數。
// 記住,是所有事件對應的所有處理函數,夠快夠狠。
if (!arguments.length) {
    vm._events = Object.create(null)
return vm
  }
// 如果事件名是個數組,則遞歸$off。與$on中類似,是以可以多元數組
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
      vm.$off(event[i], fn)
    }
return vm
  }
// 以下情況為指定了特定事件的處理
const cbs = vm._events[event]
// 如果事件本身就沒有處理函數,則直接傳回
if (!cbs) {
return vm
  }
// 如果沒有指定要移除的處理函數,則直接清空該事件的所有處理函數
if (!fn) {
    vm._events[event] = null
return vm
  }
// 如果指定了處理函數,則在事件對應的處理函數中找到該處理函數,移出數組
let cb
let i = cbs.length
while (i--) {
    cb = cbs[i]
// 這裡的cb.fn是為了相容$once中的用法
if (cb === fn || cb.fn === fn) {
      cbs.splice(i, 1)
break
    }
  }
return vm
}
           

3.1.4

$once

的實作

$once

的實作邏輯如下:

Vue.prototype.$once = function (event: string, fn: Function): Component {
const vm: Component = this
function on () {
    vm.$off(event, on)
    fn.apply(vm, arguments)
  }
  on.fn = fn
  vm.$on(event, on)
return vm
}
           

其實

$once

的實作邏輯也比較簡單,封裝了一個

on

的函數,然後在内部調用的時候會執行一次

$off

,進而實作調用一次就登出事件。

最後解釋下

vm.$on

中的事件修飾符,因為除

once

外的修飾符都隻能用于原生的 DOM 事件,而

vm.$on

不支援原生 DOM 事件,是以不會有相關實作,僅僅實作了可以支援自定義事件的

once

3.2

v-on="{...}"

的實作邏輯

本文要讨論的是

v-on="{...}"

實作綁定多事件的邏輯,但因為實作多事件的邏輯和正常的

v-on:event

用法是兩個不同的邏輯分支,本文隻讨論多事件的邏輯。如果對于正常用法感興趣的話,可以參考一下韭菜[9]的《深入剖析 Vue 源碼 - 揭秘 Vue 的事件機制》[10]一文。

3.2.1 模闆編譯收集

v-on

指令

與正常的

v-on:eventName

類似,不帶事件名的

v-on="{...}"

也會在模闆編譯時候進行處理收集。

在源碼中的

src/compiler/parser

中的

processAttrs

函數中,有如下一段邏輯:

// 是否是指令
export const dirRE = process.env.VBIND_PROP_SHORTHAND
  ? /^v-|^@|^:|^\.|^#/
  : /^v-|^@|^:|^#/
// v-on及其簡寫的正則
export const onRE = /^@|^v-on:/
// v-bind及其簡寫的正則
export const bindRE = /^:|^\.|^v-bind:/

// 處理屬性
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, syncGen, isDynamic
for (i = 0, l = list.length; i < l; i++) {
    name = rawName = list[i].name
    value = list[i].value

// 是否是指令屬性
if (dirRE.test(name)) {
// ...

// v-bind 處理
if (bindRE.test(name)) {
// ...
// 正常v-on 處理
      } else if (onRE.test(name)) {
// ... 參考上面提到的文章,本文重點不在這裡
// v-on動态綁定多事件比較特殊,會按照通用指令來處理
      } else {
        name = name.replace(dirRE, '')
// parse arg
const argMatch = name.match(argRE)
let arg = argMatch && argMatch[1]
        isDynamic = false
if (arg) {
          name = name.slice(0, -(arg.length + 1))
if (dynamicArgRE.test(arg)) {
            arg = arg.slice(1, -1)
            isDynamic = true
          }
        }
// *** 重點在這裡 ***
        addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])
// ...
     }
    } else {
// 正常屬性處理邏輯
    }
  }
}
           

如上代碼,通過

v-on

動态綁定多事件時,在 Vue 的處理邏輯中,是被當做一般指令來處理的,最後會調用

addDirective

方法。此時

value

的值仍是對象字面量的字元串。

3.2.2 on 指令的邏輯

調用

addDirective

之後,會把

v-on="{...}"

這一用法當做普通指令,我們找到

src/compiler/directives/on.js

。其代碼如下:

export default function on (el: ASTElement, dir: ASTDirective) {
// 不可以使用修飾符,否則會有如下警告:
if (process.env.NODE_ENV !== 'production' && dir.modifiers) {
    warn(`v-on without argument does not support modifiers.`)
  }
  el.wrapListeners = (code: string) => `_g(${code},${dir.value})`
}
           

核心内容是

_g

函數,是以我們再次找到

_g

對應的函數

bindObjectListeners

(在

src/core/instance/render-helpers/index.js

中有對應關系),其内部具體邏輯如下:

export function bindObjectListeners (data: any, value: any): VNodeData {
// 這時value已經被轉成對象字面量了,而不是字元串了。
if (value) {
// 如果不是對象字面量會報錯
if (!isPlainObject(value)) {
      process.env.NODE_ENV !== 'production' && warn(
'v-on without argument expects an Object value',
this
      )
    } else {
// 處理對象,将其加入到data.on中記錄下來
const on = data.on = data.on ? extend({}, data.on) : {}
for (const key in value) {
const existing = on[key]
const ours = value[key]
        on[key] = existing ? [].concat(existing, ours) : ours
      }
    }
  }
return data
}
           

3.2.3

updateListeners

上一步中,收集到的

data.on

,最後會在 VNode 的生命周期中被

updateListeners

消費,該函數的核心邏輯如下:

export function updateListeners (
  on: Object,
  oldOn: Object,
  add: Function,
  remove: Function,
  createOnceHandler: Function,
  vm: Component) {
let name, def, cur, old, event
for (name in on) {
    def = cur = on[name]
    old = oldOn[name]
    event = normalizeEvent(name)
// 如果處理函數未定義,則警告
if (isUndef(cur)) {
      process.env.NODE_ENV !== 'production' && warn(
`Invalid handler for event "${event.name}": got ` + String(cur),
        vm
      )
// 如果不存在舊的處理函數
    } else if (isUndef(old)) {
if (isUndef(cur.fns)) {
        cur = on[name] = createFnInvoker(cur, vm)
      }
if (isTrue(event.once)) {
        cur = on[name] = createOnceHandler(event.name, cur, event.capture)
      }
      add(event.name, cur, event.capture, event.passive, event.params)
// 如果存在舊的處理函數的處理邏輯
    } else if (cur !== old) {
      old.fns = cur
      on[name] = old
    }
  }
for (name in oldOn) {
if (isUndef(on[name])) {
      event = normalizeEvent(name)
      remove(event.name, oldOn[name], event.capture)
    }
  }
}
           

函數中有一個

normalizeEvent

需要關注一下,該方法會通過名稱解析出來部分修飾符,分别是

passive/once/capture

。為什麼會隻有這幾個修飾符呢,應該是因為這幾個修飾符是在處理函數中通過代碼無法實作的。

下面我們看下具體的函數邏輯:

const normalizeEvent = cached((name: string): {
name: string,
once: boolean,
capture: boolean,
passive: boolean,
  handler?: Function,
  params?: Array
} => {const passive = name.charAt(0) === '&'
  name = passive ? name.slice(1) : nameconst once = name.charAt(0) === '~' // Prefixed last, checked first
  name = once ? name.slice(1) : nameconst capture = name.charAt(0) === '!'
  name = capture ? name.slice(1) : namereturn {
    name,
    once,
    capture,
    passive
  }
})           

從代碼可以看出,

passive

是事件名前加

&

once

是事件名前加

~

capture

是事件名前加

!

,并且三個值會有如上的順序關系。

如果我們需要添加這三個修飾符,可以通過類似這樣的方式添加

v-on="{'!click': addTodo, focus: addTodo}"

。至于其他的

stop/prevent

等其他修飾符,則需要在處理函數内部進行實作。

最後說下原生事件和自定義事件的問題,正常的

v-on:event

用法是會處理

native

修飾符的,這時候會維護兩個事件數組

events

nativeEvents

(源碼中應該是

on

nativeOn

),最後用于綁定原生事件和自定義事件,而

v-on={...}

用法不會處理

native

修飾符,最後隻會根據元素類型來綁定事件,是以**  該方式用在 DOM 原生元素上時,隻支援原生事件;用在元件上時,隻支援自定義事件**。

四、總結

今天我們讨論了如何在 Vue 中動态綁定多個事件。主要使用以下兩種方式:

  1. 通過

    vm.$on

    執行個體方法進行實作:通過

    forEach

    可以實作不同僚件不同函數的綁定;通過數組參數可以實作不同僚件同一函數,并且數組可以是多元數組。該方式有一個局限,即隻能支援元件的自定義事件。

    此外,

    $on/$off/$emit/$once

    接口傳回值仍為 vm 執行個體,是以可以鍊式調用
  2. 通過

    v-on="{...}"

    實作,該方式用在 DOM 原生元素上時,隻支援原生事件;用在元件上時,隻支援自定義事件。可以通過“

    passive

    是事件名前加

    &

    once

    是事件名前加

    ~

    , 

    capture

    是事件名前加

    !

    ”的方式支援

    passive/once/capture

    (有順序要求),其他修飾符需要在處理函數内手動實作。

以上就是我們今天要講的兩種動态綁定事件的方式,其中第二種方式已經能夠滿足我們的大部分使用需求。

如果仍舊覺得不滿足需求,可以試試用自定義指令來實作,筆者有空也會再來一篇。

參考資料

[1]

“初學 vue,請問怎麼在元素上綁定多個事件”: https://github.com/vuejs/vue/issues/1050

[2]

示例 1: https://jsbin.com/wegomutele/1/edit?html,js,output

[3]

示例 2: https://jsbin.com/juvowisedi/1/edit?html,js,output

[4]

示例 3: https://jsbin.com/vewafuxeya/1/edit?html,js,output

[5]

文檔: https://cn.vuejs.org/v2/guide/syntax.html#%E5%8A%A8%E6%80%81%E5%8F%82%E6%95%B0

[6]

示例 4: https://jsbin.com/nayiyomayi/edit?html,js,output

[7]

示例 5: https://jsbin.com/yevejofasa/6/edit?html,js,output

[8]

示例 6: https://jsbin.com/zoporitugu/1/edit?html,js,output

[9]

韭菜: https://juejin.im/user/5865c0921b69e6006b3145a1

[10]

《深入剖析 Vue 源碼 - 揭秘 Vue 的事件機制》: https://juejin.im/post/5d5a5dbd6fb9a06acc0084dd

繼續閱讀