深入理解 Vuejs 組件


本文主要歸納在 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> 

JSbin 預覽

該示例中,由於 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> 

JSbin 預覽

這樣我們在遵循規范的同時,也使用了 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 必須是一個函數 functionreturn 值為一個對象。而不能直接是一個對象。因為子組件不像根組件,只會被調用一次,可能在不同的地方被調用多次。所以通過函數 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> 

JSbin 預覽

在子組件中,綁定了 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> 

JSbin 預覽

傳值總結

  • 父組件通過屬性的形式向子組件進行傳值
  • 子組件通過事件觸發的形式向父組件傳值
  • 父子組件傳值時,有單向數據流的規定。父組件可以向子組件傳遞任何的數據,但子組件不能修改父組件傳遞過來的數據。如果一定要進行修改,只能通過修改復制副本的方式進行。

組件參數校驗 和 非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> 

JSbin 預覽

插槽 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" }) 

JSbin 預覽

除此之外, <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" }) 

JSbin 預覽

具名插槽同樣可以擁有默認值。

作用域插槽

當子組件做循環,或者某一部分的 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" }) 

JSbin 預覽

動態組件 與 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> 

JSbin 預覽

動態組件

使用 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> 

JSbin 預覽

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>' })


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM