在vue
內部初始化時會爲每一個組件實例掛載一個this._events
私有的空對象屬性:vue
vm._events = Object.create(null) // 沒有__proto__屬性
複製代碼
這個裏面存放的就是當前實例上的自定義事件集合,也就是自定義事件中心,它存放着當前組件全部的自定義事件。和自定義事件相關的API
分爲如下四個:this.$on
、this.$emit
、this.$off
、this.$once
,它們會往這個事件中心中添加、觸發、移除對應的自定義事件,從而組成了vue
的自定義事件系統,接下來看下它們都是怎麼實現的。面試
描述:監聽當前實例上的自定義事件。事件能夠由
vm.$emit
觸發,回調函數會接收全部傳入事件觸發函數的額外參數。vuex
export default {
created() {
this.$on('test', res => {
console.log(res)
})
},
methods: {
handleClick() {
this.$emit('test', 'hello-vue~')
}
}
}
複製代碼
以上示例首先在created
鉤子內往當前組件實例的事件中心_events
中添加一個名爲test
的自定義事件,第二個參數爲該自定義事件的回調函數,而觸發handleClick
這個方法後,就會在事件中心中嘗試找到test
自定義事件,觸發它並傳遞給回調函數hello-vue~
這個字符串,從而打印出來。咱們來看下$on
的實現:數組
Vue.prototype.$on = function (event, fn) {
const hookRE = /^hook:/ //檢測自定義事件名是不是hook:開頭
const vm = this
if (Array.isArray(event)) { // 若是第一個參數是數組
for (let i = 0; i < event.length; i++) {
this.$on(event[i], fn) // 遞歸
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
// 若是有對應事件名就push,沒有建立爲空數組而後push
if (hookRE.test(event)) { // 若是是hook:開頭
vm._hasHookEvent = true // 標誌位爲true
}
}
return vm
}
複製代碼
以上就是$on
的實現了,它接受兩個參數,自定義事件名event
和對應的回調函數fn
。主要就是往事件中心_events
下掛載對應的event
事件名key
,而事件名對應的key
又是一個數組形式,這樣相同事件名的回調會在一個數組以內。而接下來的_hasHookEvent
標誌位表示是否監聽組件的鉤子函數,這個以後示例說明。bash
描述:觸發當前實例上的事件,附加參數都會傳給監聽器回調。app
Vue.prototype.$emit = function (event) {
const vm = this
let cbs = vm._events[event] // 找到事件名對應的回調集合
if (cbs) {
const args = toArray(arguments, 1) // 將附加參數轉爲數組
for (let i = 0; i < cbs.length; i++) {
cbs[i].apply(vm, args) // 挨個執行對應的回調集合
}
}
return vm
}
複製代碼
而$emit
的實現會更好理解些,首先從事件中心中找到event
對應的回調集合,而後將$emit
其他參數轉爲args
數組,最後挨個執行回調集合內的回調並傳入args
。經過這麼一對樸實的API
能夠幫咱們理解三件小事:函數
1. 理解自定義事件原理
app.vue
<template>
<child-component @test='handleTest' />
</template>
export default {
methods: {
handleTest(res) {
console.log(res)
}
}
}
----------------------------------------
child.vue
<template>
<button @click='onClick'>btn</button>
</template>
export default {
methods: {
onClick() {
this.$emit('test', 'hello-vue~')
}
}
}
複製代碼
以上是父子組件經過自定義事件通訊,想必你們很是熟悉。自定義事件的實現原理和一般解釋的會不一樣,它們的原理是父組件在通過編譯模板後,會將定義在子組件上的自定義事件test
及其回調handleTest
經過$on
添加到子組件的事件中心中,當子組件經過$emit
觸發test
自定義事件時,會在它的事件中心中去找test
,找到後傳遞hello-vue~
給回調函數並執行,不過由於回調函數handleTest
是在父組件做用域內定義的,因此看起來就像是父子組件之間通訊般。post
2. 監聽組件的鉤子函數
也就是$on
內自定義事件名以前是hook:
的狀況,能夠監聽組件的鉤子函數觸發:ui
app.vue
<template>
<child-component @hook:created='handleHookEvent' />
</template>
複製代碼
以上示例爲當子組件的created
鉤子觸發時,就觸發父組件內定義的handleHookEvent
回調。接下來讓咱們再看一個官網的示例,使用這個特性如何幫咱們寫出更優雅的代碼:
監聽組件鉤子以前:
mounted () {
this.picker = new Pikaday({ // Pikaday是一個日期選擇庫
field: this.$refs.input,
format: 'YYYY-MM-DD'
})
},
beforeDestroy () { // 銷燬日期選擇器
this.picker.destroy()
}
監聽組件鉤子以後:
mounted() {
this.attachDatepicker('startDateInput')
this.attachDatepicker('endDateInput') // 同時爲兩個input添加日期選擇
},
methods: {
attachDatepicker(refName) { // 封裝爲一個方法
const picker = new Pikaday({ // Pikaday是一個日期選擇庫
field: this.$refs[refName], // 爲input添加日期選擇
format: 'YYYY-MM-DD'
})
this.$once('hook:beforeDestroy', () => { // 監聽beforeDestroy鉤子
picker.destroy() // 銷燬日期選擇器
}) // $once和$on相似,只是只會觸發一次
}
}
複製代碼
首先不用在當前實例下掛載一個額外的屬性,其次能夠封裝爲一個方法,複用更方便。
3. 不借助
vuex
跨組件通訊
再開發組件庫時,由於都是獨立的組件,從而引入vuex
這種強依賴是不現實的,並且不少時候是用插槽來放置子組件,因此子組件的位置、嵌套、數量並不會肯定,從而在組件庫內完成跨組件的通訊就尤其重要。
經過接下來的示例介紹組件庫中會運用到的一種,使用$on
和$emit
來實現跨組件通訊,子組件經過父組件的name
屬性找到對應的實例,找到後使用$emit
觸發父組件的自定義事件,而在這以前父組件已經使用$on
完成了自定義事件的添加:
export default {
methods: { // 混入mixin使用
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root // 找父組件
let name = parent.$options.name // 父組件的name屬性
while (parent && (!name || name !== componentName)) { // 和傳入的componentName進行匹配
parent = parent.$parent // 一直向上查找
if (parent) {
name = parent.$options.name // 從新賦值name
}
}
if (parent) { // 找到匹配的組件實例
parent.$emit.apply(parent, [eventName].concat(params)) // $emit觸發自定義事件
}
}
}
}
複製代碼
接下來介紹表單驗證組件內的使用案例:
iForm
爲整個表單,一個是
iFormItem
爲其中的某個表單項:
iForm組件:
<template>
<div> <slot /> </div> // 只有一個插槽
</template>
<script>
export default {
name: "iForm", // 組件名很重要
data() {
return {
fields: [] // 收集全部表單項的集合
};
},
created() {
this.$on("on-form-item-add", field => { // $on必須得比$emit先執行,由於要先添加嘛
this.fields.push(field) // 添加到集合內
});
},
methods: {
validataAll() { // 驗證全部的接口方法
this.fields.forEach(item => {
item.validateVal() // 執行每一個表單項內的validateVal方法
});
}
}
};
</script>
複製代碼
模板只有一個slot
插槽,這個組件主要是作兩件事,將全部的表單項的實例收集到fields
內,提供一個能夠驗證全部表單項的方法validataAll
,而後看下iFormItem
組件:
<template>
<div>
<input v-model="curValue" style="border: 1px solid #aaa;" />
<span style="color: red;" v-show="showTip">輸入不能爲空</span>
</div>
</template>
<script>
import emitter from "./emitter" // 引入以前的dispatch方法
export default {
name: "iFormItem",
mixins: [emitter], // 混入
data() {
return {
curValue: "", // 表單項的值
showTip: false // 是否驗證經過
};
},
created() {
this.dispatch("iForm", "on-form-item-add", this) // 將當前實例傳給iForm組件
},
methods: {
validateVal() { // 某個表單項的驗證方法
if (this.curValue === "") { // 不能爲空
this.showTip = true // 驗證不經過
}
}
}
};
</script>
複製代碼
看到這裏咱們知道了原來這種表單驗證原理是將每一個表單項的實例傳入給iForm
,而後在iForm
內遍歷的執行每一個表單項的驗證方法,從而能夠一次性驗證完全部的表單項。表單驗證調用方式:
<template>
<div>
<i-form ref='form'> // 引用
<i-form-item />
<i-form-item />
<i-form-item />
<i-form-item />
<i-form-item />
</i-form>
<button @click="submit">提交</button>
</div>
</template>
<script>
import iForm from "./form"
import iFormItem from "./form-item"
export default {
methods: {
submit() {
this.$refs['form'].validataAll() // 驗證全部
}
},
components: {
iForm, iFormItem
}
};
</script>
複製代碼
這裏就使用了$on
和$emit
這麼一對API
,經過組件的名稱去查找組件實例,不論嵌套以及數量,而後使用事件API
去跨組件傳遞參數。
注意點:當
$on
和$emit
配合使用時,$on
要優先與$emit
執行。由於首先要往實例的事件中心去添加事件,才能被觸發。
描述:移除自定義事件監聽器,不過根據傳入的參數分爲三種形式:
export default {
created() {
this.$on('test1', this.test1)
this.$on('test2', this.test2)
},
mounted() {
this.$off() // 沒有參數,清空事件中心
}
}
-------------------------------------------
export default {
created() {
this.$on('test1', this.test1)
this.$on('test2', this.test2)
},
mounted() {
this.$off('test1') // 在事件中心中移除test1
}
}
-------------------------------------------
export default {
created() {
this.$on('test1', this.test1)
this.$on('test1', this.test3)
this.$on('test2', this.test2)
},
mounted() {
this.$off('test1', this.test3) // 在事件中心中移除事件test1的test3回調
}
}
複製代碼
知道了這個API
的調用方式以後,接下來看下$off
的實現方式:
Vue.prototype.$off = function (event, fn) {
const vm = this
if (!arguments.length) { // 若是沒有傳遞參數
vm._events = Object.create(null) // 重置事件中心
return vm
}
if (Array.isArray(event)) { // event若是是數組
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn) // 遞歸清空
}
return vm
}
if (!fn) { // 只傳遞了事件名沒回調
vm._events[event] = null // 清空對應全部的回調
return vm
}
const cbs = vm._events[event] // 獲取回調集合
let cb
let i = cbs.length
while (i--) {
cb = cbs[i] // 回調集合裏的每一項
if (cb === fn || cb.fn === fn) { // cb.fn爲$once時掛載的
cbs.splice(i, 1) // 找到對應的回調,從集合內移除
break
}
}
return vm
}
複製代碼
也是分爲了三種狀況,根據參數的不一樣作分別處理。
描述:監聽一個自定義事件,可是隻觸發一次,在第一次觸發以後移除監聽器。
效果和$on
是相似的,只是說觸發一次以後會從事件中心中移除。因此它的實現思路也很好理解,首先經過$on
實現功能,當觸發以後從事件中心中移除這個事件。來看下它的實現原理:
Vue.prototype.$once = function (event, fn) {
const vm = this
function on () {
vm.$off(event, on)
fn.apply(vm, arguments)
}
on.fn = fn // 回調掛載到on下,移除時好作判斷
vm.$on(event, on) // 將on添加到事件中心中
return vm
}
複製代碼
首先將回調fn
掛載到on
函數下,將on
函數註冊到事件中心去,觸發自定義事件時首先會在$emit
內執行on
函數,在on
函數內執行$off
將on
函數移除,而後執行傳入的fn
回調。這個時候事件中心沒有了on
函數,回調函數也執行了一次,完成$once
功能~
事件
API
總結:$on
往事件中心添加事件;$emit
是觸發事件中內心的事件;$off
是移除事件中內心的事件;$once
是觸發一次事件中內心的事件。哪怕是如此不顯眼的API
,再理解了它們的實現原理後,也能讓咱們再更多場景更好的使用它們~
最後按照慣例咱們仍是以一道vue
可能會被問到的面試題做爲本章的結束(想不到事件相關特別好的題目~)。
面試官微笑而又不失禮貌的問道:
懟回去:
this.$emit
觸發事件時,會在當前實例的事件中心去查找對應的事件,而後執行它。不過這個事件回調是在父組件的做用域裏定義的,因此$emit
裏的參數會傳遞給父組件的回調函數,從而完成父子組件通訊。順手點個贊或關注唄,找起來也方便~