本文主要歸納在 Vuejs 學習過程中對於 Vuejs 組件的各個相關要點。由於本人水平有限,如文中出現錯誤請多多包涵並指正,感謝。如果需要看更清晰的代碼高亮,請跳轉至我的個人站點的 深入理解 Vuejs 組件 查看本文。
組件使用細節
is屬性
我們通常使用 is
屬性解決模板標簽 bug 的問題。下面我們通過一個 table
標簽的 bug 案例進行說明。
我們先寫一個簡單的 Vue 實例,並創造一個 row
的組件,將它的模板 template
置為 '<tr><td>this is a row</td></tr>'
,按照下面的示例進行放置。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>is屬性</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script> </head> <body> <div id="app"> <table> <tbody> <row></row> <row></row> <row></row> </tbody> </table> </div> <script> Vue.component('row',{ template: '<tr><td>this is a row</td></tr>' }) var vm = new Vue({ el: "#app" }) </script> </body> </html>
該示例中,由於 H5 的規范 table
標簽下 tbody
下只能是 tr
,所以瀏覽器在渲染的時候出了問題。可以看到組件row
渲染的 this is a row 都跑到了 table
之外。

解決這個問題的方法就是,我們按照規范在 tbody
之下使用 tr
。但我們用 is=
將 tr
變成 row
組件。
<div id="app"> <table> <tbody> <tr is="row"></tr> <tr is="row"></tr> <tr is="row"></tr> </tbody> </table> </div>
這樣我們在遵循規范的同時,也使用了 Vuejs 的組件模板。可以看到接下來的瀏覽器 DOM 渲染已經正常。

在使用
ul
時,同樣建議使用li
與is=
,而不是直接使用組件模板。
在使用select
時,同樣建議使用option
與is=
,而不是直接使用組件模板。
子組件 data
我們還是通過上面這個已有的案例進行演示。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script> </head> <body> <div id="app"> <table> <tbody> <tr is="row"></tr> <tr is="row"></tr> <tr is="row"></tr> </tbody> </table> </div> <script> Vue.component('row',{ data: { content: 'this is a row' }, template: '<tr><td>{{content}}</td></tr>' }) var vm = new Vue({ el: "#app" }) </script> </body> </html>
經過之前的修改,我們將 tr
標簽的 bug 解決掉了,並進行了修正。我們想在 Vue.component
的子組件中,添加數據 data
,並在模板 template
中使用插值表達式使用該數據內容。但這種寫法打開瀏覽器是沒有任何顯示的。
因為在子組件中定義 data
時,data
必須是一個函數 function
,return
值為一個對象。而不能直接是一個對象。因為子組件不像根組件,只會被調用一次,可能在不同的地方被調用多次。所以通過函數 function
來讓每一個子組件都有獨立的數據存儲,就不會出現多個子組件相互影響的情況。
即在子組件中正確的寫法應該是:
Vue.component('row',{ data: function(){ return { content: 'this is a row' } }, template: '<tr><td>{{content}}</td></tr>' })
ref 引用
在 Vuejs 中,使用 ref
引用的方式,可以找到相關的 DOM 元素。
在 div
標簽中添加一個 ref="hello"
,標記這個標簽的引用名為 hello
。並給他綁定一個事件,在點擊它之后,輸出出這個引用節點的 innerText
<body> <div id="app"> <div ref="hello" @click="handleClick">hello world</div> </div> <script> var vm = new Vue({ el: "#app", methods: { handleClick: function(){ alert(this.$refs.hello.innerText) } } }) </script> </body>
而當在一個組件中去設置 ref
,然后通過 this.$refs.name
獲取 ref
里面的內容時,這個時候獲取到的內容是子組件內容的引用。
參考下面的計數器求和案例
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>計數器求和</title> <script src="./vue.js"></script> </head> <body> <div id="app"> <counter ref="one" @change="handleChange"></counter> <counter ref="two" @change="handleChange"></counter> <div>{{total}}</div> </div> <script> Vue.component('counter',{ template: '<div @click="handleClick">{{number}}</div>', data: function(){ return { number: 0 } }, methods: { handleClick: function(){ this.number ++ this.$emit('change') } } }) var vm = new Vue({ el: "#app", data: { total: 0 }, methods: { handleChange: function(){ this.total = this.$refs.one.number + this.$refs.two.number } } }) </script> </body> </html>
在子組件中,綁定了 handleClick
事件使其每次點擊后自增1,並且發布 $emit
將 change
傳遞給父組件,在組件中監聽 @change
,執行 handleChange
事件,在父組件 methods
中設置 handleChange
事件,並使用 this.$refs.one.number
來獲取子組件內容的引用。
父子組件的數據傳遞
Vue 中的單向數據流:即子組件只能使用父組件傳遞過來的數據,不能修改這些數據。因為這些數據很可能在其他地方被其他組件進行使用。
所以當子組件在收到父組件傳遞過來的數據,並在后續可能要對這些數據進行修改時。可以先將 props
里面獲取到的數據,在子組件自己的 data
的 return
中使用一個 number
進行復制,並在后續修改這個 number
即可。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>父子組件傳值</title> <script src="./vue.js"></script> </head> <body> <div id="app"> <counter :count="0"></counter> <counter :count="1"></counter> </div> <script> var counter = { props: ['count'], //在 data 的 return 中復制一份 父組件傳遞過來的值 data: function(){ return { number: this.count } }, template: '<div @click="handleClick">{{number}}</div>', methods: { handleClick: function(){ // 在子組件中不修改父組件傳遞過來的 count,而是修改自己 data 中的 number this.number ++ } } } var vm = new Vue({ el: "#app", components: { counter: counter } }) </script> </body> </html>
傳值總結
- 父組件通過屬性的形式向子組件進行傳值
- 子組件通過事件觸發的形式向父組件傳值
- 父子組件傳值時,有單向數據流的規定。父組件可以向子組件傳遞任何的數據,但子組件不能修改父組件傳遞過來的數據。如果一定要進行修改,只能通過修改復制副本的方式進行。
組件參數校驗 和 非props特性
組件參數校驗
當子組件接收父組件數據類型要進行參數校驗時,是可以通過組件參數校驗來定義規則的。例如限制子組件接收父組件傳遞數據的類型,此時props
后不再使用數組,而是使用對象。
- 當傳遞過來的值只接收數字類型時

- 當傳遞過來的值只接收字符串類型時

- 當傳遞過來的值既可以接收字符串類型、也可以接收數字類型時

當做了組件參數校驗,在傳遞過程中如果傳遞了組件參數校驗規定之外的類型,就會報錯(這里我們傳遞的是一個 Object
)。

自定義校驗器
props
中的 content
之后也可以寫成對象形式,設置更多的參數。
Vue.component('child',{ props: { content: { type: String, required: false, //設置是否必須傳遞 default: 'default value' //設置默認傳遞值 -- 無傳遞時傳遞的默認值 } }, template: '<div>{{content}}</div>' })
type
設置傳遞類型;required
設置是否必須傳遞,false
為非必須傳遞;default
設置默認傳遞值,在無傳遞時,傳遞該值。

當父組件調用子組件傳遞了 content
時,默認值便不會生效。

除此之外,還可以限制傳遞字符串的長度等等。借助 validator
Vue.component('child',{ props: { content: { type: String, //對傳入屬性通過校驗器要求它的長度必須大於5 validator: function(value){ return value.length > 5 } } }, template: '<div>{{content}}</div>' })
在這個案例中,傳入的屬性長度必須超過 5 ,如果沒有就會出現報錯。

prop 特性 與 非 props 特性
prop 特性
props
特性: 當父組件使用子組件時,通過屬性向子組件傳值,恰好子組件也聲明了對父組件傳遞過來的屬性的接收。即當父組件調用子組件時,傳遞了 content
;子組件在 props
里面也聲明了 content
。所以父子組件有一個對應的關系。這種形式的屬性,就稱之為 props
特性。
prop
特性特點:
- 屬性的傳遞,不會在 DOM 標簽進行顯示
- 當父組件傳遞給子組件之后,子組件可以直接通過插值表達式或者通過
this.content
取得內容。
非 props 特性
非 props
特性:父組件向子組件傳遞了一個屬性,子組件並沒有 props
的內容,即子組件並沒有聲明要接收父組件傳遞過來的內容。

非 prop
特性特點:
- 無法獲取父組件內容,因為沒有聲明
- 屬性會展示在子組件最外層的 DOM 標簽的 HTML 屬性里。

原生事件
在下面示例中,代碼這么書寫在點擊 Child
的時候,事件是不會被觸發的。因為這個 @click
事件實際上是綁定的一個自定義的事件。但真正的鼠標點擊 click
事件並不是綁定的這個事件。
<body> <div id="app"> <child @click="handleClick"></child> </div> <script> Vue.component('child', { template: '<div>Child</div>' }) var vm = new Vue({ el: "#app", methods: { handleClick: function(){ alert('click') } } }) </script> </body>
如果我們想要觸發這個自定義的 click
事件,應該把 @click
寫到子組件的 template
中的 div
元素上。我們將代碼改寫成下面的樣子,點擊 Chlid
彈出 chlidClick
。因為在 div
元素上綁定的事件是原生的事件,而之前在 child
上綁定的事件是監聽的一個自定義事件。
<body> <div id="app"> <child @click="handleClick"></child> </div> <script> Vue.component('child', { template: '<div @click="handleChildClick">Child</div>', methods: { handleChildClick: function(){ alert('chlidClick') } } }) var vm = new Vue({ el: "#app", methods: { handleClick: function(){ alert('click') } } }) </script> </body>
而自定義事件,只有通過 this.$emit
去觸發。
<body> <div id="app"> <child @click="handleClick"></child> </div> <script> Vue.component('child', { template: '<div @click="handleChildClick">Child</div>', methods: { handleChildClick: function(){ alert('chlidClick') this.$emit('click') } } }) var vm = new Vue({ el: "#app", methods: { handleClick: function(){ alert('click') } } }) </script> </body>
組件監聽內部原生事件
通常使用在 @click
之后加上 .native
修飾符達到直接在組件上監聽原生事件的效果。
<body> <div id="app"> <child @click.native="handleClick"></child> </div> <script> Vue.component('child', { template: '<div>Child</div>' }) var vm = new Vue({ el: "#app", methods: { handleClick: function(){ alert('click') } } }) </script> </body>
非父子組件間傳值
將左側的網頁用右側的圖進行表示。即細分組件之后,再進行二次細分。

當出現以下情況,第二層的一個組件要跟第一層的大組件進行通信。這個時候就是父子組件的傳值。即父組件通過 props
向子組件傳值,子組件通過事件觸發向父組件傳值。

當第三層的組件要和第一層的大組件進行通信。甚至兩個不同二層組件下的三層組件要進行通信時。應該采用什么方法呢?
這時顯然就不能使用逐層傳遞的方式了。因為這樣的操作會使得代碼非常的復雜。

既然不是父子組件之間傳值,說明這兩個組件之間不存在父子關系。如之前提到的第三層的組件要向第一層的大組件進行傳值,兩個不同二層組件下的三層組件要進行傳值。這些都是非父子組件傳值。
解決方案
一般有兩種方式來解決 Vue 里面復雜的非父子組件之間傳值的問題。
- 一種是借助 Vue 官方提供的一種數據層的框架 Vuex。
- 另一種是使用 發布 / 訂閱 模式來解決非父子組件之間傳值的問題,也被稱之為 總線機制。
下面着重講解如何使用 總線機制 解決非父子組件之間傳值的問題。
Bus / 總線 / 發布訂閱模式 / 觀察者模式
通過一個案例來實現該模式,當點擊 even
時,yao
變為 even
。當點擊 yao
時,even
變為 yao
。
首先 new
一個 Vue 的實例,將其賦值給 Vue.prototype.bus
。即給 Vue.prototype
上掛載了一個名為 bus
的屬性。這個屬性,指向 Vue 的實例。只要在之后,調用 new Vue()
或者創建組件的時候,每一個組件上都會擁有 bus
這個屬性。都指向同一個 Vue 的實例。
通過 this.bus.$emit
向外觸發事件,再借助生命周期鈎子 mounted
通過 this.bus.$on
監聽事件。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>非父子組件間傳值 Bus/總線/發布訂閱模式/觀察者模式)</title> <script src="./vue.js"></script> </head> <body> <div id="app"> <child content="even"></child> <child content="yao"></child> </div> <script> // new 一個 Vue 的實例,將其賦值給 Vue.prototype.bus Vue.prototype.bus = new Vue() Vue.component('child',{ data: function(){ return { //因為不能改變傳遞過來的值 所以復制一份 selfContent: this.content } }, props: { content: String }, template: '<div @click="handleClick">{{selfContent}}</div>', methods: { handleClick: function(){ //實例上掛載的bus,通過 $emit 方法向外觸發事件 this.bus.$emit('change',this.selfContent) } }, //借助生命周期鈎子 通過 $on 方法 監聽 change 事件 mounted: function(){ var _this = this this.bus.$on('change',function(msg){ _this.selfContent = msg }) } }) var vm = new Vue({ el: "#app" }) </script> </body> </html>
插槽 slot
子組件除了展示 p
標簽中的 hello 之外,還需要展示一塊內容,而這部分內容不是子組件決定的,而是父組件傳遞過來的。如果使用 v-html
搭配 content
屬性傳遞值,會出現外部必須包裹 div
的問題。這個場景就應該使用插槽 slot
。
在父組件使用 child
的時候,在標簽內部,使用 h1
標簽,並寫入 yao 。這樣就可以了。
<div id="app"> <child> <h1>yao</h1> </child> </div>
在子組件的模板里,使用 <slot></slot>
就可以使之前寫入的 yao 顯示出來了。
Vue.component('child',{
template: '<div><p>hello</p><slot></slot></div>' }) var vm = new Vue({ el: "#app" })
除此之外, <slot></slot>
之前還可以添加默認內容,即 <slot>默認內容</slot>
。添加默認內容的時候,如果在父組件使用子組件時,不傳遞插槽內容的話,就會顯示默認內容,如果父組件使用子組件時,傳遞了插槽內容的話,就會顯示傳遞的插槽內容,而不會顯示默認內容。
具名插槽
當我有多個 slot
插槽需要進行填充的時候,可以使用具名插槽,即給插槽命名。例如下列示例中的 header
和 footer
都是由外部傳遞的情況。
在父組件使用子組件的過程中,給插槽添加 slot=""
屬性,對應之后的插槽命名。
<div id="app"> <body-content> <header slot="header">header</header> <footer slot="footer">footer</footer> </body-content> </div>
在 slot
中使用 name=""
給插槽命名。
Vue.component('body-content',{
template: `<div> <slot name="header"></slot> <div class="content">content</div> <slot name="footer"></slot> </div>` }) var vm = new Vue({ el: "#app" })
具名插槽同樣可以擁有默認值。
作用域插槽
當子組件做循環,或者某一部分的 DOM 結構是由外部傳遞進來時,使用作用域插槽。
作用域插槽必須是 template
開頭和結尾的內容,同時這個插槽聲明從子組件接收的數據都放在 props
里面,然后通過相應的模板對子組件進行展示。
<div id="app"> <child> <template slot-scope="props"> <li>{{props.item}} - hello</li> </template> </child> </div>
Vue.component('child',{ data: function(){ return { list: [1,2,3,4] } }, template: `<div> <ul> <slot v-for="item of list" :item=item ></slot> </ul> </div>` }) var vm = new Vue({ el: "#app" })
動態組件 與 v-once 指令
下面代碼可以實現點擊 button
按鈕的切換效果,除了這種方式之外,還可以使用動態組件的方式實現。
<body> <div id="app"> <child-one v-if="type === 'child-one'"></child-one> <child-two v-if="type === 'child-two'"></child-two> <button @click="handleBtnClick">change</button> </div> <script> Vue.component('child-one',{ template: '<div>child-one</div>' }) Vue.component('child-two',{ template: '<div>child-two</div>' }) var vm = new Vue({ el: "#app", data: { type: 'child-one' }, methods: { handleBtnClick: function(){ this.type = this.type === 'child-one' ? 'child-two' : 'child-one' } } }) </script> </body>
動態組件
使用 component
標簽,並使用 :is
綁定數據。即可以實現上面示例中相同的效果。
即根據 :is
對應值的變化,自動的加載不同的組件。
<div id="app"> <component :is="type"></component> <!-- <child-one v-if="type === 'child-one'"></child-one> <child-two v-if="type === 'child-two'"></child-two> --> <button @click="handleBtnClick">change</button> </div>
v-once 指令
在 Vue 中通過 v-once
指令可以提高靜態內容的展示效率。例如上面的示例中,當我們不使用動態組件而使用下面的方式進行組件調用的時候。每次點擊 button
,都會摧毀當前組件,然后創建一個新的組件。
<div id="app"> <child-one v-if="type === 'child-one'"></child-one> <child-two v-if="type === 'child-two'"></child-two> <button @click="handleBtnClick">change</button> </div>
Vue.component('child-one',{ template: '<div>child-one</div>' }) Vue.component('child-two',{ template: '<div>child-two</div>' })
如果我們在這兩個組件模板中加上 v-once
指令。在 child-one
和 child-two
第一次渲染的時候,就會被放入內存之中。當進行切換的時候,就並不需要重新創建一個組件了,而是從內存中去拿出以前的組件,所以性能更高。
Vue.component('child-one',{ template: '<div v-once>child-one</div>' }) Vue.component('child-two',{ template: '<div v-once>child-two</div>' })