源碼位置
src/core/instance/events.js
逐行分析
- 首先看一下它里面用到的另一個文件中暴露的方法,在
src/core/vdom/helpers/update-listeners.js
中。
// 更新一個組件實例內部的事件的listener的方法
/**
* on 一個listeners對象
* oldOn 舊的listeners對象
* add 綁定listener的方法
* remove 移除listener的方法
* createOnceHandler 綁定一次的方法
* vm 組件實例
**/
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]
// 這個方法在這個文件里面可以找到,獲取這個解析這個事件名稱,得到的結果是{ name, once, capture, passive, handle, params },看這個事件是否是只執行一次,是否冒泡,以及是否阻止默認行為等屬性
event = normalizeEvent(name)
/* istanbul ignore if */
if (__WEEX__ && isPlainObject(def)) {
cur = def.handler
event.params = def.params
}
// isUndef方法在src/core/shared/util.js中定義,判斷參數是否等於undefined或者null
// 如果當前的定義的事件對應的對象是個空對象或者未定義,那就拋出一個警告,缺少事件處理的回調
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)
}
// 如果once屬性為true,就綁定一個只執行一次的listener,也就是在執行過后直接off掉該事件
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) {
// 相當於事件監聽的listener發生了變化
old.fns = cur // 舊的listener中改變fns指向cur
on[name] = old
}
}
// 移除掉舊監聽集合中的無效監聽
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name)
remove(event.name, oldOn[name], event.capture)
}
}
}
- 再來看看主要的事件機制的代碼,先看幾個內部定義的方法
// target是一個中間變量,當需要進行綁定事件的時候會指向組件實例,使用完成之后就會變成null
// 這個就是傳給上面方法的add
function add (event, fn) {
target.$on(event, fn)
}
// 傳給上面方法的remove
function remove (event, fn) {
target.$off(event, fn)
}
// 調用上面的方法去初始化這個組件實例的listeners
export function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm
updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
target = undefined
}
// 暴露了一個初始化組件實例的_events和_hasHookEvent的屬性,組件初始化的時候會調用該方法
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events,這一步就是將父組件監聽子組件發射的事件全部存在_events中
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
// 生成一個只執行一次的事件監聽,也是傳給上面提到的方法的參數,實現也很簡單,就是執行完成了就解綁
function createOnceHandler (event, fn) {
const _target = target
return function onceHandler () {
const res = fn.apply(null, arguments)
if (res !== null) {
_target.$off(event, onceHandler)
}
}
}
- 看完了用到的一些方法,再看看主要的代碼,也就是
on
, once
, off
, emit
的實現。
// 這個在組件初始化的時候也會調用,直接給組件實例附加$on,$off,$emit方法。
export function eventsMixin (Vue: Class<Component>) {
const hookRE = /^hook:/
// 看來$on同時監聽多個事件,調用同一個回調, 'click', ['click', 'hover']應該都可以,數組應該是2.2之后才能用的
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
// 如果是數組,就直接遞歸調用本身的$on方法
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
// 單個事件名稱的時候,向組件實例的_events豎向中添加一組監聽,一個事件可以有多個監聽事件的回調
(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
// 處理hook事件
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
// 沒啥好看的,就是在執行之后解綁了
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
}
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this
// all,如果調用$off()沒有傳參,默認清空_events記錄的所有事件監聽映射
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// array of events,數組的話就直接遍歷之后遞歸
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
// specific event
const cbs = vm._events[event]
if (!cbs) { // 沒有找到對應的handler就直接返回,沒有需要移除的
return vm
}
if (!fn) { // 如果沒有傳入對應的handler,那就直接把該事件的監聽handler置為null
vm._events[event] = null
return vm
}
// specific handler,如果傳入了對應的handler,就從事件監聽回調隊列中找到對應的handler並移除
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break
}
}
return vm
}
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
if (process.env.NODE_ENV !== 'production') {
const lowerCaseEvent = event.toLowerCase()
// 自己發射自己監聽,就推薦使用v-on綁定事件就可以
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
tip(
`Event "${lowerCaseEvent}" is emitted in component ` +
`${formatComponentName(vm)} but the handler is registered for "${event}". ` +
`Note that HTML attributes are case-insensitive and you cannot use ` +
`v-on to listen to camelCase events when using in-DOM templates. ` +
`You should probably use "${hyphenate(event)}" instead of "${event}".`
)
}
}
// 這個地方一開始,我也沒看明白,后來發現是因為初始化的時候把父組件監聽子組件發射的事件已經綁定到子組件的_events中了,所以如果子組件在發射了事件之后,如果發現父組件已經有了對應的處理方法,就執行對應的回調
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
}
}
小結
- 個人理解,事件綁定就是通過原生的事件觸發其組件內部已經定義好的句柄,
emit
看起來是發射了一個事件出去,實際只是執行了在初始化就傳遞過來的父組件綁定的監聽回調,相當於一個ajax
的回調,因為不知道子組件啥時候會發射事件,我之前還以為是父組件里面會去捕獲到子組件的事件,然后在父組件中去執行回調。