開始
這段時間一直在看vue的原始碼,原始碼非常多和雜,所以自己結合資料和理解理出了一個主線,然後根據主線去剝離其他的一些知識點,然後將各個知識點逐一學習。這裡主要是分析的事件系統的實現。
正文
一、瞭解使用方式
在分析之前先了解下幾個api的使用方式:
vm.$on(event, callback)
-
引數:
-
{string | Array<string>} event
(陣列只在 2.2.0+ 中支援) {Function} callback
-
-
用法:
$on
事件需要兩個引數,一個是監聽的當前例項上的事件名,一個是事件觸發的回撥函式,回撥函式接受的是在事件出發的時候額外傳遞的引數。 - 例子:
vm.$on(`test`, function (msg) {
console.log(msg)
})
vm.$emit(`test`, `hi`)
// => "hi"
vm.$once(event, callback)
$once
事件整體上來說和$on
事件的使用方式差不多,但是event只支援字串也就是說只支援單個事件。並且該事件再觸發一次後就移除了監聽器。
- 例子
vm.$once(`testonce`, function (msg) {
console.log(msg)
})
vm.$off([event, callback])
-
引數:
{string | Array<string>} event(僅在 2.2.2+ 支援陣列)
{Function} [callback]
-
用法:移除自定義事件監聽器
- 如果沒有提供引數,則移除所有的事件監聽器
- 如果只提供了事件,則移除該事件所有的監聽器;
- 如果同時提供了事件與回撥,則只移除這個回撥的監聽器。
- 例子:
vm.$off()
vm.$off(`test`)
vm.$off(`test1`, function (msg) {
console.log(msg)
})
vm.$off([`test1`,`test2`], function (msg) {
console.log(msg)
})
vm.$emit(event, [..args])
-
引數:
-
{string} event
要觸發的事件名 -
[...args]
可選
-
- 用法:
觸發當前例項上的事件。附加引數都會傳給監聽器回撥。
- 例子
vm.$emit(`test`, `觸發自定義事件`)
二、原始碼分析
事件的初始化工作
我們在使用自定義事件的api的時候,肯定有個地方是需要來存我們的事件和回撥的地方。在vue
中大部分的初始化工作都是在core/instance/init.js
的initMixin
方法中。所以我們能夠在initMixin
看到initEvents
方法。
// initEvents
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
上面的程式碼可以看到,在初始化Vue
事件的時候,在vm
例項上面掛載了一個_events
的空物件。後面我們自己呼叫的自定義事件都存在裡面。
因為vue本身在元件巢狀的時候就有子元件使用父元件的事件的時候。所以就可以通過updateComponentListeners
方法把父元件事件監聽器(比如click)傳遞給子元件。(這裡不做過多討論)
自定義事件的掛載方式
自定義事件的掛載是在eventsMixin
方法中實現的。這裡面將四個方法掛在Vue的原型上面。
Vue.prototype.$on
Vue.prototype.$once
Vue.prototype.$off
Vue.prototype.$emit
Vue.prototype.$on的實現
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$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
}
第一個引數就是自定義事件,因為可能是陣列,所以判斷如果是陣列,那麼就迴圈呼叫this.$on
方法。
如果不是陣列,那麼就直接向最開始定義的_events
物件集合裡面新增自定義事件。
所以這個時候_events
物件生成的格式大概就是下面:
vm._events={
`test`:[fn,fn...],
`test1`:[fn,fn...]
}
Vue.prototype.$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
}
這裡定義了一個on
函式。接著把fn
賦值給on.fn
。最後在呼叫的是vm.$on
。這裡傳入的就是事件名和前面定義的on
函式。on
函式在執行的時候會先移除_events
中對應的事件,然後呼叫fn
所以分析下得到的是:
vm._events={
`oncetest`:[
function on(){
vm.$off(event,on)
fn.apply(vm,arguments)
} ,
...
]
}
Vue.prototype.$off的實現
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this
// all
// 如果沒有傳任何引數的時候,直接清楚所有掛在_events物件上的所有事件。
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// array of events
// 如果第一個引數是陣列的話,那麼就迴圈呼叫this.$off方法
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$off(event[i], fn)
}
return vm
}
// specific event
// 獲取對應事件所有的回撥可能是個陣列
const cbs = vm._events[event]
// 沒有相關的事件的時候直接返回vm例項
if (!cbs) {
return vm
}
// 如果只傳入了事件名,那麼清除該事件名下所有的事件。 也就是說 vm._events = {`test`: null, ...}
if (!fn) {
vm._events[event] = null
return vm
}
// 如果傳入的第二個引數為真,那麼就去cbs裡面遍歷,在cbs中找到和fn相等的函式,然後通過splice刪除該函式。
if (fn) {
// specific 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 的實現
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
if (process.env.NODE_ENV !== `production`) {
const lowerCaseEvent = event.toLowerCase()
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}".`
)
}
}
// 匹配到事件列表,該列表是一個json。
let cbs = vm._events[event]
if (cbs) {
//將該json轉化成為真正的陣列
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
// 迴圈遍歷呼叫所有的自定義事件。
for (let i = 0, l = cbs.length; i < l; i++) {
try {
cbs[i].apply(vm, args)
} catch (e) {
handleError(e, vm, `event handler for "${event}"`)
}
}
}
return vm
}
上面主要意思是:匹配到json中相關key值的value,這個value先轉換成真正的陣列,再迴圈遍歷陣列,傳入給的引數執行陣列中的每個函式
最後
vue中的自定義事件主要目的是為了元件之間的通訊。因為_events
物件是掛在Vue例項上的。因此每個元件是都可以訪問到vm._events
的值的,也能夠向其中push
值的。
整個自定義事件系統呢就是在vm例項上掛載一個_events的物件,可以理解為一個json,其中json的key值就是自定義事件的名稱,一個key值可能對應著多個自定義事件,因此json中每個key對應的value都是一個陣列,每次執行事件監聽都會向陣列中push相關的函式,最終通過$emit函式傳入的引數,匹配到json中相應的key,val值,從而使用給定的引數執行陣列中的函式。
最後的_events
物件:
vm._events={
`test1`:[fn,fn,fn],
`test2`:[fn],
`oncetest`:[
function on(){
vm.$off(event,on)
fn.apply(vm,arguments)
},
...
],
...
}