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
}

如果你想要抛出多个值,那就只能使用定义函数的方式了,因为 e v e n t 访 event只能访问到 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”,除了:

  • 祖先组件不需要知道哪些后代组件使用它提供的属性
  • 后代组件不需要知道被注入的属性来自哪里

然而,依赖注入还是有负面影响的。它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的属性是非响应式的。所以我还是不推荐使用

发布了38 篇原创文章 · 获赞 535 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/Newbie___/article/details/105480827