09-Vue组件

一、什么是组件? 什么是组件化?

组件 (Component) 是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功能。在有些情况下,组件也可以表现为用 is 特性进行了扩展的原生 HTML 元素。

简单的说: 组件就是把一个很大的界面拆分为多个小的界面, 每一个小的界面就是一个组件
将大界面拆分成小界面就是组件化

组件系统让我们可以用独立可复用的小组件来构建大型应用,几乎任意类型的应用的界面都可以抽象为一个组件树:

image

组件化的好处
可以简化Vue实例的代码.
可以提高代码复用性

二、注册组件

全局注册

所有实例都能用全局组件。
1. 创建组件构造器
通过 全局API:Vue.extend()
参数:{Object} options
用法:使用基础 Vue 构造器,创建一个“子类”。参数是一个 包含组件选项的对象。

let Profile = Vue.extend({
    // 注意: 在创建组件指定组件的模板的时候, 模板只能有一个根元素
    template: `
        <div>
            <img src="images/fm.jpg" alt="">
            <p>我是描述信息</p>
        </div>
    `
});

2. 注册已经创建好的组件

Vue.component('my-component', Profile);

3. 使用注册好的组件

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

创建组件的简化方式

  • 在注册组件的时候, 除了传入一个组件构造器以外, 还可以直接传入一个 对象
Vue.component('my-component', {
    template: `
        <div>
            <img src="images/fm.jpg" alt="">
            <p>我是描述信息</p>
        </div>
    `
});
  • 在编写组件模板的时候, 除了可以在 字符串模板 中编写以外, 还可以像 art-template 一样在 script 中编写
<script id="info" type="text/html">
    <div>
        <img src="images/fm.jpg" alt="">
        <p>我是描述信息</p>
    </div>
</script>
  • 在编写组件模板的时候, 除了可以在 script 中编写以外, vue还专门提供了一个编写模板的标签 template
<template id="info">
    <div>
        <img src="images/fm.jpg" alt="">
        <p>我是描述信息</p>
    </div>
</template>

上面两种编写模板的方式在创建的时候一定要记得加上 id, 在使用的时候也要加上 id名称

Vue.component('my-component', {
    template: '#info'
});


局部注册

我们也可以在实例选项中注册局部组件,这样组件只能在这个实例中使用.
可以通过某个 Vue 实例/组件的实例选项 components 注册仅在其作用域中可用的组件:

new Vue({
    // ...
    components: {
        'my-component': {
            template: '#info'
        }
    }
});

自定义全局组件特点:
在任何一个Vue实例控制的区域中都可以使用
自定义局部组件特点:
只能在自定义的那个Vue实例控制的区域中才可以使用

三、组件中的data和methods

Vue实例控制的区域相当于一个大的组件, 在大组件中我们可以使用datamethods
而我们自定义的组件也是一个组件, 所以在自定义的组件中也能使用datamethods

1. Vue中使用data和methods

<div id="app">
    <button @click="vueFn">vue-alert</button>
    <p>{{vueMsg}}</p>
</div>

new Vue({
    el: '#app',
    methods: {
        vueFn(){
            alert('vue-Fn');
        }
    },
    data: {
        vueMsg: 'vue-Msg'
    }
});

2. 自定义组件中使用data和methods

在自定义组件中不能像在vue实例中一样直接使用data,而是必须通过返回函数的方式来使用data。
<template id="info">
    <div>
        <button @click="myFn">my-alert</button>
        <p>{{myMsg}}</p>
    </div>
</template>

Vue.component('my-component', {
    template: '#info',
    methods: {
        myFn(){
            alert('my-Fn');
        }
    },
    data: function () {
        return {
            myMsg: 'my-Msg'
        }
    }
});

自定义组件中的data为什么是一个函数
因为自定义组件可以复用, 为了保证复用时每个组件的数据都是独立的, 所以必须是一个函数
看下面这个例子:

// HTML
<div id="app">
    <my-component></my-component>
    <my-component></my-component>
    <my-component></my-component>
</div>
<template id="info">
    <div>
        <button @click="add">增加</button>
        <p>{{counter}}</p>
    </div>
</template>
// JS
Vue.component('my-component', {
    template: '#info',
    data: function () {
        return {
            counter: 0
        }
    },
    methods: {
        add(){
            this.counter++;
        }
    }
});
new Vue({
    el: '#app',
});

运行结果: 点击按钮的时候只有自己按钮下的数据会加1

image

组件中的data如果不是通过函数返回的, 那么多个组件就会共用一份数据, 就会导致数据混乱。
组件中的data如果通过函数返回的, 那么每创建一个新的组件, 都会调用一次这个方法,将这个方法返回的数据和当前创建的组件绑定在一起, 这样就有效的避免了数据混乱。
如果 Vue 没有这条规则,点击一个按钮就会像影响到其它所有实例:那么上面的例子中的数据就会一起加1;

四、组件切换

1. 通过 v-if / v-else

对于普通的元素我们可以通过v-if来实现切换,对于组件我们也可以通过v-if来实现切换。
因为组件的本质就是一个自定义元素。

// HTML
<div id="app">
    <button @click="show=!show">toggle</button>
    <my-component1 v-if="show"></my-component1>
    <my-component2 v-else></my-component2>
</div>

<template id="info1">
    <div>
        <p>我是info1</p>
    </div>
</template>
<template id="info2">
    <div>
        <p>我是info2</p>
    </div>
</template>
<script>
    Vue.component('my-component1', {
        template: '#info1'
    });
    Vue.component('my-component2', {
        template: '#info2'
    });
    new Vue({
        el: '#app',
        data: {
            show: true
        }
    });
</script>

2. 通过动态组件

通过v-if/v-else-if/v-else确实能够切换组件,但是在Vue中切换组件还有一种更专业的方式:

<component v-bind:is="需要显示组件名称"></component>

component我们称之为动态组件, 也就是你让我显示谁我就显示谁
通过使用 <component> 元素,动态地绑定到它的 is 特性

<div id="app">
    <button @click="toggle">toggle</button>
    <component :is="name"></component>
</div>

<template id="info1">
    <div>
        <p>我是info1</p>
    </div>
</template>
<template id="info2">
    <div>
        <p>我是info2</p>
    </div>
</template>
<script>
    Vue.component('my-component1', {
        template: '#info1'
    });
    Vue.component('my-component2', {
        template: '#info2'
    });
    new Vue({
        el: '#app',
        data: {
            show: true,
            name: 'my-component1'
        },
        methods: {
            toggle() {
                this.show = !this.show;
                this.name = this.name === 'my-component1' ? 'my-component2' : 'my-component1';
            }
        }
    });
</script>

为什么可以通过v-if切换还要有component
因为component可以配合keep-alive来保存被隐藏组件隐藏之前的状态, 而v-if会重新渲染页面, 所以不能保存之前的状态
<keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 <transition> 相似,<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中

<keep-alive>
    <component :is="name"></component>
</keep-alive>
<template id="info1">
    <div>
        <input type="checkbox">
        <p>我是info1</p>
    </div>
</template>

这里可以记住复选框的选中状态

如果我们需要频繁的切换页面,每次都是在组件的创建和销毁的状态间切换,这无疑增大了性能的开销。
这个时候我们也可以使用Vue提供了动态组件的 缓存。keep-alive 会在切换组件的时候缓存当前组件的状态,等到再次进入这个组件,不需要重新创建组件,只需要从前面的缓存中读取并渲染。

五、组件动画

给组件添加动画和过去给元素添加动画一样。
如果是单个组件就使用transition,如果是多个组件就使用transition-group

// HTML
<div id="app">
    <button @click="toggle">toggle</button>
    <transition>
        <component :is="name"></component>
    </transition>
</div>
<template id="info1">
    <div>
        <p>我是info1</p>
    </div>
</template>
<template id="info2">
    <div>
        <p>我是info2</p>
    </div>
</template>
// CSS
.v-enter{
    opacity: 0;
    margin-left: 500px;
}
.v-enter-to{
    opacity: 1;
}
.v-enter-active{
    transition: all 3s;
}
.v-leave{
    opacity: 1;
}
.v-leave-to{
    opacity: 0;
}
.v-leave-active{
    transition: all 3s;
    margin-left: 500px;
}
// JS
<script>
    Vue.component('my-component1', {
        template: '#info1'
    });
    Vue.component('my-component2', {
        template: '#info2'
    });
    new Vue({
        el: '#app',
        data: {
            show: true,
            name: 'my-component1',
        },
        methods: {
            toggle(){
                this.show = !this.show;
                this.name = this.name === 'my-component1' ? 'my-component2' : 'my-component1';
            }
        }
    });
</script>

效果:

image

在这个案例中可以发现一个问题: 两个的话是同时执行的.
默认情况下进入动画和离开动画是同时执行的, 如果想一个做完之后再做另一个, 需要指定动画的过渡模式.

过渡模式

过渡模式常常配合多个元素或者多个组件切换时使用,有如下两种模式:

  • in-out:新元素先进行过渡,完成之后当前元素过渡离开。(默认为该模式)
  • out-in:当前元素先过渡离开,离开完成后新元素过渡进入。

所以可以把上面的代码改造一下, 就可以让一个元素先出去, 另一个元素再进来

<transition mode='out-in'>
    <component :is="name"></component>
</transition>

六、父子组件

在一个组件中又定义了其它组件就是父子组件。
其实局部组件就是最简单的父子组件, 因为我们可以把Vue实例看做是一个大组件。
我们在Vue实例中定义了局部组件, 就相当于在大组件里面定义了小组件, 所以局部组件就是最简单的父子组件

1. 如何定义父子组件

前面讲过, 自定义组件中可以使用data, 可以使用methods. 当然自定义组件中也可以使用components
所以我们也可以在自定义组件中再定义其它组件。

  • 在全局父组件中定义子组件
Vue.component('father', {
    template: '#father',
    components: {
        'son': {
            template: '#son'
        }
    }
});
  • 在局部父组件中定义子组件
new Vue({
    el: '#app',
    components: {
        'father': {
            template: '#father',
            components: {
                'son': {
                    template: '#son'
                }
            }
        }
    }
});
  • 父子组件的使用
    把自定义子组件放到自定义父组件中, 把自定义父组件放到Vue组件中
<div id="app">
    <father></father>
</div>

<template id="father">
    <div>
        <p>我是父组件</p>
        <son></son>
    </div>
</template>
<template id="son">
    <div>
        <p>我是子组件</p>
    </div>
</template>

2. 父子组件数据传递

在Vue中子组件是不能访问父组件的数据的,如果子组件想要访问父组件的数据, 必须通过父组件传递。

如何传递数据

  • 在父组件中通过v-bind传递数据
    传递格式: v-bind:自定义接收名称 = "要传递数据"
<template id="father">
    <div>
        <p>{{msg}}</p>
        <!--这里将父组件的msg通过parentmsg传递给了子组件-->
        <son :parentmsg="msg"></son>
    </div>
</template>
  • 在子组件中通过props接收数据
    接收格式: props: ["自定义接收名称"]
//...
components: {
    'son': {
        template: '#son',
        // 这里通过parentmsg接收了父组件传递过来的数据
        props: ['parentmsg']
    }
}

如何使用数据

  • 子组件使用父组件传递的数据
<template id="son">
    <div>
        <!--这里通过parentmsg使用了父组件传递过来的数据-->
        <p>{{parentmsg}}</p>
    </div>
</template>

3. 父子组件方法传递

在Vue中子组件是不能访问父组件的方法的,如果子组件想要访问父组件的方法, 必须通过父组件传递

如何传递方法

  • 在父组件中通过 v-on 传递方法
    传递格式: v-on:自定义接收名称 = "要传递方法"
<template id="father">
    <div>
        <button @click="say">父组件按钮</button>
        <!--这里通过parentsay将父组件的say方法传递给了子组件-->
        <son @parentsay="say"></son>
    </div>
</template>
  • 在子组件中自定义一个方法
<template id="son">
    <div>
        <button @click="sonFn">子组件按钮</button>
    </div>
</template>
  • 在自定义方法中通过 this.$emit('自定义接收名称'); 触发传递过来的方法
components: {
    'son': {
        template: '#son',
        methods: {
            sonFn(){
                this.$emit('parentsay');
            }
        }
    }
}

和传递数据不同, 如果传递的是方法, 那么在子组件中不需要接收。
但是需要在子组件中自定义一个方法, 直接使用自定义的方法.
并且还需要在子组件自定义的方法中通过
this.$emit("自定义接收的名称")的方法来触发父组件传递过来的方法

$emit( eventName, […args] ) 触发事件

  • {string} eventName 需要调用的函数名称
  • [...args] 给调用的函数传递的参数

触发当前实例上的事件。附加参数都会传给监听器回调。
所以子组件可以通过这个方法给父组件传递参数.

components: {
    'son': {
        template: '#son',
        methods: {
            sonFn(){
                this.$emit('parentsay', 'son');
            }
        }
    }
}

父组件接收参数:

methods: {
    say(data){
        console.log(data);
    }
}

4. 数据和方法的多级传递

在Vue中如果儿子想使用爷爷的数据, 必须一层一层往下传递
在Vue中如果儿子想使用爷爷的方法, 必须一层一层往下传递

七、组件中的命名

1. 注册组件的时候使用了"驼峰命名", 那么在使用时需要转换成"短横线分隔命名"
例如: 注册时: myFather -> 使用时: my-father

2. 在传递参数的时候如果想使用"驼峰名称", 那么就必须写"短横线分隔命名"
例如: 传递时: parent-msg="msg" -> 接收时: props: ["parentMsg"]

3. 在传递参数的时候如果想使用"驼峰名称", 那么就必须写"短横线分隔命名"
例如: @parent-say="say" -> this.$emit("parent-say");

八、插槽

默认情况下使用子组件时,在子组件中编写的元素是不会被渲染的
如果子组件中有部分内容是使用时才确定的, 那么我们就可以使用插槽
插槽就是在子组件中放一个"坑", 以后由父组件来"填"。

比如在下面这个例子中, 没有使用插槽的话父组件在<son></son>内编写的内容是无效的

<template id="father">
    <div>
        <son>
            <!--如果没有插槽, 父组件在这里编写的内容是无效的-->
        </son>
    </div>
</template>
默认情况下是不能在使用子组件的时候, 给子组件动态的添加内容的  
如果想在使用子组件的时候, 给子组件动态的添加内容, 那么就必须使用插槽

1. 匿名插槽

没有名字的插槽, 会利用使用时指定的内容替换整个插槽

这里的slot标签就是插槽, 插槽其实就是一个坑。只要有了这个坑, 那么以后使用者就可以根据自己的需要来填这个坑。

<template id="son">
    <div>
        <div>我是头部</div>
        <slot>我是默认的内容</slot>
        <div>我是底部</div>
    </div>
</template>

插槽可以指定默认数据, 如果使用者没有填这个坑, 那么就会显示默认数据。
如果使用者填了这个坑, 那么就会利用使用者坑的内容替换整个插槽。

例如: 这个father组件在使用son组件的时候填了这个坑,那么就会用父组件坑的内容覆盖掉整个插槽.
所以最后的效果是:

image
<template id="father">
    <div>
        <!--需求: 在使用子组件的时候给子组件动态的添加一些内容-->
        <son>
            <div>我是追加的内容</div>
        </son>
    </div>
</template>

匿名插槽的特点:
有多少个匿名插槽, 填充的数据就会被拷贝几份

<template id="son">
    <div>
        <div>我是头部</div>
        <slot>我是默认的内容</slot>
        <slot>我是默认的内容</slot>
        <div>我是底部</div>
    </div>
</template>

效果图:

image

虽然我们可以指定多个匿名插槽, 但是推荐只写一个匿名插槽

2. 具名插槽

默认情况下有多少个匿名插槽, 我们填充的数据就会被拷贝多少份,这导致了所有插槽中填充的内容都是一样的。
那么如果我们想给不同的插槽中填充不同的内容怎么办呢?
这个时候就可以使用具名插槽

具名插槽的使用

  • 通过插槽的name属性给插槽指定名称
<template id="son">
    <div>
        <div>我是头部</div>
        <slot name="one">我是one默认的内容</slot>
        <slot name="two">我是two默认的内容</slot>
        <div>我是底部</div>
    </div>
</template>
  • 在使用时可以通过 slot="name" 方式, 指定当前内容用于替换哪一个插槽
    默认情况下填充的内容是不会被填充到具名插槽中的,
    只有给填充的内容指定了要填充到哪一个具名插槽之后,
    才会将填充的内容填充到具名插槽中
<template id="father">
    <div>
        <son>
            <!--这里通过slot属性告诉Vue,当前的内容是要填充到哪一个插槽中的-->
            <div slot="one">我是追加的内容one</div>
            <!--如果没有指定要替换哪个插槽中的内容, 则不会被替换-->
            <div>我是追加的内容two</div>
        </son>
    </div>
</template>

3. v-slot指令

v-slot指令是Vue2.6中用于替代slot属性的一个指令
在Vue2.6之前, 我们通过slot属性告诉Vue当前内容填充到哪一个具名插槽
从Vue2.6开始, 我们通过v-slot指令告诉Vue当前内容填充到哪一个具名插槽

格式: v-slot:插槽名称
简写: #插槽名称

注意: v-slot指令只能用在template标签上

例如:

<son>
    <template v-slot:one>
        <div>我是追加的内容one</div>
        <div>我是追加的内容one</div>
    </template>
    <template #two>
        <div>我是追加的内容two</div>
        <div>我是追加的内容two</div>
    </template>
</son>

4. 作用域插槽

作用域插槽就是带数据的插槽, 就是让父组件在填充子组件插槽内容时也能使用子组件的数据

如何使用作用域插槽

  • slot中通过 v-bind:数据名称="数据名称" 方式暴露数据
<template id="son">
    <div>
        <div>我是头部</div>
        <!--v-bind:names="names"作用: 将当前子组件的names数据暴露给父组件-->
        <slot v-bind:names="names"></slot>
        <div>我是底部</div>
    </div>
</template>
  • 在父组件中通过 <template slot-scope="作用域名称"> 接收数据
<template id="father">
    <div>
        <son>
            <!--slot-scope="sonMsg"作用: 接收子组件插槽暴露的数据-->
            <template slot-scope="sonMsg">
            </template>
        </son>
    </div>
</template>
  • 在父组件的 <template></template> 中通过 作用域名称.数据名称 方式使用数据
<template slot-scope="sonMsg">
    <div>{{sonMsg.names}}</div>
</template>

作用域插槽的应用场景:
子组件提供数据, 父组件决定如何渲染

5. v-slot 指令代替 slot-scope

在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。
它取代了 slot 和 slot-scope

也就是说我们除了可以通过 v-slot 指令告诉Vue内容要填充到哪一个具名插槽中,还可以通过v-slot指令告诉Vue如何接收作用域插槽暴露的数据

格式: v-slot:插槽名称="作用域名称"
简写: #插槽名称="作用域名称"

<!--匿名插槽的默认名称是default-->
<template v-slot:default="sonMsg">
    <div>{{sonMsg.names}}</div>
</template>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容

  • 什么是组件? 组件 (Component) 是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装...
    youins阅读 9,470评论 0 13
  • 此文基于官方文档,里面部分例子有改动,加上了一些自己的理解 什么是组件? 组件(Component)是 Vue.j...
    陆志均阅读 3,813评论 5 14
  • 三、组件 组件 (Component) 是 Vue.js 最强大的功能之一。组件可以扩展 HTML元素,封装可重用...
    小山居阅读 601评论 0 1
  • 【2019-3-4更新】Vue 2.6+修改了部分语法,对插槽的使用有了较多的更新。在本文中笔者在相应位置给出了更...
    果汁凉茶丶阅读 10,237评论 2 36
  • Vue 实例 属性和方法 每个 Vue 实例都会代理其 data 对象里所有的属性:var data = { a:...
    云之外阅读 2,204评论 0 6