iT邦幫忙

5

[筆記][Vue.js]打開Vue.js世界的大門(12)-讓組件與父元素溝通的$emit和v-model的綁定篇

嗨!讓我抓住禮拜天的尾巴,繼續研究Vue.js吧XD,之後就要分心處理React了,那廢話不多說,分隔線下進入正文!


對父級做事件處理

首先提一下關於組件的事件處理,昨天有在文章中提到官方的一個例子:
HTML

<div id="demoCounter">
    <button-counter></button-counter>
</div>

JavaScript

Vue.component('button-counter',{
    data:()=>{
        return {
            count:0,
        }
    },
    template:`<div>
                  <button @click="count++">Click me</button>
                  <span>目前點了{{count}}下</span>
              </div>`
})

let demoCounter = new Vue({
    el:'#demoCounter',
})

上方在設定組件時我們把事件寫再button@click裡面,讓他被點擊時可以把count加1,但是這僅僅於組件控制組件的資料而已,試著想想當把count的值放到demoCounterdata中時,組件內的事件該怎麼控制count加1呢?如果遇到這種情況可以使用Vue.js中的內建方法$emit傳入事件名字,讓他對父層組件demoCounter觸發用v-on綁定的事件名稱:
HTML

<div id="demoCounter">
    <!--再使用組件時設定組件內部監聽的$emit事件名稱來觸發父組件的事件-->
    <button-counter @add-count="addCount"></button-counter>
    <!--把count從內部組件移到外面-->
    <span>目前點了{{count}}下</span>
</div>

JavaScript

Vue.component('button-counter',{
    template:`<div>
                  <!--在組件內部使用v-on綁定$emit方法,傳入事件名稱-->
                  <button @click="$emit('add-count')">Click me</button>
              </div>`,
})

let counterData = {
    count:0,
}

let counterMethods = {
    //被觸發的事件在這邊
    addCount:()=>{
        counterData.count+=1;
    }
}
    
let demoCounter = new Vue({
    el:'#demoCounter',
    data:counterData,
    methods:counterMethods,
})

https://ithelp.ithome.com.tw/upload/images/20180902/20106935MqfENYO63R.jpg
如上方使用$emit建立讓外層組件監聽的事件,就可以觸發並更改父組件的方法及資料。

然後這裡在使用事件的時候也有需要注意的地方,那就是$emit傳入的方法名稱會全部變成小寫,而且和之前在設定組件名稱時不同,就算在這裡使用駝峰命名法,他在外面v-on的時候也不會幫你轉換成短橫線命名法,也就是說駝峰式命名法在這裡完全無用武之地,以下是例子:
HTML

<!--使用駝峰式命名法讓父組件監聽事件名稱-->
<button @click="$emit('addCount')">Click me</button>

<!--如果像上方$emit設定,以下兩種寫法都不會被觸發-->
<button-counter @add-count="addCount"></button-counter>
<button-counter @add-count="add-count"></button-counter>

<!--他會被轉成小寫的版本,所以只有以下寫法會被觸發-->
<button-counter @add-count="addcount"></button-counter>

------------------分隔線------------------

<!--如果一開始是短橫線命名法-->
<button @click="$emit('add-count')">Click me</button>

<!--不需另外轉換寫法也會被觸發-->
<button-counter @add-count="add-count"></button-counter>

所以說,官方在這裡註明了,不如就使用$emit時只以短橫線命名法處理,當然如果平時就是用短橫線命名法就不需要在意這個了。

另一點是,有時候父組件上監聽的事件會需要一個或以上的參數傳入處理,所以$emit他第一個參數如上面所說是呼叫父組件的事件名稱,這邊也要感謝fysh711426大大留言補充關於參數的部分,如果是由$emit指定事件,並額外傳入參數給父組件的話,$emit會把第二個以後的全部參數都放到arguments這個物件中,也就是說父組件如果要獲取子組件藉由$emit傳的參數,必須使用arguments[0]代表第一個參數、arguments[1]代表第二個參數..等等,我們看下方例子:
HTML

<div id="demoCounter">
    <!--在父組件上用arguments來接收子組件傳的參數-->
    <button-counter @add-count="addCount(arguments)"></button-counter>
    <span>目前點了{{count}}下</span>
</div>

JavaScript

Vue.component('button-counter',{
    template:`<div>
                  <!--在組件內部使用v-on綁定$emit方法,傳入事件名稱外多傳了兩個參數-->
                  <button @click="$emit('add-count','傳入一個參數','傳入第二個參數')">Click me</button>
              </div>`,
})

let counterData = {
    count:0,
}

let counterMethods = {
    addCount:(arrArg)=>{
        //在事件內分別印出arrArg物件,和第一個值還有第二個值
        console.log(arrArg)
        console.log(arrArg[0])
        console.log(arrArg[1])
        counterData.count+=1;
    }
}
    
let demoCounter = new Vue({
    el:'#demoCounter',
    data:counterData,
    methods:counterMethods,
})

結果會如下:
https://ithelp.ithome.com.tw/upload/images/20180904/20106935hOGLRiJSk0.jpg

接著來提好久不見的v-model吧!

在組件上使用v-model

之前的文章中有說過,v-model其實是個語法糖,也就是讓各位大大用起來覺得甜甜的(咦?),先複習一下吧!v-model到底幫我們做了哪些事:

<input v-model="message" />

會等於:

<input :value="message" @input="message = $event.target.value" />

上面的觀念之前的文章中有提過(還不熟的可以看這裡),所以應該不會有太大的問題,但在組件上直接使用v-model是不會有任何作用的,所以當我們知道v-model的原理以後,也可以用相同的方式放到讓v-model在組件上實現。

但是一口氣說完,第一個想法可能是「天啊!這是什麼鬼?」,所以這個部分讓我們大部分解一下過程:

  1. 在組件上設定Props屬性,並用指定value來綁定inputvalue值,有沒有初始值都沒關係,但是為了做到v-modelv-bind:value="value"必須這麼做。
    JavaScript
Vue.component('inputName',{
    //用props屬性來為組件內的DOM綁定資料
    props:['value'],
    template:`<div>
                  <!--這邊的觀念是:value="message"的部分-->
                  <input :value='value' >
              </div>`,
})
  1. 接著要處理v-model幫我們做的第二件事情@input="message = $event.target.value",但是我們v-model綁定的資料在父組件的data中,所以用上面學到的$emit來控制和觸發父組件的事件,而上面也有說過$emit的第一個參數為觸發父組件的事件名稱,那我們的事件名稱是什麼?先回頭看一下HTML的部分:
    HTML
<div id="demoInput">
    <!--在組件裡用v-model綁定父組件data中的nameVal-->
    <input-name v-model="nameVal"></input-name>
    <span>我的名字是:{{nameVal}}</span>
</div>

單就<input-name v-model="nameVal"></input-name>這一行,大家應該可以知道他做了哪些事情吧?他會等於:
HTML

<input-name :value="nameVal" @input="nameVal=$event.target.value"></input-name>

欸嘿嘿!聰明的你發現父組件的事件了嗎?沒錯!就是input,所以$emit要綁定的事件名稱就是input,但是光是呼叫到input還是不夠的,因為父組件的input會用$event.target.value的值去指定給nameVal,所以在事件後方要再多加一個參數,讓父組件的input接收,所以設定第二個參數為input的值:$event.target.value
JavaScript

Vue.component('inputName',{
    props:['value'],
    template:`<div>
                  <!--加上了@input="message = $event.target.value"的觀念,並用$emit來觸發父組件的input事件-->
                  <input :value='value' @input="$emit('input', $event.target.value)">
              </div>`,
})

//這裡是父組件,綁定的資料在data中
let demoInput = new Vue({
    el:'#demoInput',
    data:{
        nameVal:'',
    },
})

最後的程式碼,把所有註解拿掉會如下方這樣子,也完成了在組件中實做v-model,雖然看起來沒有幾行,但是卻用到了之前提到的不少觀念,所以如果有不懂得可以看一下前面的幾篇文章,或是自己練習看看,都會加深一點印象!
HTML

<div id="demoInput">
    <input-name v-model="nameVal"></input-name>
    <span>我的名字是:{{nameVal}}</span>
</div>

JavaScript

Vue.component('inputName',{
    props:['value'],
    template:`<div>
                  <input :value='value' @input="$emit('input', $event.target.value)">
              </div>`,
})

let demoInput = new Vue({
    el:'#demoInput',
    data:{
        nameVal:'',
    },
})

https://ithelp.ithome.com.tw/upload/images/20180903/2010693546jGfJDVwp.jpg
得到的結果就和我們一般使用的<input v-model="nameVal" />一樣對吧!但沒想到加入組件後會變的那麼難懂,不過還是老話一句,熟能生巧,雖然感覺我發文很頻繁,不過每次在寫範例的時候還是會一直偷看之前自己發的文XD。


最後感謝大大們的觀看!如果文章中有任何解釋不清楚或理解錯誤的地方,還請各位大大留言告訴我,另外有任何想法也都歡迎留言和我討論,不管是什麼我都會認真看過的!謝謝大家/images/emoticon/emoticon41.gif


1 則留言

1
fysh711426
iT邦研究生 4 級 ‧ 2018-09-03 23:22:26

組件的 v-model 原理講的好詳細。 /images/emoticon/emoticon37.gif

不過子組件的 props 應該是綁 value 才對,要同屬性名。

Vue.component("inputName",{
  props:['value'],
  template:`
  <input :value="value" :input="$emit('input', $event.target.value)">`

子組件應該是利用 arguments[0] 取得 $emit 傳進來的參數。

<input-name v-model='nameVal'></input-name>
//等於
<input-name :value='nameVal' :input='nameVal=arguments[0]'></input-name>

參考資料
[1] Vue组件中的v-model实现原理分析
[2] v-model原理及自定义组件上的实现

看更多先前的回應...收起先前的回應...

哇!感謝大大分享研究內容!

不過實做v-model的時候,
子組件的props應該不用特別和屬性名相同吧?
因為那只是為了綁定子組件自身的資料,讓他可以綁定v-model
我剛剛試著把props改成data也是能達到一樣的效果:

Vue.component('inputName',{
    data:()=>{
        return {
            val:""
        }
    },
    template:`<div>
                  <input :value='val' @input="$emit('input', $event.target.value)">
              </div>`,
})

有錯再麻煩大大告訴我/images/emoticon/emoticon16.gif

另外$emit傳的參數是用arguments[0]去接這我真的疏忽了!/images/emoticon/emoticon26.gif我會再把他補進文章中,很感謝大大還特別去找資料!

fysh711426 iT邦研究生 4 級‧ 2018-09-04 00:12:42 檢舉

您試試看將 nameVal:'abc' 給預設值看看,觀察 input 有沒有接到值,
props 是用來接收外層 attr 傳入的值,一般 attr 可以自訂,不過 v-model 預設的 attr 剛好是 value。

<input-name :value='nameVal' :input='nameVal=arguments[0]'>

我們如果自訂一個,應該也是可以。

<input-name v-model='nameVal' :val='nameVal'>

換成 data 就變成內部變數了,就不能由外部給子組件預設值。

哈哈,對的!
其實如果v-bind是綁定props內的值,那就可以由外部去操控組件內的預設值,這點valueval應該都可以做到。

不過換個角度想如果用data代替props的話,預設值只需要直接指定給data內的val就可以了吧!

我也還沒在實務上處理過,不知道哪種方法是目前較常用的寫法/images/emoticon/emoticon13.gif,但是直接使用value,就能夠讓畫面看起來簡潔一點,畢竟v-model都幫我們處理:value了,其實官方文件也是用value,只是我天生反骨想說把他改掉看看/images/emoticon/emoticon70.gif

感謝大大耐心說明!

fysh711426 iT邦研究生 4 級‧ 2018-09-04 12:51:30 檢舉

預設值通常需要由外部傳入,例如資料需要用 ajax 取得,因此 props 是比較適合的。

  • props 像組件對外的接口。
  • data 則是組件內部自己的狀態。

外部並不能直接改變組件內 data 的值,只能間接的透過 props 將資料傳入,
且 props 是唯讀的,所以組件內也不能通過修改 props 影響外部。

這樣的好處是組件的作用域獨立,且資料流是單向的,只會從根節點傳向子節點,這是 Vue 相比 ng1 改進的地方。

我也沒有實務上用過。
/images/emoticon/emoticon16.gif

我才是完全沒有用過前端框架的人,很感謝大大分享經驗和討論,不過就像大大說的,讓資料的來源都屬於單向會比較好,不會有的放在props有些放在data,感覺也會很亂而且不好維護。

主要是模組化的開發模式現在還不熟,這也是我要學習的地方/images/emoticon/emoticon20.gif

之後在這個系列的最後我會做一個todolist,現在對這些屬性還是很模糊,因為真得很方便XD,不過先試著把官方文件讀完,到時候說不定會有恍然大悟的感覺/images/emoticon/emoticon13.gif

大大我有把參數的部分更新上去了!
另外我還是把v-model的部分改為和官方的例子一樣,避免誤會/images/emoticon/emoticon16.gif

fysh711426 iT邦研究生 4 級‧ 2018-09-04 21:47:23 檢舉

這次竟然沒有繼續反骨。 /images/emoticon/emoticon01.gif

是指接收參數的arguments部分嗎?
其實我也有嘗試改成其他單字,但不能用/images/emoticon/emoticon25.gif

fysh711426 iT邦研究生 4 級‧ 2018-09-04 23:35:04 檢舉

改為和官方的例子一樣,避免誤會

這句話,哈哈哈

我學乖了啊/images/emoticon/emoticon02.gif

我要留言

立即登入留言