快應用的事件監聽機制和組件間通信
說起事件,做前端開發的朋友一定不會陌生。事件,即網頁上的一系列行為,可以是瀏覽器行為,如頁面完成了加載,頁面關閉;或是用戶操作行為,如用戶輸入操作,用戶點擊按鈕等,這些行為會被JavaScript監測到,并執行相應的邏輯代碼??梢哉f,前端的交互行為與事件機制息息相關,對于前端開發者而言,掌握好事件機制是絕對必要的。
所謂組件,即封裝起來的具有獨立功能的UI部件。試想,如果開發一個復雜的頁面,開發者把所有的UI部分寫在一個文件中,這樣的代碼顯然可維護性很低。但我們如果用組件的方式去重新思考UI構成,將UI上每一個功能相對獨立的模塊定義成組件,然后將小的組件通過組合或者嵌套的方式構成大的組件,進而完成整體UI的開發。這樣,我們不僅提高了代碼的復用性,且整體結構清晰,維護性則大大提高。
組件化界面
本文將介紹在快應用開發中,事件相關的主要API以及事件的監聽、觸發機制,同時會介紹快應用中組件是如何通信的。閱讀本文前,建議先了解快應用相關基礎知識。
自定義事件的監聽、移除與觸發
$on 用于監聽自定義事件;$off移除對應的事件監聽;$emit()、$dispatch()、 $broadcast()等方法可用于觸發事件。
$on(evtName, fnHandler)事件
在當前頁面注冊監聽事件, 可監聽$emit()、 $dispatch()、 $broadcast()等觸發的自定義事件,不能用于注冊組件節點的事件響應。
示例如下:
- export default {
- onInit(){
- this.$on('customEvtType1', this.customEvtType1Handler)
- },
- customEvtType1Handler(evt){
- // 事件類型,事件參數
- console.info(`觸發事件:類型:${evt.type}, 參數: ${JSON.stringify(evt.detail)}`);
- }
- }
解釋一下
'customEvtType1'為該組件上自定義的事件名稱,customEvtType1Handler為當'customEvtType1'事件被觸發時,要執行的回調函數。
$off(evtName, fnHandler)
移除事件監聽,參數 fnHandler 為可選,傳遞僅移除指定的響應函數,不傳遞則移除此事件的所有監聽。
示例如下:
- export default {
- removeEventHandler () {
- // 不傳遞fnHandler:移除所有監聽
- this.$off('customEvtType1')
- // 傳遞fnHandler:移除指定的監聽函數
- this.$off('customEvtType1', this.customEvtType1Handler)
- }
- }
頁面的交互中可能會遇到一些非手動觸發的需求,$emit() 通過觸發當前實例上的事件,達到動態觸發事件的行為,類似jquery中的trigger方法。
$emit(evtName, evtDetail)
觸發當前實例監聽事件函數,與 $on() 配合使用,注意:$emit() 目前只觸發 $on 所監聽的事件
示例如下:
- export default {
- emitEvent () {
- this.$emit('customEvtType1', { params: '參數內容' })
- }
- }
監聽原生組件事件
原生組件即框架自帶的組件,如div,text等等,其支持一系列事件,如通用事件(如:click, disappear)、組件專有事件(如:focus)。完整的原生組件列表以及事件可在快應用官網查詢到。
開發者可以在事件回調函數中,獲取到當前觸發組件的信息,并進行進一步的操作。
在響應函數執行時通過target獲取,如:onClickHandler
在響應函數綁定時傳遞參數,如:onClickHandler2
示例如下:
- <template>
- <div class="tutorial-page">
- <text id="elNode1" item-flag="{{ argName + 1 }}" onclick = "onClickHandler">組件節點1</text>
- <text id="elNode2" item-flag="{{ argName + 2 }}" onclick = "onClickHandler2('參數1', argName)">組件節點2</text>
- </div>
- </template>
- <style lang="less">
- .tutorial-page {
- flex-direction: column;
- }
- </style>
- <script>
- export default {
- data () {
- return {
- argName: '動態參數'
- }
- },
- onClickHandler (evt) {
- // 事件類型,參數詳情
- console.info(`觸發事件:類型:${evt.type}, 詳情: ${JSON.stringify(evt.detail)}`);
- if (evt.target) {
- console.info(`觸發事件:節點:${evt.target.id}, ${evt.target.attr.itemFlag}`)
- }
- },
- onClickHandler2 (arg1, arg2, evt) {
- // 事件類型,事件參數,target
- console.info(`觸發事件:類型:${evt.type}, 參數: ${arg1}, ${arg2}`);
- }
- }
- </script>
解釋一下
onClickHandler函數如果不傳參數,默認參數env即為當前觸發組件的實例;若傳遞了參數,如onClickHandler2,則參數安順序排列,env為***一個參數。
觸發原生組件事件
用戶可通過手動操作觸發事件,如點擊事件等,除此之外,也可以在代碼中通過$emitElement()完成事件的動態觸發,類似上文自定義組件中的$emit()方法。
$emitElement(evtName, evtDetail, id)
可以觸發指定組件id的事件,通過evt.detail獲取傳遞的參數;該方法僅用于原生組件,對自定義組件無效。
示例如下:
- <template>
- <div class="tutorial-page">
- <text onclick="emitElement">觸發組件節點的事件:click</text>
- <text id="elNode1" item-flag="{{ argName + 1 }}" onclick = "onClickHandler">組件節點1</text>
- <text id="elNode2" item-flag="{{ argName + 2 }}" onclick = "onClickHandler2('參數1', argName)">組件節點2</text>
- </div>
- </template>
- <style lang="less">
- .tutorial-page {
- flex-direction: column;
- }
- </style>
- <script>
- export default {
- data () {
- return {
- argName: '動態參數'
- }
- },
- onClickHandler (evt) {
- // 事件類型,參數詳情
- console.info(`觸發事件:類型:${evt.type}, 詳情: ${JSON.stringify(evt.detail)}`);
- if (evt.target) {
- console.info(`觸發事件:節點:${evt.target.id}, ${evt.target.attr.itemFlag}`)
- }
- },
- onClickHandler2 (arg1, arg2, evt) {
- // 事件類型,事件參數,target
- console.info(`觸發事件:類型:${evt.type}, 參數: ${arg1}, ${arg2}`);
- },
- emitElement () {
- // 注意:通過此類方式的事件不會攜帶target屬性,開發者可以通過detail參數實現
- this.$emitElement('click', { params: '參數內容' }, 'elNode1')
- }
- }
- </script>
解釋一下
Click事件可通過用戶點擊操作觸發,也可通過$emitElement觸發。
自定義組件
上文曾提到原生組件,通常原生組件是我們系統中最基礎的組件,然而我們在做一個稍微復雜的頁面時,如果每個頁面都只用原生組件搭建,那這樣的代碼的可維護性會差很多。打個比方,就好比一個人口眾多的國家,沒有省、市、縣這些單位,而是只以個人為單位,難以想象這個國家的管理將有多難。道理類似,自定義組件,我們可以根據具體的業務邏輯,把頁面按照功能拆成多個模塊,每個模塊負責其中的一個功能部分,***頁面將由這些模塊組合搭建起來,讓代碼結構更加清晰,易于維護。
自定義組件是開發者編寫的組件,使用起來和Native原生組件一樣,最終按照組件的<template>來渲染;同時開發起來又和頁面一樣,擁有ViewModel實現對數據、事件、方法的管理,這么來看,頁面也是一種特殊的自定義組件,無需引入即可使用,同時服務于整個頁面。
編寫自定義組件
示例如下:
- <template>
- <div class="tutorial-page">
- <text class="tutorial-title">自定義組件:</text>
- <text>{{ prop1 }}</text>
- <text>{{ prop2Object.name }}</text>
- </div>
- </template>
- <style lang="less">
- .tutorial-page {
- flex-direction: column;
- padding-top: 20px;
- .tutorial-title {
- font-weight: bold;
- }
- }
- </style>
- <script>
- // 子組件
- export default {
- props: [
- 'prop1',
- 'prop2Object'
- ],
- data: {
- },
- onInit () {
- console.info(`外部傳遞的數據:`, this.prop1, this.prop2Object)
- }
- }
- </script>
兩點注意
一是自定義組件比頁面組件的不同之處在于多了一個props屬性,用于聲明該組件可接受的外部數據傳遞;props是一個數組,數組中每個元素是暴露的屬性。
二是如果屬性名稱使用駝峰定義,如:prop2Object,那么在外部傳遞數據時請使用-連接,如:prop2-object
引入自定義組件
引入自定義組件的方式和我們平時常用的方式不同,我們平時通常會用require或import的方式引入組件,而在快應用框架中,需要使用<import>標簽來引入。
示例如下:
- <import name="comp-part1" src="./part1"></import>
<import>標簽中的的src屬性指定自定義組件的地址,name屬性指定在<template>組件中引用該組件時使用的標簽名稱
最終頁面定義與引入方式如下:
- <import name="comp-part1" src="./part1"></import>
- <template>
- <div class="tutorial-page">
- <text class="tutorial-title">頁面組件:</text>
- <text>{{ data1 }}</text>
- <text>{{ data2.name }}</text>
- <text onclick="evtType1Emit">觸發$broadcast()</text>
- <comp-part1 prop1="{{data1}}" prop2-object="{{data2}}" onevt-type3="evtTypeHandler"></comp-part1>
- </div>
- </template>
- <style lang="less">
- .tutorial-page {
- flex-direction: column;
- padding: 20px 10px;
- .tutorial-title {
- font-weight: bold;
- }
- }
- </style>
- <script>
- // 父組件
- export default {
- data: {
- data1: '傳遞字符串',
- data2: {
- name: '傳遞對象'
- }
- },
- onInit () {
- this.$page.setTitleBar({ text: '父子組件通信' })
- this.$on('evtType2', this.evtTypeHandler)
- },
- evtTypeHandler (evt) {
- console.info(`父組件:事件響應: `, evt.type, evt.detail)
- // 結束事件傳遞
- // evt.stop()
- },
- evtType1Emit () {
- this.$broadcast('evtType1', { params: '額外參數' })
- }
- }
- </script>
解釋一下
上面的代碼中有幾點需要說明:
1 在comp-part1標簽中,我們看到這樣一個屬性,onevt-type3="evtTypeHandler",這是指,在該節點上綁定了名為evtType3的方法,被觸發后,執行evtTypeHandler的函數,在下文的‘父子之間組件事件傳遞’中,會看到如何觸發該方法。
2 代碼中的evtType1Emit方法,該方法通過調用$broadcast方法,觸發了名為'evtType1'的事件,并傳遞了params參數,'evtType1'事件也可以在下文‘父子組件之間事件傳遞’中看到。
傳遞數據與數據改造
如上面所述,父組件向子組件傳遞數據,通過在子組件的props屬性中聲明對外暴露的屬性名稱,然后在組件引用標簽上聲明傳遞的父組件數據。
如果你需要在子組件中對數據進行改造,但又不想改動父組件數據時,可以使用$watch()來滿足需求。如果是監聽對象中的屬性,參數請使用.分割,如:$watch(xxx.xxx.xxx, methodName)
示例如下:
- <script>
- // 子組件
- export default {
- props: [
- 'prop1',
- 'prop2Object'
- ],
- data () {
- return {
- upperProp1: this.prop1
- }
- },
- onInit () {
- console.info(`外部傳遞的數據:`, this.prop1, this.prop2Object)
- // 監聽數據變化
- this.$watch('prop1', 'watchPropsChange')
- this.$watch('prop2Object.name', 'watchPropsChange')
- },
- /**
- * 監聽數據變化,你可以對數據處理后,設置值到data上
- * @param newV
- * @param oldV
- */
- watchPropsChange (newV, oldV) {
- console.info(`監聽數據變化:`, newV, oldV)
- this.upperProp1 = newV && newV.toUpperCase()
- }
- }
- </script>
解釋一下
上面是子組件的代碼,我們看到data中定義了upperProp1,同時也看到watchPropsChange方法中,有兩個參數,一個是newV,指變化后的屬性值,oldV指原先的屬性值,將newV賦值給upperProp1,這樣在子組件中對數據upperProp1進行改造,就不會改動父組件原先的數據。
父子組件之間的事件傳遞
當子組件對數據進行改造后,把最終數據交給父組件甚至往上,往往有兩種辦法
1、父組件傳遞的數據本身就是對象,子組件直接修改的就是這個對象中的屬性;那么父組件同樣也就拿到了最終數據
2、子組件在data中保存了一份內部數據,需要交給父組件:子組件通過$dispatch()完成事件觸發,父組件通過$on()綁定事件并響應,如:evtType2;
類似于2,子組件在data中保存了一份內部數據,需要交給父組件:子組件通過$emit()觸發在節點上綁定的事件來執行父組件的方法,如:evtType3;
示例如下:
- <script>
- // 子組件
- export default {
- props: [
- 'prop1',
- 'prop2Object'
- ],
- data () {
- return {
- upperProp1: this.prop1
- }
- },
- onInit () {
- console.info(`外部傳遞的數據:`, this.prop1, this.prop2Object)
- // 綁定VM的自定義事件
- this.$on('evtType1', this.evtTypeHandler)
- // 這里我認為官網的代碼示例存在問題,因此注釋掉了,此處應該將其移至父組件的onInit方法中。
- //this.$on('evtType2', this.evtTypeHandler)
- },
- evtTypeHandler (evt) {
- console.info(`子組件:事件響應: `, evt.type, evt.detail)
- // 結束事件傳遞
- // evt.stop()
- },
- evtType2Emit () {
- this.$dispatch('evtType2', { params: '額外參數' })
- },
- evtType3Emit () {
- this.$emit('evtType3', { params: '額外參數' })
- }
- }
- </script>
解釋一下
在上文我已做了如下說明
1 在父組件的comp-part1標簽中,我們看到這樣一個屬性,onevt-type3 = "evtTypeHandler",這是指,在該節點上綁定了名為evtType3的方法,如果子組件中evtType3Emit調用執行,則會執行父組件中的evtTypeHandler的函數, 從而完成子組件與父組件的通信。
2 父組件中的evtType1Emit方法,該方法通過調用$broadcast方法,觸發了名為'evtType1'的事件,并傳遞了params參數,'evtType1'事件則注冊在子組件的onInit方法中,從而完成父組件與子組件的通信。
所以,框架向開發者提供了雙向的事件傳遞。
向下傳遞:父組件觸發,子組件響應;調用parentVm.$broadcast()完成向下傳遞,如:evtType1
向上傳遞:子組件觸發,父組件響應;調用childVm.$dispath()完成向上傳遞,如:evtType2
兄弟組件之間的通信
傳統的兄弟等非父子組件之間通信,是通過觀察者模型來完成。觀察者模式的作用是當一個對象的狀態發生變化時,能夠自動通知其他關聯對象,自動刷新對象狀態。
開發者可以自己寫一個Pub/Sub模型實現通信解耦;不過本文并不詳細介紹如何通過觀察者模式來實現組件間通信,這個題目夠另寫一篇文章了。
其實,在業務邏輯相對簡單的情況下,我們可以使用ViewModel本身的事件綁定來處理。兄弟組件的相同點是,他們擁有相同的父組件,所以,父組件將是兄弟組件通信的橋梁,可以在以下代碼中看到這個過程。
示例如下:
子組件定義了Sub端的邏輯處理,有processMessage()、customEventInVm2(),后者同使用$on效果一致
- <template>
- <div class="tutorial-page">
- <text class="tutorial-title">自定義組件2:</text>
- <text>處理消息:{{msg}}</text>
- <text>事件內容:{{eventDetail}}</text>
- </div>
- </template>
- <style lang="less">
- </style>
- <script>
- // 子組件: part2
- export default {
- props: [
- ],
- data () {
- return {
- msg: null,
- eventDetail: null
- }
- },
- processMessage (msg) {
- const now = (new Date).toISOString()
- this.msg = `${now}: ${msg}`
- },
- /**
- * 通過events對象:綁定事件,和on效果一致
- */
- events: {
- customEventInVm2 (evt) {
- const now = (new Date).toISOString()
- this.eventDetail = `${now}: ${evt.detail}`
- }
- }
- }
- </script>
另外一個兄弟組件可以通過父組件中建立相互引用達到相互持有ViewModel的目的,通過在生命周期onReady()中執行establishRef()實現,如下代碼所示:
- <template>
- <div class="tutorial-page">
- <!-- 兄弟VM通信 -->
- <comp-part2 id="sibling1"></comp-part2>
- <comp-part3 id="sibling2"></comp-part3>
- </div>
- </template>
- <style lang="less">
- </style>
- <script>
- // 父組件
- export default {
- onReady () {
- this.establishRef()
- },
- /**
- * 建立相互VM的引用,父組件將兩個兄弟組件聯系了起來
- */
- establishRef () {
- const siblingVm1 = this.$vm('sibling1')
- const siblingVm2 = this.$vm('sibling2')
- siblingVm1.parentVm = this
- siblingVm1.nextVm = siblingVm2
- siblingVm2.parentVm = this
- siblingVm2.previousVm = siblingVm1
- }
- }
- </script>
那么另外一個子組件的Pub端定義就很簡單了,執行sendMesssage()即可完成觸發,如下代碼所示:
- <template>
- <div class="tutorial-page">
- <text class="tutorial-title">自定義組件3:</text>
- <text onclick="sendMesssage">點擊發送消息</text>
- </div>
- </template>
- <style lang="less">
- </style>
- <script>
- // 子組件: part3
- export default {
- sendMesssage () {
- if (this.previousVm) {
- // Way1. 調用方法
- this.previousVm.processMessage('兄弟之間通信的消息內容')
- // Way2. 觸發事件
- this.previousVm.$emit('customEventInVm2', '兄弟之間通信的消息內容')
- }
- }
- }
- </script>
解釋一下
通過上面的例子,我們可以看到,comp-part2和 comp-part3在父組件中通過nextVm和previousVm建立了‘兄弟關系’,基于此,它們之間可以直接調用對方的方法(如processMessage),或會通過$emit方法觸發對方監聽的事件'customEventInVm2'。
結尾
本文對快應用開發中的事件監聽以及觸發方式做了介紹,通過學習我們能夠更好的分離業務邏輯,減少方法響應上的耦合。另外,通過掌握自定義組件的開發,會讓我們的項目結構更加明朗,更易于維護;同時了解父子、兄弟組件之間的數據通信,更是完成好快應用開發的必要條件。當然,本文多數素材來源于官方文檔中的示例,但對其中模糊之處做了相應解釋和補充,希望能夠給讀者些許幫助。如有不足之處,歡迎指正。