本文主要歸納在 Vuejs 學習過程中對於 Vuejs 組件的各個相關要點。由於本人水平有限,如文中出現錯誤請多多包涵並指正,感謝。如果需要看更清晰的代碼高亮,請跳轉至我的個人站點的 深入理解 Vuejs 組件 查看本文。 組件使用細節 is屬性 我們通常使用 is 屬性解決模板標簽 bug 的問題 ...
本文主要歸納在 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>'
})