深入理解Vue中的組件

2019-06-20更新:html

Vue2.6已經更新了關於內容插槽和做用域插槽的API和用法,爲了避免誤導你們,我把插槽的內容刪除了。詳情請看官網vue

2018-07-19更新:數組

  1. 更新做用域插槽的屬性: scope -> slot-scope;
  2. 添加了對象解構。

今天看了下Vue官網上關於組件的教程,感受內容還挺多,如今把組件中基本的知識梳理一下。瀏覽器


組件的基本使用

註冊組件

註冊組件就是利用Vue.component()方法,先傳入一個自定義組件的名字,而後傳入這個組件的配置。緩存

Vue.component('mycomponent',{
    template: `<div>這是一個自定義組件</div>`,
    data () {
      return {
        message: 'hello world'
      }
    }
  })

如上方式,就已經建立了一個自定義組件,而後就能夠在Vue實例掛在的DOM元素中使用它。app

<div id="app">
    <mycomponent></mycomponent>
    <my-component></my-component>
</div>
<script>
  var app = new Vue({
    el: '#app',
    data: {
    },
    components: {
      'my-component': {
        template: `<div>這是一個局部的自定義組件,只能在當前Vue實例中使用</div>`,
      }
    }
  })
</script>

直接使用Vue.component()建立的組件,全部的Vue實例均可以使用。還能夠在某個Vue實例中註冊只有本身能使用的組件。ide

var app = new Vue({
    el: '#app',
    data: {
    },
    components: {
      'my-component': {
        template: `<div>這是一個局部的自定義組件,只能在當前Vue實例中使用</div>`,
      }
    }
  })

模板的要求

注意:組件的模板只能有一個根元素。下面的狀況是不容許的。函數

template: `<div>這是一個局部的自定義組件,只能在當前Vue實例中使用</div>
            <button>hello</button>`,

組件中的data必須是函數

能夠看出,註冊組件時傳入的配置和建立Vue實例差很少,但也有不一樣,其中一個就是data屬性必須是一個函數。
這是由於若是像Vue實例那樣,傳入一個對象,因爲JS中對象類型的變量實際上保存的是對象的引用,因此當存在多個這樣的組件時,會共享數據,致使一個組件中數據的改變會引發其餘組件數據的改變。post

而使用一個返回對象的函數,每次使用組件都會建立一個新的對象,這樣就不會出現共享數據的問題來了。ui

關於DOM模板的解析

當使用 DOM 做爲模版時 (例如,將 el 選項掛載到一個已存在的元素上), 你會受到 HTML 的一些限制,由於 Vue 只有在瀏覽器解析和標準化 HTML 後才能獲取模板內容。尤爲像這些元素 <ul><ol><table><select> 限制了能被它包裹的元素,而一些像 <option> 這樣的元素只能出如今某些其它元素內部。

在自定義組件中使用這些受限制的元素時會致使一些問題,例如:

<table>
  <my-row>...</my-row>
</table>

自定義組件 <my-row> 被認爲是無效的內容,所以在渲染的時候會致使錯誤。這時應使用特殊的 is 屬性:

<table>
  <tr is="my-row"></tr>
</table>

也就是說,標準HTML中,一些元素中只能放置特定的子元素,另外一些元素只能存在於特定的父元素中。好比table中不能放置divtr的父元素不能div等。因此,當使用自定義標籤時,標籤名仍是那些標籤的名字,可是能夠在標籤的is屬性中填寫自定義組件的名字。

應當注意,若是您使用來自如下來源之一的字符串模板,這些限制將不適用:

  • <script type="text/x-template">
  • JavaScript 內聯模版字符串
  • .vue 組件

其中,前兩個模板都不是Vue官方推薦的,因此通常狀況下,只有單文件組件.vue能夠忽略這種狀況。

組件的屬性和事件

在html中使用元素,會有一些屬性,如class,id,還能夠綁定事件,自定義組件也是能夠的。當在一個組件中,使用了其餘自定義組件時,就會利用子組件的屬性事件來和父組件進行數據交流。
<img src="/assets/images/props-events.png"/>

如上如所示,父子組件之間的通訊就是 props down,events up,父組件經過 屬性props向下傳遞數據給子組件,子組件經過 事件events 給父組件發送消息。
好比,子組件須要某個數據,就在內部定義一個prop屬性,而後父組件就像給html元素指定特性值同樣,把本身的data屬性傳遞給子組件的這個屬性。
而當子組件內部發生了什麼事情的時候,就經過自定義事件來把這個事情涉及到的數據暴露出來,供父組件處理。

<my-component v-bind:foo="baz" v-on:event-a="doThis(arg1,...arg2)"></my-component>

如上代碼,

  • foo<my-component>組件內部定義的一個prop屬性,baz是父組件的一個data屬性,
  • event-a是子組件定義的一個事件,doThis是父組件的一個方法

過程就是這樣:

  • 父組件把baz數據經過prop傳遞給子組件的foo
  • 子組件內部獲得foo的值,就能夠進行相應的操做;
  • 當子組件內部發生了一些變化,但願父組件能知道時,就利用代碼觸發event-a事件,把一些數據發送出去
  • 父組件把這個事件處理器綁定爲doThis方法,子組件發送的數據,就做爲doThis方法的參數被傳進來
  • 而後父組件就能夠根據這些數據,進行相應的操做

屬性Props

Vue組件經過props屬性來聲明一個本身的屬性,而後父組件就能夠往裏面傳遞數據。

Vue.component('mycomponent',{
    template: '<div>這是一個自定義組件,父組件傳給個人內容是:{{myMessage}}</div>',
    props: ['myMessage'],
    data () {
      return {
        message: 'hello world'
      }
    }
  })

而後調用該組件

<div id="app">
    <mycomponent my-message="hello"></mycomponent>
</div>

注意,因爲HTML特性是不區分大小寫的,因此傳遞屬性值時,myMessage應該轉換成 kebab-case (短橫線隔開式)my-message="hello"

v-bind綁定屬性值

這裏說一下v-bind綁定屬性值的一個特性:通常狀況下,使用v-bind給元素特性(attribute)傳遞值時,Vue會將""中的內容當作一個表達式。
好比:

<div attr="message">hello</div>

上面這樣,div元素的attr特性值就是message

而這樣

<div v-bind:attr="message">hello</div>

這裏的message應該是Vue實例的data的一個屬性,這樣div元素的attr特性值就是message這個屬性的值。

之因此說是通常狀況,是由於classstyle特性並非這樣。用v-bind:classclass傳入正常的類名,效果是同樣的,由於對於這兩個特性,Vue採用了合併而不是替換的原則。

動態綁定特性值

根據上面,想要把父組件的屬性綁定到子組件,應該使用v-bind,這樣,父組件中數據改變時能反映到子組件。
注意,根據父組件傳遞給子組件的屬性類型的不一樣,當在子組件中更改這個屬性時,會有如下兩種狀況:

  • 當父組件傳遞的屬性是引用類型時,在子組件中更改相應的屬性會致使父組件相應屬性的更改。

    <div id="app2">
         <div>這是父組件的parentArray:{{parentArray}}</div>
         <my-component :child-array="parentArray"></my-component>
       </div>
       <script>
         Vue.component('my-component', {
           template: `
           <div>這是接收了父組件傳遞值的子組件的childArray: {{childArray}} <br>
               <button type="button" @click="changeArray">
               點擊我改變父元素的parentArray</button>
             </div>`,
           props: ['childArray'],
           data () {
             return {
               counter: 1
             }
           },
           methods: {
             changeArray () {
               this.childArray.push(this.counter++)
             }
           }
         })
         new Vue({
           el: '#app2',
           data: {
             parentArray: []
           }
         })
       </script>
  • 當父組件傳遞值爲基本類型時,在子組件中更改這個屬性會報錯。正確的作法是,在父組件中綁定屬性值時,加上.sync修飾符。

    <my-component :child-array.sync="parentArray"></my-component>

    而後在子組件中改變相應的屬性

    methods: {
         changeArray () {
           this.counter++
           this.$emit('update:childArray', this.counter)
         }
       }

子組件但願對傳入的prop進行操做

通常來講,是不建議在子組件中對父組件中傳遞來的屬性進行操做的。若是真的有這種需求,能夠這樣:

  1. 父組件傳遞了一個基本類型值,那麼能夠在子組件中建立一個新的屬性,並以傳遞進來的值進行初始化,以後就能夠操做這個新的屬性了

    props: ['initialCounter'],
    data: function () {
      return { counter: this.initialCounter }
    }
  2. 父組件傳遞了一個引用類型值,爲了不更改父組件中相應的數據,最好是對引用類型進行復制。複雜的狀況,確定應該是深複製。

給子組件傳遞正確類型的值

一樣是上面的緣由,靜態的給子組件的特性傳遞值,它都會把他當作一個字符串。

<!-- 傳遞了一個字符串 "1" -->
<comp some-prop="1"></comp>

子組件中,特性的值是字符串 "1" 而不是 number 1。若是想傳遞正確的數值,應該使用v-bind傳遞,這樣就能把傳遞的值當作一個表達式來處理,而不是字符串。

<!-- 傳遞實際的 number 1 -->
<comp v-bind:some-prop="1"></comp>

Prop驗證

咱們能夠給組件的props屬性添加驗證,當傳入的數據不符合要求時,Vue會發出警告。

Vue.component('example', {
  props: {
    // 基礎類型檢測 (`null` 意思是任何類型均可以)
    propA: Number,
    // 多種類型
    propB: [String, Number],
    // 必傳且是字符串
    propC: {
      type: String,
      required: true
    },
    // 數字,有默認值
    propD: {
      type: Number,
      default: 100
    },
    // 數組/對象的默認值應當由一個工廠函數返回
    propE: {
      type: Object,
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定義驗證函數
    propF: {
      validator: function (value) {
        return value > 10
      }
    }
  }
})

type 能夠是下面原生構造器:

  • String
  • Number
  • Boolean
  • Function
  • Object
  • Array
  • Symbol

type 也能夠是一個自定義構造器函數,使用 instanceof 檢測。

// 自定義Person構造器
 function Person(name, age) {
    this.name = name
    this.age = age
  }
  Vue.component('my-component', {
    template: `<div>名字: {{ person-prop.name }}, 年齡: {{ person-prop.age }} </div>`,
    props: {
      person-prop: {
        type: Person     // 指定類型
      }
    }
  })
  new Vue({
    el: '#app2',
    data: {
      person: 2        // 傳入Number類型會報錯
    }
  })

非Prop類型的屬性

也能夠像在html標籤中添加data-開頭的自定義屬性同樣,給自定義組件添加任意的屬性。而不只限於data-*形式,這樣作的話,Vue會把這個屬性放在自定義組件的根元素上。一個自定義組件的模板只能有一個根元素

覆蓋非Prop屬性

若是父組件向子組件的非prop屬性傳遞了值,那麼這個值會覆蓋子組件模板中的特性。

<div id="app3">
    <my-component2 att="helloParent"></my-component2>
</div>
<script>
  Vue.component('my-component2', {
    template: `<div att="helloChild">子組件原有的特性被覆蓋了</div>`
  })
  new Vue({
    el: '#app3'
  })
</script>

上面渲染的結果是,divatt屬性是helloParent
注意:前面已經提到過,覆蓋原則對於classstyle不適用,而是採用了合併(merge)的原則。

<div id="app3">
    <my-component2 att="helloParent" class="class2" style="color: red;"></my-component2>
</div>
<script>
  Vue.component('my-component2', {
    template: `<div att="helloChild" class="class1" style="background: yellow;">子組件原有的特性被覆蓋了</div>`
  })
  new Vue({
    el: '#app3'
  })
</script>

上面的渲染結果是,div的類名是class1 class2,行內樣式是color:red; background:yellow;

自定義事件

經過prop屬性,父組件能夠向子組件傳遞數據,而子組件的自定義事件就是用來將內部的數據報告給父組件的。

<div id="app3">
    <my-component2 v-on:myclick="onClick"></my-component2>
</div>
<script>
  Vue.component('my-component2', {
    template: `<div>
    <button type="button" @click="childClick">點擊我觸發自定義事件</button>
    </div>`,
    methods: {
      childClick () {
        this.$emit('myclick', '這是我暴露出去的數據', '這是我暴露出去的數據2')
      }
    }
  })
  new Vue({
    el: '#app3',
    methods: {
      onClick () {
        console.log(arguments)
      }
    }
  })
</script>

如上所示,共分爲如下步驟:

  1. 子組件在本身的方法中將自定義事件以及須要發出的數據經過如下代碼發送出去

    this.$emit('myclick', '這是我暴露出去的數據', '這是我暴露出去的數據2')
    • 第一個參數是自定義事件的名字
    • 後面的參數是依次想要發送出去的數據
  2. 父組件利用v-on爲事件綁定處理器

    <my-component2 v-on:myclick="onClick"></my-component2>

    這樣,在Vue實例的methods方法中就能夠調用傳進來的參數了

注意: 在使用v-on綁定事件處理方法時,不該該傳進任何參數,而是直接寫v-on:myclick="onClick",否則,子組件暴露出來的數據就沒法獲取到了

綁定原生事件

若是想在某個組件的根元素上監聽一個原生事件。可使用 .native 修飾 v-on

<my-component v-on:click.native="doTheThing"></my-component>

探究v-model

v-model能夠對錶單控件實現數據的雙向綁定,它的原理就是利用了綁定屬性和事件來實現的。好比input控件。不使用v-model,能夠這樣實現數據的雙向綁定:

<div id="app4">
    <input type="text" v-bind:value="text" v-on:input="changeValue($event.target.value)">
    {{text}}
  </div>
  <script>
      new Vue({
        el: '#app4',
        data: {
          text: '444'
        },
        methods: {
          changeValue (value) {
            this.text = value
          }
        }
      })
  </script>

上面的代碼一樣實現了數據的雙向綁定。其本質就是:

  • inputvalue特性綁定到Vue實例的屬性text上,text改變,input中的內容也會改變
  • 而後把表單的input事件處理函數設置爲Vue實例的一個方法,這個方法會根據輸入參數改變Vue中text`的值
  • 相應的,在input中輸入內容時,觸發了input事件,把event.target.value傳給這個方法,最後就實現了改變綁定的數據的效果。

v-model就是上面這種方式的語法糖,也就是把上面的寫法封裝了一下,方便咱們使用。

使用自定義事件建立自定義的表單輸入組件

理解了v-model的內幕,也就能夠把這個效果用在自定義表單組件上了。
來實現一個簡單的只能輸入hello的表單輸入組件。

<div id="app5">
    <my-component3 v-model="hello"></my-component3>
    <div>{{hello}}</div>
</div>
<script>
  Vue.component('my-component3', {
    template: `<input ref="input" type="text" :value="value" @input="checkInput($event.target.value)">`,
    props: ['value'],
    methods: {
      checkInput (value) {
        var hello = 'hello'
        if (!hello.includes(value)) {
          this.$emit('input', hello)
          this.$refs.input.value = hello
        } else {
          this.$emit('input', value)
        }
      }
    }
  })
  new Vue({
    el: '#app5',
    data: {
      hello: ''
    }
  })
</script>

定製組件的v-model

默認狀況下,一個組件的 v-model 會使用 value 屬性和 input 事件,可是諸如單選框、複選框之類的輸入類型可能把 value 屬性用做了別的目的。model 選項能夠迴避這樣的衝突:

Vue.component('my-checkbox', {
  model: {
    prop: 'checked',   // 將輸入的特性改成checked
    event: 'change'        // 觸發的自定義事件類型爲change
  },
  props: {
    checked: Boolean,
    // this allows using the `value` prop for a different purpose
    value: String
  }
})

這樣設置的話,

<my-checkbox v-model="foo" value="some value"></my-checkbox>

上面的代碼就等同於

<my-checkbox :checked="foo" @change="val => { foo = val }" value="some value"></my-checkbox>

實際使用時,與以前不一樣的地方是:

  1. 把子組件中接收外部數據的prop屬性改成checked
  2. 向父組件發出事件時,事件類型應改成change
Vue.component('my-component3', {
    template: `<input ref="input" type="text" :value="checked" @input="checkInput($event.target.value)">`,
    props: ['checked'],        // 屬性名改變
    model: {
      prop: 'checked',
      event: 'change'
    },
    methods: {
      checkInput (value) {
        var hello = 'hello'
        if (!hello.includes(value)) {
          this.$emit('change', hello)   // 事件類型改變
          this.$refs.input.value = hello
        } else {
          this.$emit('change', value)  // 事件類型改變
        }
      }
    }
  })

動態組件

經過使用保留的 <component> 元素,動態地綁定到它的 is 特性,可讓多個組件使用同一個掛載點,並動態切換:

<div id="app6">
    <select v-model="currentComponent">
      <option value="home">home</option>
      <option value="post">post</option>
      <option value="about">about</option>
    </select>
    <component :is="currentComponent"></component>
  </div>
  <script>
      new Vue({
        el: '#app6',
        data: {
          currentComponent: 'home'
        },
        components: {
          home: {
            template: `<header>這是home組件</header>`
          },
          post: {
            template: `<header>這是post組件</header>`
          },
          about: {
            template: `<header>這是about組件</header>`
          }
        }
      })
</script>

也能夠直接綁定到組件對象上:

var Home = {
  template: `<header>這是home組件</header>`
}
new Vue({
  el: '#app6',
  data: {
    currentComponent: Home
  }
})

保留切換出去的組件,避免從新渲染

若是把切換出去的組件保留在內存中,能夠保留它的狀態或避免從新渲染。爲此能夠添加一個 keep-alive 指令參數:

<keep-alive>
  <component :is="currentComponent">
    <!-- 非活動組件將被緩存! -->
  </component>
</keep-alive>
相關文章
相關標籤/搜索