组件 (Component) 是 Vue.js 最核心的功能,也是整个框架设计最精彩的部分,当然也是最难掌握的。

组件与复用

组件用法

组件与创建 Vue 实例类似,需要注册后才可以使用。注册有全局注册和局部注册两种方式。全局注册后任何 Vue 实例都可以使用。

全局注册

全局注册示例代码如下:

Vue.component('my-component', {
//选项
})

my-component 就是注册的组件自定义名称,推荐使用小写加减号分隔的形式命名。

要在父实例中使用这个组件,必须要在实例创建前注册,之后就可以用 <my-component> 的形式使用组件了,实例代码如下:

<div id="app">
<my-component></my-component>
</div>
<script>
Vue.component('my-component', {
//选项
}); var app = new Vue({
el: '#app'
})
</script>

此时打开页面是空的,因为我们注册的组件没有任何内容,需要在组件选项中添加 template 选项,例如:

Vue.component('my-component', {
template: '<div>这里是组件内容</div>'
});

渲染后的结果是:

<div id="app">
<div>这里是组件内容</div>
</div>

template 的DOM结果必须被一个元素包含,如果这里直接写成 “这里是组件的内容”,不带 "<div></div>" 是无法渲染的。

局部注册

在 Vue 实例中,使用 components 选项可以局部注册组件,注册后的组件只有在该实例作用于下有效,组件中也可以使用 components 选项来注册组件,使组件也可以嵌套,例如:

<div id="app">
<my-component></my-component>
</div>
<script>
var child = {
template: '<div>局部注册组件的内容</div>'
} var app = new Vue({
el: '#app',
components: {
'my-component': child
}
})
</script>

is属性

Vue 组件的模板在某些情况下会受到 HTML 的限制,比如 <table> 只允许 <tr> <td> <th> 等这些表格元素,所以在 <table> 内直接使用组件是无效的。这种情况下,可以使用特殊的 is 属性来挂载组件,例如:

<div id="app">
<table>
<tbody is="my-component"></tbody>
</table>
</div>
<script>
Vue.component('my-component', {
template: '<div>这里是组件的内容</div>'
}); var app = new Vue({
el: '#app',
})
</script>

tbody 在渲染时,会被替换成组件的内容。常见的限制元素还有 <ul> <ol> <select> 。

data选项

除了 template 选项,组件中还可以像 Vue 实例那样使用其他的选项,比如 data、computed、methods 等。但是在使用 data 时,和实例稍有区别,data 必须是函数,然后将数据 return 出去,例如:

<div id="app">
<table>
<tbody is="my-component"></tbody>
</table>
</div>
<script>
Vue.component('my-component', {
template: '<div>{{ message }}</div>',
data: function () {
return {
message: '组件内容'
}
}
}); var app = new Vue({
el: '#app',
})
</script>

使用 props 传递数据

基本用法

组件不仅仅是要把模板的内容复用,更重要的是组件间进行通信。父组件要正向地向子组件传递数据或参数,这个正向传递数据的过程就是通过 props 来实现的。

在组件中使用 props 来声明需要从父组件接收的数据,props 的值可以是两种,一种是字符串数组,一种是对象,先介绍数组的用法,构造一个数组,接收一个来自父级的数据 message,并在模板中渲染,例如:

<div id="app">
<my-component message="来自父组件的内容"></my-component>
</div>
<script>
Vue.component('my-component', {
props: ['message'],
template: '<div>{{ message }}</div>', }); var app = new Vue({
el: '#app',
})
</script>

props 中声明的数据与组件 data 函数 return 的数据主要区别就是 props 的来自父级,而 data 中的是组件自己的数据,作用域是组件本身,这两种数据都能在模板 template 及计算属性 computed 和方法 methods 中使用。

由于 HTML 特性不区分大小写,当使用 DOM 模板时,驼峰命名的 props 命名要转为短横分隔命名,例如:

<div id="app">
<my-component warning-text ="提示信息"></my-component>
</div>
<script>
Vue.component('my-component', {
props: ['warningText'],
template: '<div>{{ warningText }}</div>',
}); var app = new Vue({
el: '#app',
})
</script>

有时候,传递的数据并不是直接写死的,而是来自父级的动态数据,这时可以使用指令 v-bind 来动态绑定 props 的值,当父组件的数据变化时,也会传递给子组件。例如:

<div id="app">
<input type="text" v-model="parentMessage">
<my-component :message ="parentMessage"></my-component>
</div>
<script>
Vue.component('my-component', {
props: ['message'],
template: '<div>{{ message }}</div>',
}); var app = new Vue({
el: '#app',
data: {
parentMessage: ''
}
})
</script>

注意:如果你要直接传递数字,布尔值,数组,对象,但不使用 v-bind,传递的仅仅是字符串,例如:

<div id="app">
<my-component message ="[1,2,3]"></my-component>
<my-component :message ="[1,2,3]"></my-component>
</div>
<script>
Vue.component('my-component', {
props: ['message'],
template: '<div>{{ message.length }}</div>',
}); var app = new Vue({
el: '#app',
data: {
parentMessage: ''
}
})
</script>

同一个组件使用了两次,区别是第二个使用了 v-bind,第一个是 7,第二个是 3。

单向数据流

Vue 2.x 通过 props 传递数据是单向的了,也就是父组件数据变化时会传递给子组件,但是反过来不行。

业务会经常遇到两种需要改变 prop 的情况,一种是父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改。这种情况下可以在 data 内再声明一个数据,引用父组件的 prop,例如:

<div id="app">
<my-component :init-count="1"></my-component>
</div>
<script>
Vue.component('my-component', {
props: ['initCount'],
template: '<div>{{ count }}</div>',
data: function () {
return {
count: this.initCount
}
}
}); var app = new Vue({
el: '#app',
})
</script>

另一种情况就是 prop 作为需要被转变的原始值传入,这种情况下用计算属性就可以了,例如:

<div id="app">
<my-component :size="50"></my-component>
</div>
<script>
Vue.component('my-component', {
props: ['size'],
template: '<div :style = "style">组件内容</div>',
computed: {
style: function () {
return {
fontSize: this.size + 'px'
}
}
}
}); var app = new Vue({
el: '#app',
})
</script>

注意:在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,所以 prop 是对象和数组时,在子组件内改变是会影响父组件的。

数据验证

上面所介绍的 props 选项都是一个数组,当 prop 需要验证时,就需要对象写法。

一般当你的组件需要提供给别人使用时,推荐使用数据验证,比如某个数据必须是数字类型,如果传入字符串,就会在控制台弹出警告。

以下是几个 prop 的示例:

Vue.component('my-component', {
props: {
//必须是数字类型
propA: Number,
//必须是字符串或数字类型
propB: [String, Number],
//布尔值,默认为true
propC: {
type: Boolean,
default: true
},
//数字,而且是必传
propD: {
type: Number,
required: true
},
//如果是数组或对象,默认值必须用一个函数返回
propE: {
type: Array,
default: function () {
return [];
}
},
//自定义一个验证函数
propF: {
validator: function (value) {
return value > 10;
}
}
}
});

验证的 type类型可以是:

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

type 也可以是一个自定义构造器,使用 instanceof 检测。

当 prop 验证失败时,在开发版本下会在控制台抛出一天警告。

组件通信

我们已经知道,父组件向子组件通信,通过 props 传递数据就可以,但 Vue 组件通信的场景不止有这一种,归纳起来,组件通信可以用户下图表示:

组件关系可分为父子组件通信,兄弟组件通信,跨级组件通信。

父子组件通信

自定义事件

当子组件需要向父组件传递数据时,就需要用到自定义事件。子组件用 $emit() 来触发事件,父组件用 $on() 来监听子组件的事件。

父组件可以直接在子组件的自定义标签上使用 v-on 来监听子组件的自定义事件,例如:

<div id="app">
<p>总数:{{ total }}</p>
<my-component @increase="getTotal" @reduce="getTotal">
</my-component>
</div>
<script>
Vue.component('my-component', {
template: '\
<div>\
<button @click="handleIncrease">+1</button>\
<button @click="handleReduce">-1</button>\
</div>',
data: function () {
return {
count: 0
}
},
methods: {
handleIncrease: function () {
this.count++;
this.$emit('increase', this.count);
},
handleReduce: function () {
this.count--;
this.$emit('reduce', this.count);
}
}
}); var app = new Vue({
el: '#app',
data: {
total: 0
},
methods: {
getTotal: function (total) {
this.total = total;
}
}
})
</script>

除了用 v-on 在组件上监听自定义组件,也可以监听 DOM 事件,这时可以用 .native 修饰符表示监听一个原生事件,监听的是该组件的根元素,例如:

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

使用 v-model

Vue 2.x 可以在自定义组件上使用 v-model 指令,例如:

<div id="app">
<p>总数:{{ total }}</p>
<my-component v-model = "total"></my-component>
</div>
<script>
Vue.component('my-component', {
template: '<button @click="handleClick">+1</button>',
data: function () {
return {
count: 0
}
},
methods: {
handleClick: function () {
this.count++;
this.$emit('input', this.count);
},
}
}); var app = new Vue({
el: '#app',
data: {
total: 0
}
})
</script>

仍然是点击按钮加 1 点效果,不过这次的 $emit() 的事件名是特殊的 input,在使用组件的父级并没有在 <my-component> 上使用 @input="hander",而是直接用了 v-model 绑定一个数据 total。这也可以称作是一个语法糖,因为上面的示例可以间接地用自定义事件实现:

<div id="app">
<p>总数:{{ total }}</p>
<my-component @input="getTotal"></my-component>
</div>
<script>
Vue.component('my-component', {
template: '<button @click="handleClick">+1</button>',
data: function () {
return {
count: 0
}
},
methods: {
handleClick: function () {
this.count++;
this.$emit('input', this.count);
},
}
}); var app = new Vue({
el: '#app',
data: {
total: 0
},
methods: {
getTotal: function (total) {
this.total = total;
}
}
})
</script>

v-model 还可以用来创建自定义的表单输入组件,进行数据双向绑定,例如:

<div id="app">
<p>总数:{{ total }}</p>
<my-component v-model="total"></my-component>
<button @click="handleReduce">-1</button>
</div>
<script>
Vue.component('my-component', {
props: ['value'],
template: '<input :value="value" @input="updateValue">',
methods: {
updateValue: function (event) {
this.$emit('input', event.target.value);
},
}
}); var app = new Vue({
el: '#app',
data: {
total: 0
},
methods: {
handleReduce: function () {
this.total--;
}
}
})
</script>

实现这样一个具有双向绑定的 v-model 组件要满足下面两个要求:

  • 接收一个 value 属性。
  • 在有新的 value 时触发 input 事件。

非父子组件通信

在实际业务中,除了父子组件通信外,还有很多非父子组件通信的场景,非父子组件一般有两种,喜爱兄弟组件和跨多级组件。

中央事件总线

在 Vue.js 2.x 中,推荐使用一个空的 Vue 实例作为中央事件总线(bus),也就是一个中介。例如:

<div id="app">
{{ message }}
<component-a></component-a>
</div>
<script>
var bus = new Vue(); Vue.component('component-a', {
template: '<button @click="handleEvent">传递事件</button>',
methods: {
handleEvent: function () {
bus.$emit('on-message', '来自组件 component-a 的内容');
},
}
}); var app = new Vue({
el: '#app',
data: {
message: ''
},
mounted: function () {
var _this = this;
//在实例初始化时,监听来自 bus 实例的事件
bus.$on('on-message', function (msg) {
_this.message = msg;
});
}
})
</script>

父链

在子组件中,使用 this.\(parent 可以直接访问该组件的父实例或组件,父组件也可以通过this.\)children 访问它所有的子组件,而且可以递归自上而下无线访问,直到根实例或最内层的组件。例如:

<div id="app">
{{ message }}
<component-a></component-a>
</div>
<script>
Vue.component('component-a', {
template: '<button @click="handleEvent">传递事件</button>',
methods: {
handleEvent: function () {
//访问到父链后,可以做任何操作
this.$parent.message = '来自组件 component-a 的内容';
},
}
}); var app = new Vue({
el: '#app',
data: {
message: ''
}
})
</script>

尽管 Vue 允许这么做,但在业务中,子组件应该尽可能地避免依赖父组件的数据,更不应该去主动修改它的数据,因为这样使得父子组件紧耦合,很难理解父组件的状态,因为它可能被任意组件修改,理想情况下,只要组件自己能修改它的状态,父子组件最好还是通过 props 和 $emit 来通信。

子组件索引

当子组件较多时,通过 this.$children 来一一遍历我们需要的一个组件实例是比较困难的,尤其在组件动态渲染时,他们的序列是不固定的。Vue 提供了子组件的索引的方法,用特殊的属性 ref 来为子组件指定一个索引名称,例如:

<div id="app">
{{ message }}
<button @click="handleRef">通过 ref 获取子组件实例</button>
<component-a ref="comA"></component-a>
</div>
<script>
Vue.component('component-a', {
template: '<div>子组件</div>',
data: function () {
return {
message: '子组件内容'
}
}
}); var app = new Vue({
el: '#app',
data: {
message: ''
},
methods: {
handleRef: function () {
this.message = this.$refs.comA.message;
}
}
})
</script>

注意:$ref 只在组件渲染完成后才填充,并且它是非响应式的。它仅仅作为一个直接访问子组件的应急方案,应当避免在模板或计算属性中使用 $refs。

使用 solt 分发内容

什么是 slot

很多网站由一级导航、二级导航、左侧列表、正文、以及底部版权信息 5 个模块组成,如果要将他们都组件化,这个结构可能会是:

<app>
<menu-main></menu-main>
<menu-sub></menu-sub>
<div class="container">
<menu-left></menu-left>
<container></container>
</div>
<app-footer></app-footer>
</app>

当需要让组件组合使用,混合父组件的内容与子组件的模板时,就会用到 slot,这个过程叫做内容分发。以 <app> 为例,它有两个特点:

  • <app> 组件不知道它的挂载点会有什么内容。挂载点的内容是由 <app> 的父组件决定的。
  • <app> 组件很可能有自己的组件。

props 传递数据、events 触发事件和 slot 内容分发就构成了 Vue 组件的 3 个 API 来源,再复杂的组件也是由这 3 部分构成的。

作用域

正式介绍 slot 前,需要先知道一个概念:编译的作用域。例如:

<child-component>
{{ message }}
</child-component>

这里的 message 就是一个 slot,但是它绑定的是父组件的数据,而不是组件 <child-component> 的数据。

父组件的模板的内容是在父组件作用域编译,子组件模板的内容是在子组件作用域内编译。

注意:slot分发的内容,作用域是在父组件上的。

solt用法

单个 Slot

在子组件内使用特殊的 <slot> 元素就可以为这个子组件开启一个 slot(插槽),在父组件模板里,插入在子组件标签内的所有内容将替代 <slot> 标签及它的内容。例如:

<div id="app">
<child-component>
<p>分发的内容</p>
<p>更多分发的内容</p>
</child-component>
</div>
<script>
Vue.component('child-component', {
template: '\
<div>\
<slot>\
<p>如果父组件没有插入内容,我将作为、默认出现</p>\
</slot>\
</div>'
}); var app = new Vue({
el: '#app',
})
</script>

渲染完后的结果为:

<div id="app">
<div>
<p>分发的内容</p>
<p>更多分发的内容</p>
</div>
</div>

注意:子组件 <slot> 内的备用内容,它的作用是子组件本身。

具名Slot

给 <slot> 元素指定一个 name 后可以分发多个内容,具名 Slot 可以与单个Slot 共存,例如:

<div id="app">
<child-component>
<h2 slot="header">标题</h2>
<p>分发的内容</p>
<p>更多的正文内容</p>
<div slot="footer">底部信息</div>
</child-component>
</div>
<script>
Vue.component('child-component', {
template: '\
<div class="container">\
<div class="header">\
<slot name="header"></slot>\
</div>\
<div class="main">\
<slot></slot>\
</div>\
<div class="footer">\
<slot name="footer"></slot>\
</div>\
</div>'
}); var app = new Vue({
el: '#app',
})
</script>

子组件里声明了 3 个 <slot> 元素,其中 <div class="main"> 内的 <slot> 没有使用 name 特性,它将作为默认 slot 出现,父组件没有使用 slot 特性的元素和内容都将出现在这里。如果没有指定

默认匿名的 slot,父组件内多余的内容片断都将被抛弃。

上面最终的渲染结结果为:

<div class="container">
<div class="header">
<h2>标题</h2>
</div>
<div class="main">
<p>分发的内容</p>
<p>更多的正文内容</p>
</div>
<div class="footer">
<div>底部信息</div>
</div>
</div>

作用域插槽

作用域插槽是一种特殊的 slot,使用一个可以复用的模板替换已渲染元素,概念比较难以理解,先看一个简单的示例来了解它的基本用法。例如:

<div id="app">
<child-component>
<template scope="props">
<p>来自父组件的内容</p>
<p>{{ props.msg }}</p>
</template>
</child-component>
</div>
<script>
Vue.component('child-component', {
template: '\
<div class="container">\
<slot msg="msg"></slot>\
</div>',
data: function () {
return {
msg: '来自子组件的内容'
}
}
}); var app = new Vue({
el: '#app',
})
</script>

在 <slot> 元素上有一个类似 props 传递数据给组件的写法 msg=“xxx”,将数据传入到了插槽。这里的 props 只是一个临时变量,就像 v-for="item in items" 里面的 item 一样。template 内可以通过临时变量 props 访问来自子组件的数据 msg。

上面渲染后的最终结果为:

<div id="app">
<div class=="container">
<p>来自父组件的内容</p>
<p>来自子组件的内容</p>
</div>
</div>

作用域插槽更具代表性的用例是列表组件,允许组件自定义应该如何渲染列表每一项。例如:

<div id="app">
<my-list :books="books">
<!-- 作用域插槽也可以是具名的 Slot -->
<template slot="book" scope="props">
<li>{{ props.bookName }}</li>
</template>
</my-list>
</div>
<script>
Vue.component('my-list', {
props: {
books: {
type: Array,
default: function () {
return [];
}
}
},
template: '\
<ul>\
<slot name="book" v-for="book in books" \
:book-name="book.name">\
<!--这里也可以写默认的 slot 内容-->\
</slot>\
'
}); var app = new Vue({
el: '#app',
data: {
books: [
{ name: 'Java编程思想'},
{ name: 'Java核心技术'},
{ name: 'JS 语言精粹'},
]
}
})
</script>

访问 slot

Vue.js 2.x 提供了用来访问被 slot 分发的内容的方法 $slots,例如:

div id="app">
<child-component>
<h2 slot="header">标题</h2>
<p>分发的内容</p>
<p>更多的正文内容</p>
<div slot="footer">底部信息</div>
</child-component>
</div>
<script>
Vue.component('child-component', {
template: '\
<div class="container">\
<div class="header">\
<slot name="header"></slot>\
</div>\
<div class="main">\
<slot></slot>\
</div>\
<div class="footer">\
<slot name="footer"></slot>\
</div>\
</div>',
mounted: function () {
var header = this.$slots.header;
var main = this.$slots.default;
var footer = this.$slots.footer;
console.log(footer);
console.log(footer[0].elm.innerText);
}
}); var app = new Vue({
el: '#app',
})
</script>

$slot 在业务中几乎用不到,在用 render 函数创建组件时会比较有用,但主要还是用于独立组件开发中。

Vue(七) 组件详解的更多相关文章

  1. vue的组件详解

    什么是组件 组件(Component)是 Vue.js 最强大的功能之一.(好比电脑中的每一个元件(键盘,鼠标,CPU),它是一个具有独立的逻辑和功能或界面,同时又能根据规定的接口规则进行互相融合,变 ...

  2. vue函数式组件详解

    本篇将详细介绍vue组件化之函数式组件,会用到以下api: Vue.component().Vue.extend().$createElement.patch(). 从事vue开发的小伙伴,平时组件化 ...

  3. vue.js基础知识篇(6):组件详解

    第11章:组件详解 组件是Vue.js最推崇也最强大的功能之一,核心目标是可重用性. 我们把组件代码按照template.style.script的拆分方式,放置到对应的.vue文件中. 1.注册 V ...

  4. vue 源码详解(二): 组件生命周期初始化、事件系统初始化

    vue 源码详解(二): 组件生命周期初始化.事件系统初始化 上一篇文章 生成 Vue 实例前的准备工作 讲解了实例化前的准备工作, 接下来我们继续看, 我们调用 new Vue() 的时候, 其内部 ...

  5. vue 文件目录结构详解

    vue 文件目录结构详解 本篇文章主要介绍了vue 文件目录结构详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考.一起跟随小编过来看看吧 项目简介 基于 vue.js 的前端开发环境,用于前后 ...

  6. Angular6 学习笔记——组件详解之组件通讯

    angular6.x系列的学习笔记记录,仍在不断完善中,学习地址: https://www.angular.cn/guide/template-syntax http://www.ngfans.net ...

  7. Vue props用法详解

    Vue props用法详解 组件接受的选项之一 props 是 Vue 中非常重要的一个选项.父子组件的关系可以总结为: props down, events up 父组件通过 props 向下传递数 ...

  8. main.js index.html与app.vue三者关系详解

    main.js index.html与app.vue三者关系详解 2019年01月23日 11:12:15 Pecodo 阅读数 186   main.js与index.html是nodejs的项目启 ...

  9. vue 源码详解(一):原型对象和全局 `API`的设计

    vue 源码详解(一):原型对象和全局 API的设计 1. 从 new Vue() 开始 我们在实际的项目中使用 Vue 的时候 , 一般都是在 main.js 中通过 new Vue({el : ' ...

随机推荐

  1. git中设置http代理和取消http代理

    设置http代理 git config --global https.proxy https://127.0.0.1:1080 取消http代理git config --global --unset ...

  2. Ch02 控制结构和函数 - 练习

    1. 一个数字如果为正数,则它的signum为1:如果是负数,则signum为-1:如果是0,则signum为0.编写一个函数来计算这个值. scala> def signum(x:Int):I ...

  3. python+requests+excel+unittest+ddt接口自动化数据驱动并生成html报告

    1.环境准备: python3.6 requests xlrd openpyxl HTMLTestRunner_api 2.目前实现的功能: 封装requests请求方法 在excel填写接口请求参数 ...

  4. 转载一篇较为详细的caffe-ssd编译环境的搭建

    这篇搭建的文章写得还是比较全面的,亲测有效:https://blog.csdn.net/lukaslong/article/details/81390276

  5. Unity之获取资源包的路径

    先从缓存中获取,如果获取不到,则从包中获取. 如下: public static string GetAssetBundlePath(string path) { // 先尝试从 persist 目录 ...

  6. [完美]原生JS获取浏览器版本判断--支持Edge,IE,Chrome,Firefox,Opera,Safari,以及各种使用Chrome和IE混合内核的浏览器

    截至自2017-08-11,支持现世已出的几乎所有PC端浏览器版本判断. 受支持的PC端浏览器列表: Edge IE Chrome Firefox Opera Safari QQ浏览器 360系列浏览 ...

  7. 搭建一个Web Server站点

    题:搭建一个Web Server站点.安装web服务,并在本地创建index.html测试 1.安装http服务 yum -y install httpd 2.进入网站目录 cd /var/www/h ...

  8. 创成汇丨投脑风暴·创心不止|路演日 第2期,寻IT创业者

    创成汇丨投脑风暴·创心不止|路演日 第2期   无畏荆棘之路的风雨 誓做浪潮之巅的勇者 你说,创业是一场孤注一掷的较量 你说,创新从来都是与过去battle 你还说,坚持总是比开始更让你难以琢磨 所以 ...

  9. 使用JS调用手机本地摄像头或者相册图片识别二维码/条形码

    接着昨天的需求,不过这次不依赖微信,使用纯js唤醒手机本地摄像头或者选择手机相册图片,识别其中的二维码或者是条形码.昨天,我使用微信扫一扫识别,效果超棒的.不过如果依赖微信的话,又怎么实现呢,这里介绍 ...

  10. TabBar + TabBarView导航风格

    import 'package:flutter/material.dart'; import 'News.dart'; import 'Video.dart'; import 'Chat.dart'; ...