Vue.js入门-6-组件

组件

什么是组件

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

所有的 Vue 组件同时也都是 Vue 的实例,所以可接受相同的选项对象 (除了一些根级特有的选项) 并提供相同的生命周期钩子。

使用组件

全局注册

要注册一个全局组件,可以使用 Vue.component(tagName, options)。例如:

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

组件在注册之后,便可以作为自定义元素 <my-component></my-component> 在一个实例的模板中使用。注意确保在初始化根实例之前注册组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="example">
<my-component></my-component>
</div>

// 注册
Vue.component('my-component', {
template: '<div>A custom component!</div>'
})

// 创建根实例
new Vue({
el: '#example'
})

渲染为:

1
2
3
<div id="example">
<div>A custom component!</div>
</div>

局部注册

可以通过某个 Vue 实例/组件的实例选项 components 注册仅在其作用域中可用的组件:

1
2
3
4
5
6
7
8
9
10
11
var Child = { 
template: '<div>A custom component!</div>'
}

new Vue({
// ...
components: {
// <my-component> 将只在父组件模板中可用
'my-component': Child
}
})

DOM 模板解析注意事项

当使用 DOM 作为模板时 (例如,使用 el 选项来把 Vue 实例挂载到一个已有内容的元素上),你会受到 HTML 本身的一些限制,因为 Vue 只有在浏览器解析、规范化模板之后才能获取其内容。尤其要注意,像

      、、

在自定义组件中使用这些受限制的元素时会导致一些问题,例如:

1
2
3
<table>
<my-row>...</my-row>
</table>

自定义组件 <my-row> 会被当作无效的内容,因此会导致错误的渲染结果。变通的方案是使用特殊的 is 特性:

1
2
3
<table>
<tr is="my-row"></tr>
</table>

组件实例中 data 必须是一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="example-2">
<simple-counter></simple-counter>
<simple-counter></simple-counter>
<simple-counter></simple-counter>
</div>
var data = { counter: 0 }

Vue.component('simple-counter', {
template: '<button v-on:click="counter += 1">{ { counter } }</button>',
// 技术上 data 的确是一个函数了,因此 Vue 不会警告,
// 但是我们却给每个组件实例返回了同一个对象的引用
data: function () {
return data
}
})

new Vue({
el: '#example-2'
})

由于这三个组件实例共享了同一个 data 对象,因此递增一个 counter 会影响所有组件!我们可以通过为每个组件返回全新的数据对象来修复这个问题:

1
2
3
4
5
data: function () { 
return {
counter: 0
}
}

现在每个 counter 都有它自己内部的状态了

组件组合

组件设计初衷就是要配合使用的,最常见的就是形成父子组件的关系,通过一个良好定义的接口来尽可能将父子组件解耦也是很重要的。这保证了每个组件的代码可以在相对隔离的环境中书写和理解,从而提高了其可维护性和复用性。

在 Vue 中,父子组件的关系可以总结为 **prop 向下传递,事件向上传递。父组件通过 prop给子组件下发数据,子组件通过事件**给父组件发送消息。看看它们是怎么工作的。

Prop

使用 Prop 传递数据

组件实例的作用域是孤立的。这意味着不能 (也不应该) 在子组件的模板内直接引用父组件的数据。父组件的数据需要通过 prop 才能下发到子组件中。

子组件要显式地用 props 选项声明它预期的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Vue.component('child', { 
// 声明 props
props: ['message'],
// 就像 data 一样,prop 也可以在模板中使用
// 同样也可以在 vm 实例中通过 this.message 来使用
template: '<span>{ { message } }</span>'
})

// 然后我们可以这样向它传入一个普通字符串:

<child message="hello!"></child>

//结果:

hello!

camelCase vs. kebab-case

HTML 特性是不区分大小写的。所以,当使用的不是字符串模板时,camelCase (驼峰式命名) 的 prop 需要转换为相对应的 kebab-case (短横线分隔式命名):

1
2
3
4
5
6
7
8
Vue.component('child', { 
// 在 JavaScript 中使用 camelCase
props: ['myMessage'],
template: '<span>{ { myMessage } }</span>'
})

<!-- 在 HTML 中使用 kebab-case -->
<child my-message="hello!"></child>

动态Prop

与绑定到任何普通的 HTML 特性相类似,我们可以用 v-bind 来动态地将 prop 绑定到父组件的数据。每当父组件的数据变化时,该变化也会传导给子组件:

1
2
3
4
5
6
7
8
9
<div>
<input v-model="parentMsg">
<br>
<child v-bind:my-message="parentMsg"></child>
</div>

//你也可以使用 v-bind 的缩写语法:

<child :my-message="parentMsg"></child>

如果你想把一个对象的所有属性作为 prop 进行传递,可以使用不带任何参数的 v-bind (即用 v-bind 而不是 v-bind:prop-name)。例如,已知一个 todo 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
todo: { 
text: 'Learn Vue',
isComplete: false
}

//然后:

<todo-item v-bind="todo"></todo-item>

//将等价于:

<todo-item
v-bind:text="todo.text"
v-bind:is-complete="todo.isComplete"
></todo-item>

字面量语法 vs 动态语法

如果想要给组件传递一个数字,是不可以使用常量传递的。因为它传递的并不是实际的数字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="watch-example">
<demo-component size="10"></demo-component>
</div>
<script type="text/javascript">
Vue.component("demo-component", {
props: ["message", "size"],
template: "<div><button v-on:click=add>add</button><span>{ { size } }</span></div>",
methods:{
add:function(){
this.size += 1; // 1011111111...
}
}
});
var vm = new Vue({
el: "#watch-example"
})
</script>

如果想传递一个真正的 JavaScript 数值,则需要使用 v-bind,从而让它的值被当作 JavaScript 表达式计算:

1
2
<!-- 传递真正的数值 -->
<comp v-bind:some-prop="1"></comp>

单项数据流

Prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是反过来不会。

每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop。

  • Prop 作为初始值传入后,子组件想把它当作局部数据来用。正确的应对方式是:定义一个局部变量,并用 prop 的值初始化它:
1
2
3
4
props: ['initialCounter'],
data: function () {
return { counter: this.initialCounter }
}
  • Prop 作为原始数据传入,由子组件处理成其它数据输出。正确的应对方式是:定义一个计算属性,处理 prop 的值并返回:
1
2
3
4
5
6
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}

注意在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态。

Prop验证

我们可以为组件的 prop 指定验证规则。如果传入的数据不符合要求,Vue 会发出警告。

要指定验证规则,需要用对象的形式来定义 prop,而不能用字符串数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Vue.component('example', { 
props: {
// 基础类型检测 (`null` 指允许任何类型)
propA: Number,
// 可能是多种类型
propB: [String, Number],
// 必传且是字符串
propC: {
type: String,
required: true
},
// 数值且有默认值
propD: {
type: Number,
default: 100
},
// 数组/对象的默认值应当由一个工厂函数返回
propE: {
type: Object,
default: function () {
return { message: 'hello' }
}
},
// 自定义验证函数
propF: {
validator: function (value) {
return value > 10
}
}
}
})

非 Prop 特性

非 prop 特性,就是指它可以直接传入组件,而不需要定义相应的 prop。

组件可以接收任意传入的特性,这些特性都会被添加到组件的根元素上。

1
<bs-date-input data-3d-date-picker="true"></bs-date-input>

添加属性 data-3d-date-picker="true" 之后,它会被自动添加到 bs-date-input 的根元素上。

替换/合并现有的特性

对于多数特性来说,传递给组件的值会覆盖组件本身设定的值。

classstyle 这两个特性的值都会做合并 (merge) 操作,让最终生成的值

自定义事件

父组件是使用props传递数据给子组件,如果子组件要把数据传回给父组件。就需要使用自定义事件

使用 v-on 绑定自定义事件

每个 Vue 实例都实现了事件接口,即:

  • 使用 $on(eventName) 监听事件
  • 使用 $emit(eventName) 触发事件

另外,父组件可以在使用子组件的地方直接用 v-on 来监听子组件触发的事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<div id="counter-event-example">
<p>{ { total } }</p>
<button-counter v-on:increment="incrementTotal"></button-counter>
<button-counter v-on:increment="incrementTotal"></button-counter>
</div>

Vue.component('button-counter', {
template: '<button v-on:click="incrementCounter">{ { counter } }</button>',
data: function () {
return {
counter: 0
}
},
methods: {
incrementCounter: function () {
this.counter += 1
this.$emit('increment')
}
},
})

new Vue({
el: '#counter-event-example',
data: {
total: 0
},
methods: {
incrementTotal: function () {
this.total += 1
}
}
})

给组件绑定原生事件

在某个组件的根元素上监听一个原生事件。可以使用 v-on 的修饰符 .native。例如:

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

.sync 修饰符

.sync 修饰符会被扩展为一个自动更新父组件属性的 v-on 监听器。

1
<comp :foo.sync="bar"></comp>

会被扩展为:

1
<comp :foo="bar" @update:foo="val => bar = val"></comp>

当子组件需要更新 foo 的值时,它需要显式地触发一个更新事件:

1
this.$emit('update:foo', newValue)

使用自定义事件的表单输入组件

自定义事件也可以用来创建自定义的表单输入组件,使用v-model 来进行数据双向绑定

1
<input v-model="something">

只是一个语法糖,它对应的语句是

1
2
3
<input v-bind:value="something" v-on:input="something = $event.target.value">

<custom-input v-bind:value="something" v-on:input="something = arguments[0]"></custom-input>

所以要让组件的 v-model 生效,它应该 (从 2.2.0 起是可配置的):

  • 接受一个 value prop
  • 在有新的值时触发 input 事件并将新值作为参数

一个简单的货币输入的自定义控件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<currency-input v-model="price"></currency-input>
Vue.component('currency-input', {
template: '\
<span>\
$\
<input\
ref="input"\
v-bind:value="value"\
v-on:input="updateValue($event.target.value)"\
>\
</span>\
',
props: ['value'],
methods: {
// 不是直接更新值,而是使用此方法来对输入值进行格式化和位数限制
updateValue: function (value) {
var formattedValue = value
// 删除两侧的空格符
.trim()
// 保留 2 位小数
.slice(
0,
value.indexOf('.') === -1
? value.length
: value.indexOf('.') + 3
)
// 如果值尚不合规,则手动覆盖为合规的值
if (formattedValue !== value) {
this.$refs.input.value = formattedValue
}
// 通过 input 事件带出数值
this.$emit('input', Number(formattedValue))
}
}
})

自定义组件的 v-model

默认情况下,一个组件的 v-model 会使用 value propinput 事件。但是诸如单选框、复选框之类的输入类型可能把 value 用作了别的目的。model 选项可以避免这样的冲突:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Vue.component('my-checkbox', { 
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean,
// 这样就允许拿 `value` 这个 prop 做其它事了
value: String
},
// ...
})
<my-checkbox v-model="foo" value="some value"></my-checkbox>

// 上述代码等价于:

<my-checkbox
:checked="foo"
@change="val => { foo = val }"
value="some value">
</my-checkbox>

非父子组件的通信

有时候,非父子关系的两个组件之间也需要通信。在简单的场景下,可以使用一个空的 Vue 实例作为事件总线:

1
2
3
4
5
6
7
8
9
var bus = new Vue()

// 触发组件 A 中的事件
bus.$emit('id-selected', 1)

// 在组件 B 创建的钩子中监听事件
bus.$on('id-selected', function (id) {
// ...
})

使用插槽分发内容

在使用组件时,经常需要像这样组合它们

1
2
3
4
<app>
<app-header></app-header>
<app-footer></app-footer>
</app>

有两点需要注意:

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

  • <app> 组件很可能有它自己的模板

为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个过程被称为内容分发。

Vue.js 实现了一个内容分发 API,使用特殊的 <slot> 元素作为原始内容的插槽。

编译作用域

1
2
3
<child-component>
{ { message } }
</child-component>

上面例子中,message应该绑定到父组件的数据。

组件作用域简单地说是:

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


如果要绑定子组件作用域内的指令到一个组件的根节点,你应当在子组件自己的模板里做:

1
2
3
4
5
6
7
8
9
Vue.component('child-component', { 
// 有效,因为是在正确的作用域内
template: '<div v-show="someChildProperty">Child</div>',
data: function () {
return {
someChildProperty: true
}
}
})

单个插槽

除非子组件模板包含至少一个 <slot> 插口,否则父组件的内容将会被丢弃。当子组件模板只有一个没有属性的插槽时,父组件传入的整个内容片段将插入到插槽所在的 DOM 位置,并替换掉插槽标签本身。

最初在 <slot> 标签中的任何内容都被视为备用内容。备用内容在子组件的作用域内编译,并且只有在宿主元素为空,且没有要插入的内容时才显示备用内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//my-component 组件有如下模板:

<div>
<h2>我是子组件的标题</h2>
<slot>
只有在没有要分发的内容时才会显示。
</slot>
</div>

// 父组件模板:

<div>
<h1>我是父组件的标题</h1>
<my-component>
<p>这是一些初始内容</p>
<p>这是更多的初始内容</p>
</my-component>
</div>

// 渲染结果:

<div>
<h1>我是父组件的标题</h1>
<div>
<h2>我是子组件的标题</h2>
<p>这是一些初始内容</p>
<p>这是更多的初始内容</p>
</div>
</div>

具名插槽

<slot> 元素可以用一个特殊的特性 name 来进一步配置如何分发内容。多个插槽可以有不同的名字。具名插槽将匹配内容片段中有对应 slot 特性的元素。

仍然可以有一个匿名插槽,它是默认插槽,作为找不到匹配的内容片段的备用插槽。如果没有默认插槽,这些找不到匹配的内容片段将被抛弃。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// app-layout 组件,它的模板为:

<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>

// 父组件模板:

<app-layout>
<h1 slot="header">这里可能是一个页面标题</h1>

<p>主要内容的一个段落。</p>
<p>另一个主要段落。</p>

<p slot="footer">这里有一些联系信息</p>
</app-layout>

// 渲染结果为:

<div class="container">
<header>
<h1>这里可能是一个页面标题</h1>
</header>
<main>
<p>主要内容的一个段落。</p>
<p>另一个主要段落。</p>
</main>
<footer>
<p>这里有一些联系信息</p>
</footer>
</div>

作用域插槽

作用域插槽是一种特殊类型的插槽,2.1.0 新增,用作一个 (能被传递数据的) 可重用模板,来代替已经渲染好的元素。

在子组件中,只需将数据传递到插槽,就像你将 prop 传递给组件一样:

1
2
3
<div class="child">
<slot text="hello from child"></slot>
</div>

在父级中,具有特殊特性 slot-scope<template> 元素必须存在,表示它是作用域插槽的模板。slot-scope 的值将被用作一个临时变量名,此变量接收从子组件传递过来的 prop 对象:

1
2
3
4
5
6
7
8
<div class="parent">
<child>
<template slot-scope="props">
<span>hello from parent</span>
<span>{ { props.text } }</span>
</template>
</child>
</div>

如果我们渲染上述模板,得到的输出会是:

1
2
3
4
5
6
<div class="parent">
<div class="child">
<span>hello from parent</span>
<span>hello from child</span>
</div>
</div>

动态组件

通过使用保留的 <component> 元素,并对其 is 特性进行动态绑定,你可以在同一个挂载点动态切换多个组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var vm = new Vue({ 
el: '#example',
data: {
currentView: 'home'
},
components: {
home: { /* ... */ },
posts: { /* ... */ },
archive: { /* ... */ }
}
})

<component v-bind:is="currentView">
<!-- 组件在 vm.currentview 变化时改变! -->
</component>

也可以直接绑定到组件对象上:

1
2
3
4
5
6
7
8
9
10
var Home = { 
template: '<p>Welcome home!</p>'
}

var vm = new Vue({
el: '#example',
data: {
currentView: Home
}
})

keep-alive

如果把切换出去的组件保留在内存中,可以保留它的状态或避免重新渲染。为此可以添加一个 keep-alive 指令参数:

1
2
3
4
5
<keep-alive>
<component :is="currentView">
<!-- 非活动组件将被缓存! -->
</component>
</keep-alive>

杂项

编写可复用组件

在编写组件时,最好考虑好以后是否要进行复用。一次性组件间有紧密的耦合没关系,但是可复用组件应当定义一个清晰的公开接口,同时也不要对其使用的外层数据作出任何假设。

Vue 组件的 API 来自三部分——prop、事件和插槽:

  • Prop 允许外部环境传递数据给组件;

  • 事件允许从组件内触发外部环境的副作用;

  • 插槽允许外部环境将额外的内容组合在组件中。

使用 v-bind 和 v-on 的简写语法,模板的意图会更清楚且简洁:

1
2
3
4
5
6
7
8
9
<my-component
:foo="baz"
:bar="qux"
@event-a="doThis"
@event-b="doThat"
>
<img slot="icon" src="...">
<p slot="main-text">Hello!</p>
</my-component>

子组件引用

尽管有 prop 和事件,但是有时仍然需要在 JavaScript 中直接访问子组件。为此可以使用 ref 为子组件指定一个引用 ID。例如:

1
2
3
4
5
6
7
<div id="parent">
<user-profile ref="profile"></user-profile>
</div>

var parent = new Vue({ el: '#parent' })
// 访问子组件实例
var child = parent.$refs.profile

当 ref 和 v-for 一起使用时,获取到的引用会是一个数组,包含和循环数据源对应的子组件。

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

异步组件

Vue.js 允许将组件定义为一个工厂函数,异步地解析组件的定义。Vue.js 只在组件需要渲染时触发工厂函数,并且把结果缓存起来,用于后面的再次渲染。例如:

1
2
3
4
5
6
7
8
Vue.component('async-example', function (resolve, reject) { 
setTimeout(function () {
// 将组件定义传入 resolve 回调函数
resolve({
template: '<div>I am async!</div>'
})
}, 1000)
})
文章目录
  1. 1. 什么是组件
  2. 2. 使用组件
    1. 2.1. 全局注册
    2. 2.2. 局部注册
    3. 2.3. DOM 模板解析注意事项
    4. 2.4. 组件实例中 data 必须是一个函数
    5. 2.5. 组件组合
  3. 3. Prop
    1. 3.1. 使用 Prop 传递数据
    2. 3.2. camelCase vs. kebab-case
    3. 3.3. 动态Prop
    4. 3.4. 字面量语法 vs 动态语法
    5. 3.5. 单项数据流
    6. 3.6. Prop验证
  4. 4. 非 Prop 特性
    1. 4.1. 替换/合并现有的特性
  5. 5. 自定义事件
    1. 5.1. 使用 v-on 绑定自定义事件
    2. 5.2. 给组件绑定原生事件
    3. 5.3. .sync 修饰符
    4. 5.4. 使用自定义事件的表单输入组件
    5. 5.5. 自定义组件的 v-model
    6. 5.6. 非父子组件的通信
  6. 6. 使用插槽分发内容
    1. 6.1. 编译作用域
    2. 6.2. 单个插槽
    3. 6.3. 具名插槽
    4. 6.4. 作用域插槽
  7. 7. 动态组件
    1. 7.1. keep-alive
  8. 8. 杂项
    1. 8.1. 编写可复用组件
    2. 8.2. 子组件引用
    3. 8.3. 异步组件
|