1. 程式人生 > >深入理解 Vuejs 組件

深入理解 Vuejs 組件

self 節點 keyword alert 形式 http 是否 顯示 ans

本文主要歸納在 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 時,同樣建議使用 liis=,而不是直接使用組件模板。
在使用 select 時,同樣建議使用 optionis=,而不是直接使用組件模板。

子組件 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,並且發布 $emitchange 傳遞給父組件,在組件中監聽 @change ,執行 handleChange 事件,在父組件 methods 中設置 handleChange 事件,並使用 this.$refs.one.number 來獲取子組件內容的引用。

父子組件的數據傳遞

Vue 中的單向數據流:即子組件只能使用父組件傳遞過來的數據,不能修改這些數據。因為這些數據很可能在其他地方被其他組件進行使用。

所以當子組件在收到父組件傳遞過來的數據,並在後續可能要對這些數據進行修改時。可以先將 props 裏面獲取到的數據,在子組件自己的 datareturn 中使用一個 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 插槽需要進行填充的時候,可以使用具名插槽,即給插槽命名。例如下列示例中的 headerfooter 都是由外部傳遞的情況。

在父組件使用子組件的過程中,給插槽添加 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-onechild-two 第一次渲染的時候,就會被放入內存之中。當進行切換的時候,就並不需要重新創建一個組件了,而是從內存中去拿出以前的組件,所以性能更高。

    Vue.component(‘child-one‘,{
      template: ‘<div v-once>child-one</div>‘
    })

    Vue.component(‘child-two‘,{
      template: ‘<div v-once>child-two</div>‘
    })

深入理解 Vuejs 組件