vue組件和父子組件間通信的13種方式(包含動態、異步組件和常用的prop、$emit、插槽等)

本篇文章分成了兩個部分,第一部分是組件基礎,介紹全局組件、局部組件的註冊,動態組件和異步組件的使用等等,第二部分是組件之間的通信方式,本文整理了13種,在下面依次介紹。文章較長,看的時候要有耐心哦~

組件基礎

我們在製作一個網站的時候,通常會用到代碼相同的部分,比如說導航欄,如果不能很好的複用,那麼就會導致大量重複代碼,不好維護,vue中的組件,很好的解決了這個問題,什麼是組件呢?

組件是可複用的Vue實例,且帶有一個名字,例如名字爲monk-component,那麼我們則可以在一個通過new Vue創建的根實例中,把這個組件作爲自定義元素來使用,是可以多次使用的:

<div id="app">
  <monk-component></monk-component>
  <monk-component></monk-component>
</div>

因爲組件是可複用的 Vue 實例,所以它們與 new Vue 接收相同的選項,例如 datacomputedwatchmethods 以及生命週期鉤子等。僅有的例外是像 el 這樣根實例特有的選項。就像這樣:

Vue.component('monk-component',{
    data(){
        return {
            name:"monk"
        }
    },
    template:`
        <strong> {{ name }} </strong>
    `
})
const vm = new Vue({
    el:"#app",
    data:{
        name:"young monk"
    },
})

上面例子中的組件是在全局組件,組件的註冊分爲全局組件和局部組件,我來依次介紹一下:

  • 全局組件:通過Vue.component({string} id, {Function | Object}[definition])來註冊,參數:
    • 字符串類型的id(組件名)
    • 一個可選的對象或方法,我們通常傳一個對象,這個對象中含有和vue實例相同的屬性和方法(data,computed,watch等)。

組件註冊之後可以用在任何創建的 Vue 根實例 (new Vue) 的模板中。就像這樣來創建一個組件:

Vue.component('button-counter', {
  data () {
    return {
      name: "monk",
    }
  },
  template: `
    <strong> {{ name }} </strong>
  `
})
  • 局部組件:通過vue實例中的components選項來進行定義,components選項是一個對象,屬性名就是自定義組件的名字,屬性值就是自定義的組件。看個例子就明白了
const monkComponent = {
  data () {
    return {
      count: 0
    }
  },
  template: `
    <strong> {{ name }} </strong>
  `,
}

const vm = new Vue({
  el: '#app',
  components: {
    'monk-componentr': monkComponent
  }
})
<div id="app">
  <monk-component></monk-component>
</div>

當然我們上面的組件都是在js中創建的組件,我再舉一個使用vue單文件組件的例子吧

MonkCompontent.vue

<template>
  <div class="name">
    {{ name }}
  </div>
</template>

<script>
export default {
  name: 'MonkCompontent',
  data() {
    return {
      name:"monk"
    }
  }
}
</script>

App.vue

<template>
  <div id="app">
    <monk-compontent></monk-compontent>
  </div>
</template>
<script>
import MonkCompontent from './components/MonkCompontent.vue'
export default {
  name: 'App',
  components: {
    MonkCompontent
  }
}
</script>

註冊組件,那肯定是要定義組件名的,我們在命名時要最尋語義化的規範,定義組件名有兩種方式:

  • kebab-case (橫短線分隔命名):當使用kebab-case定義一個組件時**,你必須在引用這個自定義元素時使用kebab-case**,例如:<monk-component></monk-component>
Vue.component('monk-component', {/***/});
  • PascalCase (大駝峯命名):當使用PascalCase定義一個組件時,你在引用這個自定義元素時兩種命名法都可以。也就是說<monk-component><MonkComponent> 都是可接受的。
Vue.component('MonkComponent', {/***/});

注意:直接在 DOM (即字符串模板或單文件組件) 中使用時只有 kebab-case 是有效的。

補充:我們強烈推薦遵循 W3C 規範中的自定義組件名 (字母全小寫且必須包含一個連字符)。這會幫助你避免和當前以及未來的 HTML 元素相沖突。

我們在定義組件中,data並不是一個對象,而是寫成了一個函數的形式,是因爲每個實例可以維護一份單獨的對象,防止變量污染

data () {
  return {
    name: ""
  }
}

如果 Vue 沒有這條規則,點擊一個按鈕就可能會像下面一樣影響到其它所有實例(官網偷的圖):

在這裏插入圖片描述

在最後我們需要注意的是:每個組件必須只有一個根元素,當模板的根元素大於1時,可以將模板的內容包裹在一個父元素內。

動態組件

當我們在一個多標籤的界面中,在不同組件之間進行動態切換是非常有用的。先看個例子吧:

我實現這個選項卡的功能,就是通過動態組件component實現的,componentis 特性來綁定展示哪一個組件。代碼如下:

<div id="app">
    <div style="margin-bottom: 10px;">
        <button  @click="pageCmp = 'base-home'" >主頁</button>
        <button  @click="pageCmp = 'base-more'">更多內容</button>
    </div>
    <component :is="pageCmp"></component>
</div>
<script>
    //註冊home組件
    Vue.component('base-home', {
        data() {
            return {
               postCmp:'',
               buttons:[{title:"標題1",content:{template:`<h4>內容1</h4>`}},
                        {title:"標題2",content:{template:`<h4>內容2</h4>`}},
                        {title:"標題3",content:{template:`<h4>內容3</h4>`}}]
            }
        },
        mounted() {
            this.postCmp = this.buttons[0].content
        },
        template: `
            <div>
            <button
                v-for="(btn,index) in buttons"
                @click="postCmp = btn.content"
                :key="index"
            >{{ btn.title }}</button>
            <component :is="postCmp"></component>
            </div>
        `
    })
    Vue.component('base-more', {
        template: `<h3>更多內容</h3>`
    })

    const vm = new Vue({
        el: '#app',
        data: {
            pageCmp: 'base-home'
        },
    })
</script>

通過上面方法,我們可以實現組件間的切換,能夠注意到的是:每一次切換標籤時,都會創建一個新的組件實例,重新創建動態組件在更多情況下是非常有用的。

在這個案例中,我們會更希望哪些標籤的組件實例能夠在它們第一次被創建的時候緩存下來。爲了解決這個問題,我們可以用一個<keep-alive>元素將動態組件包裹起來。

keep-alive<keep-alive> 包裹動態組件時,會緩存不活動的組件實例,而不是銷燬它們。<keep-alive> 是一個抽象組件:它自身不會渲染一個 DOM 元素,也不會出現在父組件鏈中。當組件在 <keep-alive> 內被切換,它的 activated 和 deactivated 這兩個生命週期鉤子函數將會被對應執行。

  • activated:keep-alive 組件激活時調用。

  • deactivated:keep-alive 組件停用時調用。

使用完的效果,我們可以看到當我在標題2切換到更多內容,再切回到主頁時,顯示的還是標題2:

```html ```

注意<keep-alive> 要求被切換到的組件都有自己的名字,不論是通過組件的 name 選項還是局部/全局註冊。

異步組件

在項目中,有些組件不會在第一次進入首屏時加載,而是當執行了某些操作時,纔會加載進來,所以此時,我們可以將該組件設置成異步加載,什麼時候用,什麼時候再加載進來,以達到提升首屏性能的目的。

使用方法:在vue文件中使用

components: {
  AsyncCmp: () => import (url);
}

在webpack中,使用異步組件的方式調用的時候,在打包階段會把該組件單獨打成一個文件,如文件果太多的話,無疑會增加http性能開銷,所以我們通常將多個需要同時加載的組件合併到一個文件中:只需要加上特殊註釋就可以了:

components: {
  AsyncCmp1: () => import(/* webpackChunkName: "async" */ 'url'),
  AsyncCmp2: () => import(/* webpackChunkName: "async" */ 'url'),
}

異步加載的文件,會在link標籤上設置 el="prefech"。瀏覽器會在空閒時間內下載對應的資源,使用時可以直接從緩存中獲取。與之對應的 el="preload",會及時下載對應的資源。

父子組件間的通信

我先來總體介紹一下,再依次介紹每一個的用法

  • prop:父組件傳遞數據給子組件時,可以通過特性傳遞。推薦使用這種方式進行父->子通信。

  • $attrs:祖先組件傳遞數據給子孫組件時,可以利用$attrs傳遞。$attrs的真正目的是撰寫基礎組件,將非Prop特性賦予某些DOM元素。

  • $emit:子組件傳遞數據給父組件時,觸發事件,從而拋出數據。推薦使用這種方式進行子->父通信。

  • .native:將原生的事件綁定到組件上,$emit的語法糖

  • $listeners:可以在子孫組件中執行祖先組件的函數,從而實現數據傳遞。$listeners的真正目的是將所有的事件監聽器指向這個組件的某個特定的子元素。使用$listeners就不要使用.native

  • v-mode & .sync:在組件上使用

  • slot:插槽,將組件中html中的內容傳遞給組件。

  • $root:可以在子組件中訪問根實例的數據。

  • $parent:可以在子組件中訪問父實例的數據。

  • $children:可以在父組件中訪問子實例的數據。。

  • ref:可以在父組件中訪問子實例的數據。$refs 只會在組件渲染完成之後生效,並且它們不是響應式的。

  • provide & inject:祖先組件提供數據(provide),子孫組件按需注入(inject)。會將組件的阻止方式,耦合在一起,從而使組件重構困難,難以維護。

  • Vuex:狀態管理,中大型項目時強烈推薦使用此種方式,以後介紹

我推薦使用prop$emitslot、和vuex這四種方式進行通信,ref在書寫模態框或表單通用組件時常用到來控制組件的顯示/隱藏和初始化。其他的方式都不建議使用,會使數據流難以控制和難以理解。

組件默認只是寫好結構、樣式和行爲,使用的數據應由外界傳遞給組件。

Prop:父傳子

Prop的基本使用

如何使用prop屬性?首先我們要註冊需要接受的prop,使用的數據數據應由外界傳遞給組件。啥?看不懂,看個例子你就明白了!

<div id="app">
    <!-- 註冊一個name屬性 -->
    <monk-component name="young monk"></monk-component>
</div>
<script>
    Vue.component('monk-component',{
        //在組件中接收name屬性
        props:["name"],
        template:`
            <strong> {{ name }} </strong>
        `
    })
    const vm = new Vue({
        el:"#app",
    })
</script>

我們首先註冊一個monk-component組件,註冊一個name屬性,在組件monk-component中使用props接收name屬性,然後我們就可以使用啦。我們在組件中訪問這個值,解就像訪問data中的值一樣這個props接收的數據在created的階段就已經初始化完成了。我們最早可以在created階段使用它!

是不是很簡單!我們再在vue文件中使用一下:

<!-- App.vue -->
<template>
  <div id="app">
    <monk-compontent name="young monk"></monk-compontent>
  </div>
</template>
<script>
import MonkCompontent from './components/MonkCompontent.vue'
export default {
  name: 'App',
  components: {
    MonkCompontent
  }
}
</script>

<!-- MonkCompontent.vue -->

<template>
  <div class="name">
    {{ name }}
  </div>
</template>
<script>
export default {
  name: 'MonkCompontent',
  props:["name"]
}
</script>

在vue文件中使用和使用Vue.component註冊的方式相同,所以下面的例子,我就不再使用vue文件的方式舉例了(其實是我太懶了,嘿嘿嘿)。

我們在上面的例子中可以看到,傳入的是靜態的prop數據,那如何傳入動態的Prop呢?

  • 靜態Prop
<monk-compontent name="young monk"></monk-compontent>
  • 動態Prop:若想要傳遞一個動態的值,可以配合v-bind指令進行傳遞,如:
<monk-compontent :name="name"></monk-compontent>
  • 傳遞一個對象的所有屬性
<!-- 
person: {
  name: 'monk',
  age: 18
} 
-->

<monk-compontent :name="person"></monk-compontent>

上述代碼等價於:

<monk-component
  :name="person.name"
  :age="person.age"
></monk-component>

Prop的大小寫

HTML 中的特性名是大小寫不敏感的,所以瀏覽器會把所有大寫字符解釋爲小寫字符。故:當 傳遞的prop爲 短橫線分隔命名時,組件內 的props 應爲 駝峯命名 。如:

<div id="app">
  <!-- 在 HTML 中是 kebab-case 的 -->
  <nav-item nav-title="hello!"></nav-item>
</div>
Vue.component('nav-item', {
  // 在 JavaScript 中是 camelCase 的
  props: ['navTitle'],
  template: '<h3>{{ postTitle }}</h3>'
})

注意:如果使用的是字符串模板,那麼這個限制就不存在了。

Prop驗證

在開發中我們通常會爲組件的 prop 指定驗證要求,例如你可以要求一個 prop 的類型爲什麼。如果說需求沒有被滿足的話,那麼Vue會在瀏覽器控制檯中進行警告,這在開發一個會被別人用到的組件時非常的有幫助。

這時候你prop就不再是一個字符串數組了,而是一個對象。like this:

Vue.component('monk-component', {
  props: {
    name: String,
    age: Number
  }
})

上述代碼中,對prop進行了基礎的類型檢查,name必須是字符串,age必須是數字,類型值可以爲下列原生構造函數中的一種StringNumberBooleanArrayObjectDateFunctionSymbol、任何自定義構造函數、或上述內容組成的數組。需要注意的是**nullundefined 會通過任何類型驗證。**

除基礎類型檢查外,我們還可以配置高級選項,對prop進行其他驗證,如:類型檢測、自定義驗證和設置默認值。

Vue.component('my-component', {
  props: {
    name: {
      type: String, 
      default: 'the young monk',
      required: true,
      validator (prop) { 
        return prop.length < 30;//該prop長度小於30個
      }
    }
  }
})

我來依次解釋一下上面用到的檢查:

  • type:檢查 prop 是否爲給定的類型
  • default:定義該 prop 的默認值,對象或數組的默認值必須從一個工廠函數返回,就像這樣
Vue.component('my-component', {
  props: {
    person: {
      type: Object,
      default: default(){
      	return { name: "monk" }
  	  }
    }
  }
})
  • required: 定義該 prop 是否是必填項
  • validator:自定義驗證函數

注意:prop 會在一個組件實例創建之前進行驗證,所以實例的屬性 (如 datacomputed 等) 在 defaultvalidator 函數中是不可用的。

爲了更好的團隊合作,在提交的代碼中,prop 的定義應該儘量詳細,至少需要指定其類型。

單向數據流

所有的 prop 都使得其父子 prop 之間形成了一個單向下行綁定:父級 prop 的更新會向下流動到子組件中,但是反過來則不行。這樣會防止從子組件意外改變父級組件的狀態,從而導致你的應用的數據流向難以理解。

那麼,問題來了,如果你想要將prop變量修改該怎麼辦呢?這裏有兩種可以改變prop的方法:

  • 定義本地的data,把prop的值賦值給它。例如:這個 prop 用來傳遞一個初始值;這個子組件接下來希望將其作爲一個本地的 prop 數據來使用,在後續操作中,會將這個值進行改變。
props: ['initCounter'],
data: function () {
  return {
    counter: this.initCounter
  }
}
  • 定義一個有關prop值的計算屬性。例如:這個 prop 以一種原始的值傳入且需要進行轉換。
props: ['firstName','lastName'],
computed: {
  fullName: function () {
    return `${this.firstName}·${this.lastName}`
  }
}

非Prop的特性

非Prop特性指的是,一個未被組件註冊的特性。當組件接收了一個非Prop特性時,該特性會被添加到這個組件的根元素上。就像這樣:

<div id="app">
    <monk-component class="date" name="monk"></monk-component>
</div>
<script>
    Vue.component('monk-component',{
        template:`
            <div class="myClass" name="young">
                <input class="input" />
            </div>
        `
    })
    const vm = new Vue({
        el:"#app",
        data:{
            name:"young monk"
        },
    })
</script>

我們看一下它掛載後的結果:他會把非prop屬性,直接當成組件的根元素的特性。

<div name="monk" class="myClass date">
    <input class="input">
</div>

合併已有的特性:上面的例子中,我們在組件的模板中定義了一個自己的class類名,然後又通過組件穿了一個date,最後,class是合併了,而非替換掉。只有class和style例外

替換已有的特性:對於大多數特性來說(除了class和style),從外部提供給組件的值會替換掉組件內部設置好的值。就像上面的name特性一樣。

禁用特性繼承:如果不希望組件的根元素繼承特性,那麼可以在組件選項中設置 inheritAttrs: false。如:

Vue.component('monk-component',{
    inheritAttrs:false,
    template:`
        <div class="myClass" name="young">
            <input class="input" />
        </div>
    `
})

結果如下:這個時候name的特性值就不會被替換掉了,但是我們也看到myClass依舊被合併到class特性上了

<div name="young" class="myClass date">
    <input class="input">
</div>

這就需要我們注意:inheritAttrs: false 選項不會影響 style 和 class 的綁定。

$attrs

在這種情況下,非常適合去配合實例的 $attrs 屬性使用,這個屬性是一個對象,鍵名爲傳遞的特性名,鍵值爲傳遞特性值。使用 inheritAttrs: false$attrs 相互配合,我們就可以手動決定這些特性會被賦予哪個元素。

<div id="app">
    <monk-component class="date" name="monk" placeholder='Enter your username'></monk-component>
</div>
<script>
    Vue.component('monk-component',{
        inheritAttrs:false,
        created() {
            console.log(this.$attrs)
        },
        template:`
            <div class="myClass" name="young">
                <input class="input" :placeholder="$attrs.placeholder"/>
            </div>
        `
    })
    const vm = new Vue({
        el:"#app",
        data:{
            name:"young monk"
        },
    })
</script>

在組件中,控制檯中的輸出console.log(this.$attrs) 的結果是:

{name: "monk", placeholder: "Enter your username"}

我們再看一下渲染結果,name還是youngmyClass被綁定到class特性上,inputplaceholder也拿到了正確的值

$emit組件事件監聽:子傳父

首先,我們來寫一個組件,用途呢,就是來承認我真的很帥,嘿嘿,如下:

<div id="app">
    <div>
        <p>我真的很帥!</p> 
        <span>同意人數:</span><strong>{{ count }}</strong>
    </div>
    <div style="margin-top: 20px;">
        <monk v-for="(number,index) in numbers" :key="index" :number="number" @add="add"></monk>
    </div>
</div>

我們註冊一下,monk組件

Vue.component('monk', {
    props: {
        number:Number
    },
    methods: {
        addCount(number){
            this.$emit("add",number)
        }
    },
    template: `
        <button v-on:click="addCount(number)">同意人數 +{{ number }} </button>
    `,
})

最後創建一個vue實例

const vm = new Vue({
    el: '#app',
    data: {
        numbers:[1,10,100],
        count: 1
    },
    methods: {
        add(num){
            this.count += num
        }
    },
})

先看一眼效果圖,然後我再解釋一下用法,哈哈哈哈,我是真的閒,都是我點出來的!

在這裏插入圖片描述
可以看到,每一個子組件中都有一個按鈕,可以增加父組件的同意人數,這就涉及到了子組件向父組件傳遞數據,

首先我們給點擊按鈕添加一個事件,如下:

 methods: {
    addCount(number){
        this.$emit("add",number)
    }
}

這部分的重點來了,$emit,通過調用$emit()方法( $emit(事件名, 參數1, 參數2, ...) ),傳入一個事件名,來觸發一個自定義事件。自定義事件必須要在子組件上定義過:

<monk v-for="(number,index) in numbers" :key="index" :number="number" @add="add">

就像這樣,子組件上自定義add事件,綁定一個add函數。這樣,父組件就可以接收該事件,更新數據 count 的值了。

$emit()方法的二個參數,是想要拋出的值,我們有兩種方式可以接收:

  • 直接使用$event訪問該值
<monk  @add="count += $event"></monk>
  • 定義一個函數,傳入該值
<monk @add="add"></monk>

<!-- methods -->
add(num){
    this.count += num
}

如果你想要拋出多個值,那就只能使用定義函數的方式了,因爲eventevent只能訪問到emit的第二個參數

$emit的大小寫

不同於組件和prop,事件名不存在任何自動化的大小寫轉換。而是觸發的事件名需要完全匹配監聽這個事件所有的名稱。如果觸發一個camelCase名字的事件:

this.$emit('myEvent')

則監聽這個名字的kabab-case版本是不會有任何效果的。

<!-- 沒有效果 -->
<my-component v-on:my-event="doSomething"></my-component>

並且 v-on 事件監聽器在 DOM 模板中會被自動轉換爲全小寫,所以 @myEvent將會變成 @myevent,導致 myEvent不可能被監聽到。

因此,推薦始終使用 kebab-case 的事件名

.native:將原生事件綁定到組件

在組件上去監聽事件時,我們監聽的是組件的觸發的自定義事件,但是在一些情況下,我們可能想要在一個組件的根元素上直接監聽一個原生事件。這時,可以使用 v-on 指令的 .native 修飾符,如:

<div id="app">
    <base-input @focus.native="onFocus" @blur.native="onBlur"></base-input>
</div>
<script>
    Vue.component('base-input', {
        template: `
            <input type="text" />
        `
    })
    const vm = new Vue({
        el: '#app',
        methods: {
            onFocus() {
                console.log("onFocus")
            },
            onBlur() {
                console.log("onBlur")
            }
        },
    })
</script>

我們通過.native修飾符,就可以直接在根元素上觸發在父組件中定義的input的原生事件。但是我們的子組件通常不會就只寫一個input的組件,經常會有其他的重構:

<label>
  姓名:<input type="text">
</label>

可以看到,此時組件的根元素實際上是一個label元素,那麼父級的.native監聽器將靜默失敗。它不會產生任何報錯,但是onFocusonBlur處理函數不會如預期被調用。

$listeners

爲了解決這個問題,Vue提供了一個$listeners屬性,它是一個對象,裏面包含了作用在這個組件上的所有監聽器。例如:

{
  focus: function (event) { /* ... */ }
  blur: function (event) { /* ... */ },
}

有了這個 $listeners 屬性,我們可以配合 v-on="$listeners" 將所有的事件監聽器指向這個組件的某個特定的子元素,如:

<div id="app">
    <base-input @focus="onFocus" @blur="onBlur"></base-input>
</div>
<script>
    Vue.component('base-input', {
        mounted(){
            console.log(this.$listeners)
        }
        template: `
            <label>
                姓名:<input v-on="$listeners" />
            </label>
            `
    })
    const vm = new Vue({
        el: '#app',
        methods: {
            onFocus() {
                console.log("onFocus")
            },
            onBlur() {
                console.log("onBlur")
            }
        },
    })
</script>

注意:此時使用$listeners時,就不要再使用.native

v-model:在組件上使用

由於自定義事件的出現,在組件上也可以使用v-model指令。我在之前的博客上講過在 input 元素上使用 v-mode指令時,相當於綁定了value特性以及監聽了input事件:

<input v-model="searchText" />

等價於:

<input
  :value="searchText"
  @input="searchText = $event.target.value"
>

當把v-model指令用在組件上時:

<base-input v-model="searchText" /> 

則等價於:

<base-input
  :value="searchText"
  @input="searchText = $event"
/>

input 元素一樣,在組件上使用v-model指令,也是綁定了value特性,監聽了input事件。

所以,爲了讓 v-model 指令正常工作,這個組件內的<input>必須:

1、將其value特性綁定到一個叫 valueprop

2、在其input事件被觸發時,將新的值通過自定義的input事件拋出

Vue.component('base-input', {
    props: ['value'],
    template: `<input 
				:value="value"
				@input="$emit('input', $event.target.value)" />`
})

完整代碼:可以看到我這裏並沒有通過prop屬性傳遞value的值,是因爲根元素會繼承組件的特性

<div id="app">
    <base-input v-model="searchText" ></base-input>
</div>
<script>
    Vue.component('base-input', {
        template: `<input @input="$emit('input', $event.target.value)" />`
    })
    const vm = new Vue({
        el: '#app',
        data:{
            searchText:"please enter content"
        },
        watch: {
            searchText(){
                console.log(this.searchText)
            }
        },
    })
</script>

這樣操作後,v-model就可以在這個組件上工作起來了。當然在我做過的項目中都沒有人這麼寫,感覺沒用什麼亂用。

.sync 修飾符

除了使用 v-model 指令實現組件與外部數據的雙向綁定外,我們還可以用 v-bind 指令的修飾符 .sync 來實現。

先回憶一下,不利用 v-model 指令來實現組件的雙向數據綁定:

<base-input :value="searchText" @input="searchText = $event"></base-input>
Vue.component('base-input', {
  props: ['value'],
  template: `
    <input 
      :value="value"
      @input="$emit('input', $event.target.value)"
    />
  `
}) 

.sync 修飾符其實一個語法糖,省略了定義自定義事件的過程

<div id="app">
    <base-input :value.sync="searchText"></base-input>
</div>
<script>
    Vue.component('base-input', {
        props: ["value"],
        template: `<input 
                    :value="value"
                    @input="$emit('update:value', $event.target.value)" />
                `
    })
    const vm = new Vue({
        el: '#app',
        data: {
            searchText: "please enter content"
        },
        watch: {
            searchText() {
                console.log(this.searchText)
            }
        }
    })
</script>

在組件上使用:

<base-input :value.sync="searchText"></base-input>

等價於:

<base-input
  :value="searchText"
  @update:value="searchText = $event"
/>

注意:

  • 帶有.sync修飾符的v-bind指令,只能提供想要綁定的屬性名,不能和表達式一起使用,如::title.sync="1+1",這樣操作是無效的
  • v-bind.sync 用在 一個字面量對象上,如 v-bind.sync="{ title: 'haha' }",是無法工作的,因爲在解析一個像這樣的複雜表達式的時候,有很多邊緣情況需要考慮。

v-model 和 .sync 的異同

.sync的發展史:先明確一件事情,在 vue 1.x 時,就已經支持 .sync 語法,但是此時的 .sync 可以完全在子組件中修改父組件的狀態,造成整個狀態的變換很難追溯,所以官方在2.0時移除了這個特性。然後在 vue2.3時,.sync又迴歸了,跟以往不同的是,現在的.sync完完全全就是一個語法糖的作用,跟v-model的實現原理是一樣的,也不容易破環原有的數據模型,所以使用上更安全也更方便。

  • 兩者都是用於實現雙向數據傳遞的,實現方式都是語法糖,最終通過 prop + 事件 來達成目的。
  • vue 1.x 的 .sync 和 v-model 是完全兩個東西,vue 2.3 之後可以理解爲一類特性,使用場景略微有區別
  • 當一個組件對外只暴露一個受控的狀態,切都符合統一標準的時候,我們會使用v-model來處理。.sync則更爲靈活,凡是需要雙向數據傳遞時,都可以去使用。

slot:插槽

插槽基本使用

我們使用過組件的話應該知道,組件中的內容會被直接覆蓋掉,並不會展示。但是如果我們想要和 HTML 元素一樣,將組件中的內容保存下來,就要使用插槽slot,就像這樣:

<div id="app">
    <monk-cmp>
        the young monk
    </monk-cmp>
</div>
<script>
    Vue.component("monk-cmp",{
        template: `<div><slot></slot></div>`
    })
    const vm = new Vue({
        el: "#app"
    })
</script>

頁面渲染爲:

the young monk

我來講解一下具體的使用方法:

插槽內容:我們把需要的內容寫在組件裏

<monk-cmp>
  寫在組件標籤結構中的內容
</monk-cmp>

組件模板:在組件模板中使用插槽 slot

<div>
  <slot></slot>
</div>

當組件渲染時,<slot></slot>將會被替換爲“寫在組件標籤結構中的內容”。插槽內可以包含任何模板代碼,包括HTML和其他組件。如果<monk-cmp>沒有包含<slot>元素,則該組件起始標籤和結束標籤之間的任何內容都會被拋棄。

編譯作用域

當在插槽中使用數據時:

<monk-cmp>
  名字:{{ name }}
</monk-cmp>

該插槽跟模板的其他地方一樣可以訪問相同的實例屬性,也就是相同的“作用域”,而不能訪問<monk-cmp>的作用域。

<div id="app">
    <monk-cmp>
        <!-- 此時name的值是monk,相同作用域是根vue實例 -->
        名字:{{ name }}
    </monk-cmp>
</div>
<script>
    Vue.component("monk-cmp",{
        data() {
            return {
                name: "the young monk"
            }
        },
        template: `<div><slot></slot></div>`
    })
    const vm = new Vue({
        data:{
            name: "monk"
        },
        el: "#app"
    })
</script>

此時的名字namemonk,使用的是根實例的作用域,我們只需要記住:父級模板裏的所有內容都是在父級作用域中編譯的;子模板裏的所有內容都是在子作用域中編譯的。

後備內容

我們可以設置插槽使用的默認值,它會在沒有提供內容時被渲染,比如:我們希望這個<button>內絕大多數情況下都渲染文本“Submit”,只有少數情況下使用其他的內容,此時就可以將“Submit”作爲後備內容,like this:

Vue.compopnent('monk-cmp', {
  template: `
    <button type="submit">
      <slot>Submit</slot>
    </button>
  `
})

當使用組件未提供插槽時,後備內容將會被渲染。如果提供插槽,則後備內容將會被取代。

具名插槽

具名插槽,故名思意,就是帶有名字的插槽,有時我們需要多個插槽,希望不同的插槽顯示不同的內容,如何正確綁定?這是就可以使用具名插槽,如<monk-cmp>組件:

Vue.compopnent('monk-cmp', {
  template: `
    <div class="container">
      <header>
        <!-- 導航欄 -->
        <slot name="header"></slot>
      </header>

      <main>
        <!-- 主要內容,不帶名字,默認是name是default -->
        <slot></slot>
      </main>

      <footer>
      	<!-- 頁腳 -->
        <slot name="footer"></slot>
      </footer>
    </div>
  `
})

此時,可以在<slot>元素上使用一個特殊的特性:name。利用這個特性定義額外的插槽。一個不帶 name<slot> 出口會帶有隱含的名字default。在向具名插槽提供內容的時候,我們可以在一個 <template> 元素上使用 v-slot 指令,並以 v-slot 的參數的形式提供其名稱:

<monk-cmp>
    <template v-slot:header>
    	<h1>頭部</h1>
    </template>
    
    <div>
        <p>內容1</p>
        <p>內容2</p>
    </div>
    <!-- 更好的寫法,模板更爲清晰,上面的無命名的插槽 -->
    <!-- 
        <template v-slot:default>
            <p>內容</p>
            <p>內容</p>
        </template>
	-->
    <template v-slot:footer>
    	<p>底部</p>
    </template>
</monk-cmp>

現在<template>元素中的所有內容都會被傳入相應的插槽。任何沒有被包裹在帶有v-slot<template>中的內容都會被視爲默認插槽的內容。

注意:v-slot只能添加在<template>上,只有一種例外情況,就是下面介紹的的獨佔默認插槽的縮寫語法。

作用域插槽

爲了能夠讓插槽內容訪問子組件的數據,我們可以將子組件的數據作爲<slot>元素的一個特性綁定上去:

Vue.component('monk-cmp', {
    data() {
        return {
            description:"the young monk"
        }
    },
    template: `<div>
					<slot v-bind:description="description"></slot>
			   </div>`,
})

綁定在 <slot> 元素上的特性被稱爲插槽 prop

那麼在父級作用域中,我們可以給v-slot帶一個值來定義我們提供的插槽prop的名字:

<div id="app">
    <monk-cmp>
        <template v-slot:default="slotProps">
       		 {{ slotProps.description }}
        </template>
    </monk-cmp>
</div>

獨佔默認插槽的縮寫語法

當被提供的內容只有默認插槽時,組件的標籤可以被當作插槽的模板來使用,此時,可以將v-slot直接用在組件上:

<monk-cmp v-slot:default="slotProps">
  {{ slotProps.description }}
</monk-cmp>

也可以更簡單:

<monk-cmp v-slot="slotProps">
  {{ slotProps.description }}
</monk-cmp>

注意:默認插槽的縮寫語法不能和具名插槽混用,因爲它會導致作用域不明確

<!-- 無效,會導致警告 -->
<monk-cmp v-slot="slotProps">
  {{ slotProps.description }}
  <template v-slot:other="otherSlotProps">
    slotProps 在這裏是不合法的
  </template>
</monk-cmp>

只要出現多個插槽,就需要爲所有的插槽使用完整的基於<template>的語法。

解構插槽Prop

Vue.component('monk-cmp', {
    data() {
        return {
            person:{
                name:"monk",
                description:"the young monk"
            }
        }
    },
    template: `<div><slot v-bind:person="person"></slot></div>`,
})

我們可以使用解構傳入具體的插槽prop,如:

<monk-cmp>
    <template v-slot="{ person }">
        {{ person.description }}
    </template>
</monk-cmp>

這樣模板會更簡潔,尤其是在爲插槽提供了多個prop時。我們同樣可以使用解構的重命名

<monk-cmp>
    <template v-slot="{ person: user }">
        {{ user.description }}
    </template>
</monk-cmp>

自定義後備內容(默認值):當插槽prop是undefined時生效:

<monk-cmp v-slot="{ person = { name: 'monk' } }">
  {{ person.name }}
</monk-cmp>

動態插槽名

Vue 2.6.0新增

我們可以使用變量名,作爲插槽名:

<monk-cmp>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</monk-cmp>

具名插槽的縮寫

Vue 2.6.0新增

v-onv-bind一樣,v-slot也有縮寫,將v-slot:替換爲#

<monk-cmp>
  <template #header>
    <h1>頭部</h1>
  </template>

  <template #default>
    <p>內容</p>
    <p>內容</p>
  </template>

  <template #footer>
    <p>底部</p>
  </template>
</monk-cmp>

當然,和其它指令一樣,該縮寫只在其有參數的時候纔可用。

最後還有兩個:屬性帶有slot特性的具名插槽帶有slot-scope特性的作用域插槽,但這兩個在vue 2.6.0被廢除了,所以我就不解釋了。需要用到的話可以去官網查看。

https://cn.vuejs.org/v2/guide/components-slots.html

$root:訪問根實例

在每個子組件中,可以通過 $root 訪問根實例。

我們創建三個子組件,下面的幾個例子都是在這三個組件上進行的,組件結構是這個樣子的

<div id="#app">
	<monk-parent>
		<monk-b>
			<monk-c></monk-c>
		</monk-b>
	</monk-parent>
</div>

創建這些組件:

Vue.component('monk-parent', {
    data() {
        return { name:"monkParent" }
    },
    template: ` <div> monk-parent
					<monk-b></monk-b>
				/div>`
})
Vue.component('monk-b', {
    data() {
        return { name:"monkB" }
    },
    template: `<div> monk-b
					<monk-c></monk-c>
			   </div>`
})
Vue.component('monk-c', {
    data() {
        return { name:"monkC" }
    },
    template: `<div>monk-c</div>`
})

創建一個vue實例:

const vm = new Vue({
    el: '#app',
    data:{
        name:"monk"
    },
    methods:{
        printName(this.name)
    }
    computed:{
        getName(){
            return this.name + "是最帥的!"
        }
    }
})

所有的子組件都可以將這個實例作爲一個全局 store 來訪問或使用。

// 獲取根組件的數據
this.$root.name

// 寫入根組件的數據
this.$root.name = "young monk"

// 訪問根組件的計算屬性
this.$root.getName

// 調用根組件的方法
this.$root.printName()

在練習或在有少量組件的小型應用中使用是非常方便的。但是在大型應用裏使用就會很複雜了。所以我們不推薦使用。

$parent:訪問父級組件實例

在子組件中,可以通過 $parent 訪問 父組件實例。這可以替代將數據以prop的方式傳入子組件的方式。如:

<div id="#app">
	<monk-parent>
		<monk-b>
			<monk-c></monk-c>
		</monk-b>
	</monk-parent>
</div>

monk-parent 需要共享一個屬性 name,它的所有子元素都需要訪問 name 屬性,在這種情況下 monk-b 可以通過 this.$parent.name 的方式訪問name。但是如果 monk-c 也想要區訪問name屬性,就需要先查看一下父組件中是否存在name,如果不存在,再向上級查找:

const name = this.$parent.name || this.$parent.$parent.name;

這樣做,很快組件就會失控:觸達父級組件會使應用更難調試和理解,尤其是當變更父組件數據時,過一段時間後,很難找出變更是從哪裏發起的。所以我也不推薦使用。

$children:訪問子組件實例

我們先創建好組件,一個父組件,父組件中包含子組件:

Vue.component('monk-parent', {
    data() {
        return { name:"parent monk" }
    },
    mounted(){
        console.log(this.$children[0].name)
    },
    template: ` <div><monk-child></monk-child></div>`
})
Vue.component('monk-child', {
    data(){
        return { name:"child monk" }
    },
    template: `<div>monk-child component</div>`
})

我們可以看到,我在父組件的mounted方法中,調用 this.$children 方法獲取到子組件的實例。因爲子組件有多個,所以 this.$children 返回的是一個實例數組,我們取到需要的實例,獲取裏面的數據即可

ref:訪問子組件實例或子元素

儘管存在prop和事件,但是有時候我們仍可能需要在JS裏直接訪問一個子組件,那麼此時,我們可以通過 ref 特性爲子組件賦予一個ID引用:

<monk-cmp ref="monkCmp"></monk-cmp>

這樣就可以通過this.$refs.monkCmp訪問<monk-cmp>實例。
ref 也可以 對指定DOM元素進行訪問,如:

<input ref="input" />

那麼,我們可以通過 this.$refs.input 來訪問到該DOM元素。

注意:當ref 和 v-for 一起使用時,得到的引用將會是一個包含了對應數據源的這些子組件的數組。$refs 只會在組件渲染完成之後生效,並且它們不是響應式的。應該避免在模板或計算屬性中訪問 $refs

我在項目開發中使用過這個方式,子組件是一個模態框,在事件中控制模態框的顯示和初始化:

show(data) {
    
    //一系列初始化操作...
    
    this.visible = true;
}

我們在父組件通過$refs 的方式來調用,如下:

this.$refs.model.show({name:"monk"})

provide 和 inject :依賴注入

在上面的例子中,利用 $parent 屬性,沒有辦法很好的擴展到更深層級的嵌套組件上。這也是依賴注入的用武之地,它用到了兩個新的實例選項:provideinject

provide:允許我們指定想要提供給後代組件的數據/方法,例如:

Vue.component('monk-parent', {
    provide(){
        return{
            name:this.name
        }
    },
    data() {
        return {
            name:"young monk"
        }
    },
    template: `<div><monk-b></monk-b></div>`
})

inject:在任何後代組件中,我們都可以使用 inject 選項來接受指定想要添加在實例上的屬性。

Vue.component('monk-b', {
   inject:["name"],
    template: `
        <div>
            {{ this.name }}
        </div>
    `
})

相比 $parent 來說,這個用法可以讓我們在任意後代組件中訪問 name屬性,而不需要暴露整個 monk-parent 實例。這允許我們更好的持續研發該組件,而不需要擔心我們可能會改變/移除一些子組件依賴的東西。同時這些組件之間的接口是始終明確定義的,就和 props 一樣。

實際上,你可以把依賴注入看作一部分“大範圍有效的 prop”,除了:

  • 祖先組件不需要知道哪些後代組件使用它提供的屬性
  • 後代組件不需要知道被注入的屬性來自哪裏

然而,依賴注入還是有負面影響的。它將你應用程序中的組件與它們當前的組織方式耦合起來,使重構變得更加困難。同時所提供的屬性是非響應式的。所以我還是不推薦使用

這樣就可以通過this.$refs.monkCmp訪問<monk-cmp>實例。
ref 也可以 對指定DOM元素進行訪問,如:

<input ref="input" />

那麼,我們可以通過 this.$refs.input 來訪問到該DOM元素。

注意:當ref 和 v-for 一起使用時,得到的引用將會是一個包含了對應數據源的這些子組件的數組。$refs 只會在組件渲染完成之後生效,並且它們不是響應式的。應該避免在模板或計算屬性中訪問 $refs

我在項目開發中使用過這個方式,子組件是一個模態框,在事件中控制模態框的顯示和初始化:

show(data) {
    
    //一系列初始化操作...
    
    this.visible = true;
}

我們在父組件通過$refs 的方式來調用,如下:

this.$refs.model.show({name:"monk"})

provide 和 inject :依賴注入

在上面的例子中,利用 $parent 屬性,沒有辦法很好的擴展到更深層級的嵌套組件上。這也是依賴注入的用武之地,它用到了兩個新的實例選項:provideinject

provide:允許我們指定想要提供給後代組件的數據/方法,例如:

Vue.component('monk-parent', {
    provide(){
        return{
            name:this.name
        }
    },
    data() {
        return {
            name:"young monk"
        }
    },
    template: `<div><monk-b></monk-b></div>`
})

inject:在任何後代組件中,我們都可以使用 inject 選項來接受指定想要添加在實例上的屬性。

Vue.component('monk-b', {
   inject:["name"],
    template: `
        <div>
            {{ this.name }}
        </div>
    `
})

相比 $parent 來說,這個用法可以讓我們在任意後代組件中訪問 name屬性,而不需要暴露整個 monk-parent 實例。這允許我們更好的持續研發該組件,而不需要擔心我們可能會改變/移除一些子組件依賴的東西。同時這些組件之間的接口是始終明確定義的,就和 props 一樣。

實際上,你可以把依賴注入看作一部分“大範圍有效的 prop”,除了:

  • 祖先組件不需要知道哪些後代組件使用它提供的屬性
  • 後代組件不需要知道被注入的屬性來自哪裏

然而,依賴注入還是有負面影響的。它將你應用程序中的組件與它們當前的組織方式耦合起來,使重構變得更加困難。同時所提供的屬性是非響應式的。所以我還是不推薦使用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章