参考书籍:《Vue.js 实战》
组件与复用
为什么使用组件
Vue 的组件就是提高复用性,让代码可复用。
组件用法
<div id="app">
<my-component></my-component>
</div>
组件需要注册后才可以使用,注册有全局注册和局部注册两种方式。
-
全局注册后,任何 Vue 实例都可以使用。
// 要在父实例中使用这个组件,必须要在实例创建前注册。 Vue.component('my-component', { template: '<div>my component</div>' }); var app = new Vue({ el: '#app' });
-
在 Vue 实例中,使用
components
选项可以局部注册组件,注册后的组件只有在该实例作用域下有效。var Child = { template: '<div>my component</div>' }; var app = new Vue({ el: '#app', components: { 'my-component': Child } });
渲染后的结果是:
<div id="app">
<div>my component</div>
</div>
Vue 组件的模板在某些情况下会收到 HTML
的限制,比如 <table>
内规定只允许是 <tr>
、<td>
等这些表格元素,所以在 <table>
内直接使用组件是无效的,这种情况下可以使用特殊的is
属性来挂载组件。
<div id="app">
<table>
<tbody is="my-component"></tbody>
</table>
</div>
Vue.component('my-component', {
template: '<div>my component</div>'
});
var app = new Vue({
el: '#app'
});
渲染后的结果是:
<div id="app">
<table>
<div>my component</div>
</table>
</div>
在组件中使用 data
时,必须是函数,然后将数据 return 出去。
Vue.component('my-component', {
template: '<div>my component</div>',
data() {
return {
message: 'message'
}
}
});
使用 props
传递数据
基本用法
组件不仅仅是要把模板的内容进行复用,更重要的是组件间要进行通信。通常父组件的模板中包含子组件,父组件要正向地向子组件传递数据或参数,子组件接收到后根据参数的不同来渲染不同的内容或执行操作。这个正向传递数据的过程就是通过 props
来实现的。
props
中声明的数据与组件 data
函数内的数据主要区别就是 props
的数据来自父级,而 data
的数据是组件自己的数据,这两种数据都可以在模板 template
、计算属性 computed
和方法 methods
中使用。
通常,传递的数据并不是直接写死的,而是来自父级的动态数据,这时可以使用指令 v-bind
来动态绑定 props
的值,当父组件的数据变化时,也会传递给子组件。
由于 HTML
特性不区分大小写,当时用 DOM
模板时,驼峰命名(camelCase)的 props
名称要转为短横分隔命名(kebab-case)。
<div id="app">
<input type="text" v-model="parentMessage" />
<my-component :my-message="parentMessage"></my-component>
</div>
Vue.component('my-component', {
props: ['myMessage'],
template: '<div>{{ myMessage}}</div>'
});
var app = new Vue({
el: '#app',
data: {
parentMessage: ''
}
});
渲染后的结果是:
<div id="app">
<div>dataMes</div>
</div>
这里用 v-model
绑定了父级的数据 parentMessage
,当通过输入框任意输入时,子组件接收到的 props
也会实时响应,并更新组件模板。
单向数据流
Vue 通过 props
传递数据是单向的,也就是父组件数据变化时会传递给子组件,但是反过来不行。之所以这样设计,是尽可能将父子组件解耦,避免子组件无意中修改了父组件的状态。
业务中会经常遇到两种需要改变 prop
的情况。
-
一种是父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改。
<div id="app"> <my-component :init-count="1"></my-component> </div>
Vue.component('my-component', { props: ['initCount'], template: '<div>{{ count }}</div>', data() { return { count: this.initCount } } }); var app = new Vue({ el: '#app' });
组件中声明了数据
count
,它在组件初始化时会获取来自父组件的initCount
,之后就与之无关了,只用维护count
,这样就可以避免直接操作initCount
。 -
另一种情况就是
prop
作为需要被转变的原始值传入。<div id="app"> <my-component :width="100"></my-component> </div>
Vue.component('my-component', { props: ['width'], template: '<div :style="style">组件内容</div>', computed: { style() { return { width: this.width + 'px' } } } }); var app = new Vue({ el: '#app' });
因为用
CSS
传递宽度要带单位(px),但是每次都写太麻烦了,而且数值计算一般是不带单位的,所以统一在组件内使用计算属性就可以了。
数据验证
Vue.component('my-component', {
props: {
propA: Number,
propB: [String, Number],
propC: {
type: Boolean,
default: true
},
propD: {
type: Number,
required: true
},
// 如果是数组或对象,默认值必须是一个函数来返回
propE: {
type: Array,
default() {
return [];
}
},
propF: {
validator(value) {
return value > 10;
}
}
}
});
组件通信
组件关系可分为父子组件通信、兄弟组件通信和跨级组件通信。
自定义事件
当子组件需要向父组件传递数据时,就要用到自定义事件。
子组件用 $emit()
来触发事件,父组件用 $on
来监听子组件的事件。
父组件也可以直接在子组件的自定义标签上使用 v-on
来监听子组件触发的自定义事件。
<div id="app">
<p>总数:{{ total }}</p>
<my-component
@increase="handleGetTotal"
@reduce="handleGetTotal">
</my-component>
</div>
Vue.component('my-component', {
template: `
<div>
<button @click="handleIncrease">+1</button>
<button @click="handleReduce">-1</button>
</div>
`,
data() {
return {
counter: 0
}
},
methods: {
handleIncrease() {
this.counter++;
this.$emit('increase', this.counter);
},
handleReduce() {
this.counter--;
this.$emit('reduce', this.counter);
}
}
});
var app = new Vue({
el: '#app',
data: {
total: 0
},
methods: {
handleGetTotal(total) {
this.total = total;
}
}
});
上面示例中,在改变组件的 counter
后,通过 $emit()
再把它传递给父组件。$emit()
方法的第一个参数是自定义事件的名称,后面的参数是要传递的数据,可以不填或填写多个。
除了用 v-on
在组件上监听自定义事件外,也可以监听 DOM
事件,这时可以用 .native
修饰符表示监听的是一个原生事件,监听的是该组件的根元素。
<my-component v-on:click.native="handleClick"></my-component>
使用 v-model
Vue 可以在自定义组件上使用 v-model
指令。
<div id="app">
<p>总数:{{ total }}</p>
<my-component v-model="total"></my-component>
</div>
Vue.component('my-component', {
template: '<button @click="handleIncrease">+1</button>',
data() {
return {
counter: 0
}
},
methods: {
handleClick() {
this.counter++;
this.$emit('input', this.counter);
}
}
});
var app = new Vue({
el: '#app',
data: {
total: 0
}
});
在使用组件的父级,并没有在 <my-component>
使用 @input="handler"
,而是直接用了 v-model
绑定的一个数据 total
。这也可以称作是一个语法糖,因为上面的示例可以间接地用自定义事件来实现:
<div id="app">
<p>总数:{{ total }}</p>
<my-component @input="handleGetTotal"></my-component>
</div>
// 省略组件代码
var app = new Vue({
el: '#app',
data: {
total: 0
},
methods: {
handleGetTotal() {
this.total = total;
}
}
});
v-model
还可以用来创建自定义的表单输入组件,进行数据双向绑定。
<div id="app">
<p>总数:{{ total }}</p>
<my-component v-model="total"></my-component>
<button @click="handleReduce">-1</button>
</div>
Vue.component('my-component', {
props: ['value'],
template: '<input :value="value" @input="updateValue" />',
methods: {
updateValue(event) {
this.$emit('input', event.target.value);
}
}
});
var app = new Vue({
el: '#app',
data: {
total: 0
},
methods: {
handleReduce() {
this.total--;
}
}
});
实现这样一个具有双向绑定的 v-model
组件要满足下面两个条件:
- 接收一个
value
属性。 - 在有新的
value
时触发input
事件。
非父子组件通信
非父子组件一般有两种,兄弟组件和跨多级组件。
在 Vue 中,推荐使用一个空的 Vue 实例作为中央事件总线(bus),也就是一个中介。
<div id="app">
{{ message }}
<component-a></component-a>
</div>
var bus = new Vue();
Vue.component('component-a', {
template: '<button @click="handleEvent">传递事件</button>',
methods: {
handleEvent() {
bus.$emit('on-message', '来自组件 component-a 的内容');
}
}
});
var app = new Vue({
el: '#app',
data: {
message: ''
},
mounted() {
var _this = this;
// 在实例初始化时,监听来自 bus 实例的事件
bus.$on('on-message', function(msg) {
_this.message = msg;
});
}
});
首先创建一个名为 bus
的空 Vue 实例,然后定义全局组件 component-a
,最后创建 Vue 实例 app
。在 app
初始化时,监听了来自 bus
的事件 on-message
,而在组件 component-a
中,点击按钮会通过 bus
把事件 on-message
发出去,此时 app
就会接收到来自 bus
的事件,进而在回调里完成自己的业务逻辑。
这种方法巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟和跨级。如果深入使用,可以扩展 bus
实例,给它添加 data
、methods
和 computed
等选项,这些都是可以共用的,在业务中,尤其是协同开发时非常有用,因为经常需要共享一些通用的信息,比如用户登录的昵称、性别、邮箱和授权等。只需子安初始化时让 bus
获取一次,任何时间、任何组件就可以从中直接使用,在单页面富应用(SPA)中会很实用。
除了中央事件总线 bus
外,还有两种方法可以实现组件间通信:父链和子组件索引。
父链
在子组件中,使用 this.$parent
可以直接访问该组件的父实例或组件,父组件也可以通过 this.$children
访问它所有的子组件,而且可以递归向上或向下无限访问,直到根实例或最内层的组件。
<div id="app">
{{ message }}
<component-a></component-a>
</div>
Vue.component('component-a', {
template: '<button @click="handleEvent">通过父链直接修改数据</button>',
methods: {
handleEvent() {
// 访问到父链后,可以做任何操作,比如直接修改数据
this.$parent.message = '来自组件 component-a 的内容'
}
}
});
var app = new Vue({
el: '#app',
data: {
message: ''
}
});
尽管 Vue 允许这样操作,但在业务中,子组件应该尽可能避免依赖父组件的数据,更不应该去主动修改它的数据,因为这样使得父子组件耦合,只看父组件,很难理解父组件的状态,因为它可能被任意组件修改,理想情况下,只有组件自己能修改它的状态。父子组件最好还是通过 props
和 $emit()
来通信。
子组件索引
当子组件较多时,通过 this.$children
来一一遍历出我们需要的一个组件实例时比较困难的,尤其是组件动态渲染时,它们的序列是不固定的。Vue 提供了子组件索引的方法,用特殊的属性 ref
来为子组件指定一个索引名称。
<div id="app">
<button @click="handleRef">通过 ref 获取子组件实例</button>
<component-a ref="comA"></component-a>
</div>
Vue.component('component-a', {
template: '<div>子组件</div>',
data() {
return {
message: '子组件内容'
}
}
});
var app = new Vue({
el: '#app',
methods: {
handleRef() {
// 通过 $refs 来访问指定的实例
var msg = this.$refs.comA.message;
console.log(msg);
}
}
});
提示:$refs
只在组件渲染完成后才填充,并且它是非响应式的。它仅仅作为一个直接访问子组件的应急方案,应当避免在模板或计算属性中使用 $refs
。
使用 slot
分发内容
什么是 slot
下面是一个常规的网站布局组件化后的机构:
<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
,这个过程叫做内容分发(transclusion)。以 <app>
为例,它有两个特点:
-
<app>
组件不知道它的挂载点会有什么内容。挂载点的内容由<app>
的父组件决定。 -
<app>
组件很可能有它自己的模板。
props
传递数据、events
触发事件和 slot
内容分发就构成了 Vue 组件的 3 个 API来源,再复杂的组件也是由这 3 部分构成的。
作用域
父组件模板的内容是在父组件作用域内编译,子组件模板的内容是在子组件作用域内编译。
<div id="app">
<child-component v-show="showChild"></child-component>
</div>
Vue.component('child-component', {
template: '<div>子组件</div>'
});
var app = new Vue({
el: '#app',
data: {
showChild: true
}
});
这里的状态 showChild
绑定的是父组件的数据,如果想在子组件上绑定,应该是:
<div id="app">
<child-component></child-component>
</div>
Vue.component('child-component', {
template: '<div v-show="showChild">子组件</div>',
data() {
return {
showChild: true
}
}
});
var app = new Vue({
el: '#app'
});
因此,slot
分发的内容,作用域是在父组件上的。
slot
用法
单个 slot
在子组件内使用特殊的 <slot>
元素就可以为这个子组件开启一个 slot
(插槽),在父组件模板里,插入在子组件标签内的所有内容将替代子组件的 <slot>
标签及它的内容。
<div id="app">
<child-component>
<p>分发的内容</p>
<p>更多分发的内容</p>
</child-component>
</div>
Vue.component('child-component', {
template: `
<div>
<slot>
<p>如果父组件没有插入内容,我将作为默认出现</p>
</slot>
</div>
`
});
var app = new Vue({
el: '#app'
});
上例渲染后的结果是:
<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>
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'
});
上例渲染后的结果是:
<div id="app">
<div class="container">
<div class="header">
<h2>标题</h2>
</div>
<div class="main">
<p>正文内容</p>
<p>更多的正文内容</p>
</div>
<div class="footer">
<div>底部信息</div>
</div>
</div>
</div>
注意:如果没有指定默认的匿名 slot
,父组件内多余的内容片段都将被抛弃。
作用域插槽
作用域插槽是一种特殊的 slot
,使用一个可以复用的模板替换已渲染元素。
<div id="app">
<child-component>
<template scope="props">
<p>来自父组件的内容</p>
<p>{{ props.msg }}</p>
</template>
</child-component>
</div>
Vue.component('child-component', {
template: `
<div class="container">
<slot msg="来自子组件的内容"><slot>
</div>
`
});
var app = new Vue({
el: '#app'
});
观察子组件的模板,在 <slot>
元素上有一个类似 props
传递数据给组件的写法 msg="xxx"
,将数据传到了插槽。父组件中使用了 <template>
元素,而且拥有一个 scope="props"
的特性,这里的 props
只是一个临时变量,就像 v-for="item in items"
里面的 item
一样。template
内可以通过临时变量 props
访问来自子组件插槽的数据 msg
。
上例渲染后的结果是:
<div id="app">
<child-component>
<p>来自父组件的内容</p>
<p>来自子组件的内容</p>
</child-component>
</div>
作用域插槽更具代表性的用例是列表组件,允许组件自定义应该如何渲染列表每一项。
<div id="app">
<my-list :books="books">
<!-- 作用域插槽也可以是具名的 slot -->
<template slot="book" scope="props">
<li>{{ props.bookName }}</li>
</template>
</my-list>
</div>
Vue.component('my-list', {
props: {
books: {
type: Array,
default() {
return [];
}
}
},
template: `
<ul>
<slot name="book"
v-for="book in books"
:book-name="book.name"
></slot>
</ul>
`
});
var app = new Vue({
el: '#app',
data: {
books: [
{ name: '《book1》' },
{ name: '《book2》' },
{ name: '《book3》' }
]
}
});
子组件 my-list
接收一个来自父级的 prop
数组 books
,并且将它在 name
为 book
的 slot
上使用 v-for
指令循环,同时暴露一个变量 bookName
。
此例的用意主要是介绍作用域插槽的用法,并没有加入使用场景,而作用域插槽的使用场景既可以复用子组件的 slot
,又可以是 slot
内容不一致。如果此例还在其他组件内使用,<li>
的内容渲染权是由使用者掌握的,而数据却可以通过临时变量(比如 props
)从子组件内获取。
访问 slot
Vue 提供了用来访问被 slot
分发的内容的方法 $slots
。
<div id="app">
<child-component>
<h2 slot="header">标题</h2>
<p>正文内容</p>
<p>更多的正文内容</p>
<div slot="footer">底部信息</div>
</child-component>
</div>
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() {
var header = this.$slots.header,
main = this.$slots.default,
footer = this.$slots.footer;
console.log(footer);
console.log(footer[0].elm.innerHTML);
}
});
var app = new Vue({
el: '#app'
});
通过 $slots
可以访问某个具名 slot
,this.$slots.default
包括了所有没有被包含在具名 slot
中的节点。
$slots
在业务中几乎用不到,在用 render
函数创建组件时会比较有用,但主要还是用于独立组件开发中。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。