1. 程式人生 > >vue中的$on,$emit,v-on 三者關係

vue中的$on,$emit,v-on 三者關係

$on,$emit,v-on 三者關係

每個 Vue 例項都實現了事件介面:

使用 $on(eventName) 監聽事件
使用 $emit(eventName) 觸發事件
如果把Vue看成一個家庭(相當於一個單獨的components),女主人一直在家裡指派($emit)男人做事,而男人則一直監聽($on)著女士的指派($emit)裡eventName所觸發的事件訊息,一旦 $emit 事件一觸發,$on 則監聽到 $emit 所派發的事件,派發出的命令和執行派執命令所要做的事都是一一對應的。

Api 中的解釋:

vm.$emit( event, […args] )

引數:

{string} event
[…args]
觸發當前例項上的事件。附加引數都會傳給監聽器回撥。

vm.$on( event, callback )

引數:

{string | Array} event (陣列只在 2.2.0+ 中支援) {Function} callback

用法:

監聽當前例項上的自定義事件。事件可以由 vm.$emit 觸發。回撥函式會接收所有傳入事件觸發函式的額外引數。

<template>
  <div>
      <p @click='emit'>{{msg}}</p>
  <
/div> </template> <script> export default { name: 'demo', data () { return { msg : '點選後派發事件' } }, created () { this.$on('wash_Goods',(arg)=> { console.log(arg) }) }, methods : { emit () { this.$emit('wash_Goods',[
'fish',true,{name:'vue',verison:'2.4'}]) } } } </script>
<template>
  <div>
      <p @click='emit'>{{msg}}</p>
      <p @click='emitOther'>{{msg2}}</p>
  </div>
</template>

<script>
export default {
  name: 'demo',
  data () {
      return {
         msg : '點選後派發事件',
         msg2 : '點選後派發事件2',
      }
  },
  created () {

      this.$on(['wash_Goods','drive_Car'],(arg)=> {
          console.log('真多事')
      })
      this.$on('wash_Goods',(arg)=> {
          console.log(arg)
      })
      this.$on('drive_Car',(...arg)=> {
          console.log(BMW,Ferrari)
      })
  },
  methods : {
      emit () {
         this.$emit('wash_Goods','fish')
      },
      emitOther () {
         this.$emit('drive_Car',['BMW','Ferrari'])
      }
  }
}
</script>

以上案例說了什麼呢?在文章開始的時候說了 $emit的(eventName)是與 $on(eventName) 是一一對應的,再結合以上兩人在組成家庭的之前,女人會給男人列一個手冊,告訴男人我會派發 $(emit) 那些事情,男人則會在家庭組成之前 $on(eventName)後應該如何做那些事情。

通過以上說明我來進一步解釋一下官方 Api 的意思。

vm.$emit( event, […args] )

引數:

{string} event

第一個引數則是所要派發的事件名,必須是 String 型別的。

故事中就是要告訴男人所需要執行的事情。

[…args]

第二個引數是一個任何資料型別,如果我們需要傳入多個不同的資料型別,則可以寫入陣列中,像這樣[object,Boolean,function,string,…],只要傳一個引數,我們則可以直接寫入 this.$emit(‘wash_Goods’,‘fish’)

故事中就是給男人的一個手冊,告訴男人東西放在哪裡,會需要到什麼工具等等。

vm.$on( event, callback )

引數:

{string | Array} event (陣列只在 2.2.0+ 中支援)

第一個引數是相對於 $emit (eventName) 一一對應的 $on (eventName),兩者是並存的、必須是 String 型別的。

(陣列只在2.2.0+中支援)或者是Array陣列中必須包含的是 String 項,後面再具體說。

故事中就是男人在元件一個家庭 (components) 的時候所監聽的事件名。

{Function} callback

第二個引數則是一個 function,同樣也被叫作之前回調函式,裡面可以接收到由 $emit 觸發時所傳入的引數(如果是單個引數)。

故事中是男人在接收到女人派發的事情該去做那些事情。

{string | Array} event (陣列只在 2.2.0+ 中支援)
在2.2中新增這個 Api 牽扯了另一種方式,也存在這其它的獨特用法。

繼續延續故事,當女人派發的事情多了,我相信作為男人也會覺得很煩,一旦聽到事件的時候肯定會很煩躁,總會抱怨兩句。

如果女人在組成家庭之前,告訴男人將要監聽那些事情,如果做一件事就抱怨一次,啟不是多此一舉,所以我們可以通過Array event把事件名寫成一個數組,在數組裡寫入你所想監聽的那些事件,使用共享原則去執行某些派發事件。
以上案例說明了當女人無論是派發drive_Car或者是wash_Goods事件,都會打印出事真多,再執行一一對應監聽的事件。

通常情況下,以上用法是毫無意思的。在平常業務中,這種用法也用不到,通常在寫元件的時候,讓$emit在父級作用域中進行一個觸發,通知子元件的進行執行事情。接下來,可以看一個通過在父級元件中,拿到子元件的例項進行派發事件,然而在子元件中事先進行好派好事件監聽的準備,接收到一一對應的事件進行一個回撥,同樣也可以稱之為封裝元件向父元件暴露的介面。

DEMO 下拉載入 infinite-scroll

<template>
    <div>
        <slot name="list"></slot>

        <div class="list-donetip" v-show="!isLoading && isDone">
            <slot>沒有更多資料了</slot>
        </div>

        <div class="list-loading" v-show="isLoading">
            <slot>載入中</slot>
        </div>
    </div>
</template>

<script type="text/babel">

    export default {
        data() {
            return {
                isLoading: false,
                isDone: false,
            }
        },
        props: {
            onInfinite: {
                type: Function,
                required: true
            },
            distance : {
                type : Number,
                default100
            }
        },
        methods: {
            init() {
                this.$on('loadedDone', () => {
                    this.isLoading = false;
                    this.isDone = true;
                });

                this.$on('finishLoad', () => {
                    this.isLoading = false;
                });
            },
            scrollHandler() {
                if (this.isLoading || this.isDone) return;
                let baseHeight = this.scrollview == window ? document.body.offsetHeight : this.scrollview.offsetHeight
                let moreHeight = this.scrollview == window ? document.body.scrollHeight : this.scrollview.scrollHeight;
                let scrollTop = this.scrollview == window ? document.body.scrollTop : this.scrollview.scrollTop

                if (baseHeight + scrollTop + this.distance > moreHeight) {
                    this.isLoading = true;
                    this.onInfinite()
                }
            }
        },
        mounted() {
            this.scrollview = window
            this.scrollview.addEventListener('scroll', this.scrollHandler, false);
            this.$nextTick(this.init);
        },
    }
</script>

對下拉元件載入加更的元件進行了一個簡單的封裝:

data 引數解釋:

isLoading false 代表正在執行下拉載入獲取更多資料的標識,true代表資料載入完畢
isDone false 代表資料沒有全完載入完畢,true 代表資料已經全部載入完畢
props 引數解釋:

onInfinite 父元件向子元件傳入當滾動到底部時執行載入資料的函式
distance 距離滾動到底部的設定值
從此元件中,我們進行每一步的分析

在mounted的時候,對window對像進行了一個滾動監聽,監聽的函式為scrollHandler
當isLoading,isDone任何一個為true時則退出
isloading為true時防止多次同樣載入,必須等待載入完畢
isDone為true時說明所有資料已經載入完成,沒有必要再執行scrollHandler
同時在$nextTick中進行了初始化監聽
loadedDone 一旦元件例項$emit(‘loadedDone’)事件時,執行回撥,放開載入許可權
finishLoad 一旦元件例項$emit(‘finishLoad’)事件時,執行回撥,放開載入許可權
再看看 scrollHandler函式裡發生了什麼
if (this.isLoading || this.isDone) return; 一旦一者為true,則退出,原因在mounted已經敘述過了
if (baseHeight + scrollTop + this.distance > moreHeight) 當在window物件上監聽scroll事件時,當滾動到底部的時候執行
this.isLoading = true;防止重複監聽
this.onInfinite()執行載入資料函式
父元件中呼叫 infinite-scroll 元件

<template>
      <div>
          <infinite-scroll :on-infinite='loadData' ref='infinite'>
               <ul slot='list'>
                  <li v-for='n in Number'></li>
               </ul>
          </infinite-scroll>
      </div>
</template>

<script type="text/babel">
import 'InfiniteScroll' from '.......' //引入infinitescroll.vue檔案
    export default {
         data () {
           return {
              Number : 10
           }
         },
         methods : {
           loadData () {
             setTimeout(()=>{
                this.Number = 20
                this.$refs.infinite.$emit('loadDone')
             },1000) 
           }
        }
    }
</script>

在父元件中引入 infinite-scroll 元件

當滑到底部的時候,infinite-scroll 元件元件內部會執行傳入的:on-infinite='loadData’函式 同時在內部也會把 Loading 設定為 true,防止重複執行。

在這裡用this.$refs.infinite拿到infinite-scroll元件的例項,同時觸發事件之前在元件中 $on 已經監聽著的事件,在一秒後進行改變資料,同時發出loadDone事情,告訴元件內部去執行loadDone的監聽回撥,資料已經全部載入完畢,設定this.isDone = true; 一旦isDone或者isLoading一者為true,則一直保持return退出狀態。

$emit 和 $on 必須都在例項上進行觸發和監聽。

v-on 使用自定義繫結事件

第一階段 $emit 和 $on 的兩者之間的關係講完了,接下來該說說 v-on 與 $emit 的關係。

另外,父元件可以在使用子元件的引入模板直接用 v-on 來監聽子元件觸發的事件。

v-on 用接著故事直觀的說法就是,在家裡裝了一個電話,父母隨一直聽著電話,同樣也有一本小冊子,在組成家庭之前,也知識要去監聽那些事。

Warn

不能用 $on 偵聽子元件釋放的事件,而必須在模板裡直接用 v-on 繫結。

上面 Warn 的意思是 e m i t emit和 on只能作用在一一對應的同一個元件例項,而v-on只能作用在父元件引入子元件後的模板上。

就像下面這樣:

就拿官方的這個例子說吧,其實還是很直觀的:

<div id="counter-event-example">
  <p>{{ total }}</p>
  <button-counter v-on:increment="incrementTotal"></button-counter>
  <button-counter v-on:increment="incrementTotal"></button-counter>
</div>
Vue.component('button-counter', {
  template: '<button v-on:click="incrementCounter">{{ counter }}</button>',
  data: function () {
    return {
      counter: 0
    }
  },
  methods: {
    incrementCounter: function () {
      this.counter += 1
      this.$emit('increment')
    }
  },
})
new Vue({
  el: '#counter-event-example',
  data: {
    total: 0
  },
  methods: {
    incrementTotal: function () {
      this.total += 1
    }
  }
})

這樣的好處在哪裡?雖然 Vue 是進行資料單向流的,但是子元件不能直接改變父元件的資料,(也不是完全不能,但不推薦用),標準通用明瞭的用法,則是通過父元件在子元件模板上進行一個 v-on 的繫結監聽事件,同時再寫入監聽後所要執行的回撥。

在counter-event-example父元件裡,聲明瞭兩個button-count的實列,通過 data 用閉包的形式,讓兩者的資料都是單獨享用的,而且 v-on 所監聽的 eventName 都是當前自己實列中的 $emit 觸發的事件,但是回撥都是公用的一個 incrementTotal 函式,因為個例項所觸發後都是執行一種操作!

如果你只是想進行簡單的進行父子元件基礎單個數據進行雙向通訊的話,在模板上通過 v-on 和所在監聽的模板例項上進行 $emit 觸發事件的話,未免有點多餘。通常來說通過 v-on 來進行監聽子元件的觸發事件的話,我們會進行一些多步操作。
子元件

<template>
  <div>
      <p @click='emit'>{{msg}}</p>
  </div>
</template>

<script>
export default {
  name: 'demo',
  data () {
      return {
         msg : '點選後改變資料',
      }
  },
  methods : {
      emit () {
         this.$emit('fromDemo')
      },
  }
}
</script>

父元件

<template>
  <div class="hello">
     <p>hello {{msg}}</p>
     <demo v-on:fromDemo='Fdemo'></demo>
  </div>
</template>
<script>
import Demo from './Demo.vue'
export default {
  name: 'hello',
  data () {
    return {
       msg: '資料將在一秒後改變'
    }

  },
  methods: {
    waitTime() {
      return new Promise(resolve=>{
        setTimeout(()=> {
            this.msg = '資料一秒後改變了'
            resolve(1)
        },1000)
      })
    },
    async Fdemo () {
        let a = await this.waitTime();
        console.log(a)
    }
  },
  components : {
     Demo
  }
}
</script>

從上面 demo 可以看出當子元件觸發了 fromDemo 事件,同時父元件也進行著監聽。

當父元件接收到子元件的事件觸發的時候,執行了 async 的非同步事件,通過一秒鐘的等秒改變 msg,再打印出回撥後通過 promise 返回的值。

接下來想通的此例子告訴大家,這種方法通常是通過監聽子元件的事件,讓父元件去執行一些多步操作,如果我們只是簡單的示意父元件改變傳遞過來的值用此方法就顯的多餘了。

我們進行一些的改動:
子元件

<template>
  <div>
      <p @click='emit'>{{msg}}</p>
  </div>
</template>

<script>
export default {
  name: 'demo',
  props: [ 'msg' ],
  methods : {
      emit () {
         this.$emit('fromDemo','資料改變了')
      },
  }
}
</script>

父元件

<template>
  <div class="hello">
     <demo v-on:fromDemo='Fdemo' :msg='msg'></demo>
  </div>
</template>
<script>
import Demo from './Demo.vue'
export default {
  name: 'hello',
  data () {
    return {
       msg: '資料沒有改變'
    }
  },
  methods: {
    Fdemo (arg) {
      this.msg = arg 
    }
  },
  components : {
     Demo
  }
}
</script>

上面 demo 中子元件從父元件接收一個 msg 資料,但是想點選按鈕的時候,改變父元件的 msg,進行父元件的資料改動,同時再次改變子元件的 msg,但是最簡便的方法則是直接改變 prop 裡 msg 的資料。但是資料驅動都是單向資料流,為了不造成資料傳遞的混亂,我們只能依靠一些其它手段去完成
轉載GitChat