组件

什么是组件?

组件是 Vue.js 最强大的功能之一。它们帮助您扩展基本 HTML 元素以封装可重用的代码。从高层次上讲,组件是 Vue.js 编译器将附加指定行为的自定义元素。在某些情况下,它们也可能以使用特殊 is 属性扩展的原生 HTML 元素的形式出现。

使用组件

注册

我们在前面的部分中了解到,我们可以使用 Vue.extend() 创建组件构造函数。

var MyComponent = Vue.extend({
// options...
})

要将此构造函数用作组件,您需要使用 Vue.component(tag, constructor) 将其注册

// Globally register the component with tag: my-component
Vue.component('my-component', MyComponent)

请注意,Vue.js 不强制执行 W3C 规则 用于自定义标签名称(全部小写,必须包含连字符),尽管遵循此约定被认为是最佳实践。

注册后,该组件现在可以在父实例的模板中用作自定义元素 <my-component>。确保在实例化根 Vue 实例之前注册该组件。以下是完整的示例

<div id="example">
<my-component></my-component>
</div>
// define
var MyComponent = Vue.extend({
template: '<div>A custom component!</div>'
})
// register
Vue.component('my-component', MyComponent)
// create a root instance
new Vue({
el: '#example'
})

这将渲染

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

请注意,组件的模板替换了自定义元素,自定义元素仅用作挂载点。此行为可以使用 replace 实例选项进行配置。

还要注意,组件提供了一个模板,而不是使用 el 选项进行挂载!只有根 Vue 实例(使用 new Vue 定义)将包含一个 el 用于挂载。

本地注册

您不必全局注册每个组件。您可以通过使用 components 实例选项在另一个组件的范围内使组件可用。

var Child = Vue.extend({ /* ... */ })
var Parent = Vue.extend({
template: '...',
components: {
// <my-component> will only be available in Parent's template
'my-component': Child
}
})

相同的封装适用于其他资产类型,例如指令、过滤器和过渡。

注册语法糖

为了简化操作,您可以直接将选项对象而不是实际构造函数传递给 Vue.component()component 选项。Vue.js 会在幕后自动为您调用 Vue.extend()

// extend and register in one step
Vue.component('my-component', {
template: '<div>A custom component!</div>'
})
// also works for local registration
var Parent = Vue.extend({
components: {
'my-component': {
template: '<div>A custom component!</div>'
}
}
})

组件选项注意事项

大多数可以传递给 Vue 构造函数的选项都可以在 Vue.extend() 中使用,有两个特殊情况:datael。假设我们只是将一个对象作为 data 传递给 Vue.extend()

var data = { a: 1 }
var MyComponent = Vue.extend({
data: data
})

这样做的问题是,相同的 data 对象将在 MyComponent 的所有实例之间共享!这很可能不是我们想要的,因此我们应该使用一个返回新对象的函数作为 data 选项。

var MyComponent = Vue.extend({
data: function () {
return { a: 1 }
}
})

el 选项在 Vue.extend() 中使用时也需要一个函数值,原因完全相同。

模板解析

Vue.js 模板引擎是基于 DOM 的,它使用浏览器提供的原生解析器,而不是提供自定义解析器。与基于字符串的模板引擎相比,这种方法有很多优点,但也有一些注意事项。模板必须是单独有效的 HTML 片段。某些 HTML 元素对其中可以出现的元素有限制。最常见的限制是

在实践中,这些限制会导致意外行为。虽然在简单的情况下它可能看起来有效,但您不能依赖于自定义元素在浏览器验证之前扩展。例如,<my-select><option>...</option></my-select> 不是有效的模板,即使 my-select 组件最终扩展为 <select>...</select>

另一个结果是,您不能在 selecttable 和其他具有类似限制的元素中使用自定义标签(包括自定义元素和特殊标签,如 <component><template><partial>)。自定义标签将被提升,因此无法正确渲染。

对于自定义元素,您应该使用 is 特殊属性

<table>
<tr is="my-component"></tr>
</table>

对于 <table> 中的 <template>,您应该使用 <tbody>,因为表允许有多个 tbody

<table>
<tbody v-for="item in items">
<tr>Even row</tr>
<tr>Odd row</tr>
</tbody>
</table>

Props

使用 Props 传递数据

每个组件实例都有自己的隔离作用域。这意味着您不能(也不应该)直接在子组件的模板中引用父数据。可以使用 props 将数据传递给子组件。

“prop”是组件数据上的一个字段,预期从其父组件传递下来。子组件需要使用 props 选项 明确声明它期望接收的 props。

Vue.component('child', {
// declare the props
props: ['msg'],
// the prop can be used inside templates, and will also
// be set as `this.msg`
template: '<span>{{ msg }}</span>'
})

然后,我们可以像这样传递一个普通字符串给它

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

结果

camelCase vs. kebab-case

HTML 属性不区分大小写。当使用 camelCased prop 名称作为属性时,您需要使用它们的 kebab-case(连字符分隔)等效项

Vue.component('child', {
// camelCase in JavaScript
props: ['myMessage'],
template: '<span>{{ myMessage }}</span>'
})
<!-- kebab-case in HTML -->
<child my-message="hello!"></child>

动态 Props

与将普通属性绑定到表达式类似,我们也可以使用 v-bind 将 props 动态绑定到父级的数据。每当父级的数据更新时,它也会向下传递给子级

<div>
<input v-model="parentMsg">
<br>
<child v-bind:my-message="parentMsg"></child>
</div>

使用 v-bind 的简写语法通常更简单

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

结果


字面量 vs. 动态

初学者常犯的一个错误是尝试使用字面量语法传递一个数字

<!-- this passes down a plain string "1" -->
<comp some-prop="1"></comp>

但是,由于这是一个字面量 prop,因此其值作为普通字符串 "1" 传递下来,而不是实际的数字。如果我们想传递一个实际的 JavaScript 数字,我们需要使用动态语法使其值被评估为 JavaScript 表达式

<!-- this passes down an actual number -->
<comp :some-prop="1"></comp>

Prop 绑定类型

默认情况下,所有 props 在子属性和父属性之间形成单向向下绑定:当父属性更新时,它将向下传递给子属性,但反之则不行。此默认值旨在防止子组件意外地改变父级的状态,这会使您的应用程序的数据流更难推理。但是,也可以使用 .sync.once 绑定类型修饰符 明确强制执行双向绑定或一次性绑定

比较语法

<!-- default, one-way-down binding -->
<child :msg="parentMsg"></child>
<!-- explicit two-way binding -->
<child :msg.sync="parentMsg"></child>
<!-- explicit one-time binding -->
<child :msg.once="parentMsg"></child>

双向绑定将同步子级 msg 属性的变化回父级的 parentMsg 属性。一次性绑定一旦设置,就不会在父级和子级之间同步未来的更改。

请注意,如果传递下来的 prop 是一个对象或一个数组,则它是按引用传递的。在子级中改变对象或数组本身影响父级状态,无论您使用哪种绑定类型。

Prop 验证

组件可以指定它接收的 props 的要求。当您编写一个旨在供其他人使用的组件时,这很有用,因为这些 prop 验证要求本质上构成了您的组件的 API,并确保您的用户正确使用您的组件。而不是将 props 定义为字符串数组,您可以使用包含验证要求的对象哈希格式

Vue.component('example', {
props: {
// basic type check (`null` means accept any type)
propA: Number,
// multiple possible types (1.0.21+)
propM: [String, Number],
// a required string
propB: {
type: String,
required: true
},
// a number with default value
propC: {
type: Number,
default: 100
},
// object/array defaults should be returned from a
// factory function
propD: {
type: Object,
default: function () {
return { msg: 'hello' }
}
},
// indicate this prop expects a two-way binding. will
// raise a warning if binding type does not match.
propE: {
twoWay: true
},
// custom validator function
propF: {
validator: function (value) {
return value > 10
}
},
// coerce function (new in 1.0.12)
// cast the value before setting it on the component
propG: {
coerce: function (val) {
return val + '' // cast the value to string
}
},
propH: {
coerce: function (val) {
return JSON.parse(val) // cast the value to Object
}
}
}
})

type 可以是以下本机构造函数之一

此外,type 也可以是自定义构造函数,断言将使用 instanceof 检查进行。

当 prop 验证失败时,Vue 将拒绝在子组件上设置该值,并在使用开发版本时抛出一个警告。

父子通信

父链

子组件可以访问其父组件作为 this.$parent。根 Vue 实例将作为 this.$root 可用于其所有后代。每个父组件都有一个数组 this.$children,其中包含其所有子组件。

虽然可以访问父链中的任何实例,但您应该避免直接依赖子组件中的父数据,而是更喜欢使用 props 明确传递数据。此外,从子组件中改变父状态是一个非常糟糕的做法,因为

  1. 它使父级和子级紧密耦合;

  2. 当单独查看父状态时,它使父状态更难推理,因为它的状态可能会被任何子级修改!理想情况下,只有组件本身应该被允许修改自己的状态。

自定义事件

所有 Vue 实例都实现了一个自定义事件接口,该接口有助于在组件树中进行通信。此事件系统独立于原生 DOM 事件,并且工作方式不同。

每个 Vue 实例都是一个事件发射器,可以

与 DOM 事件不同,Vue 事件会在沿着传播路径第一次触发回调后自动停止传播,除非回调显式返回 true

一个简单的例子

<!-- template for child -->
<template id="child-template">
<input v-model="msg">
<button v-on:click="notify">Dispatch Event</button>
</template>
<!-- template for parent -->
<div id="events-example">
<p>Messages: {{ messages | json }}</p>
<child></child>
</div>
// register child, which dispatches an event with
// the current message
Vue.component('child', {
template: '#child-template',
data: function () {
return { msg: 'hello' }
},
methods: {
notify: function () {
if (this.msg.trim()) {
this.$dispatch('child-msg', this.msg)
this.msg = ''
}
}
}
})
// bootstrap parent, which pushes message into an array
// when receiving the event
var parent = new Vue({
el: '#events-example',
data: {
messages: []
},
// the `events` option simply calls `$on` for you
// when the instance is created
events: {
'child-msg': function (msg) {
// `this` in event callbacks are automatically bound
// to the instance that registered it
this.messages.push(msg)
}
}
})

消息:{{ messages | json }}

v-on 用于自定义事件

上面的例子很好,但是当我们查看父组件的代码时,并不明显 "child-msg" 事件来自哪里。如果我们可以在模板中声明事件处理程序,就在使用子组件的地方,会更好。为了实现这一点,v-on 可以用于监听自定义事件,当它用于子组件时

<child v-on:child-msg="handleIt"></child>

这使得事情非常清晰:当子组件触发 "child-msg" 事件时,父组件的 handleIt 方法将被调用。任何影响父组件状态的代码都将在 handleIt 父方法中;子组件只关注触发事件。

子组件引用

尽管存在 props 和事件,但有时你可能仍然需要在 JavaScript 中直接访问子组件。为了实现这一点,你必须使用 v-ref 为子组件分配一个引用 ID。例如

<div id="parent">
<user-profile v-ref:profile></user-profile>
</div>
var parent = new Vue({ el: '#parent' })
// access child component instance
var child = parent.$refs.profile

v-refv-for 一起使用时,你得到的引用将是一个数组或一个对象,包含反映数据源的子组件。

使用插槽进行内容分发

使用组件时,通常希望像这样组合它们

<app>
<app-header></app-header>
<app-footer></app-footer>
</app>

这里有两点需要注意

  1. <app> 组件不知道其挂载目标中可能存在什么内容。它由使用 <app> 的任何父组件决定。

  2. <app> 组件很可能拥有自己的模板。

为了使组合工作,我们需要一种方法来交织父“内容”和组件自己的模板。这是一个称为**内容分发**(如果你熟悉 Angular,则称为“转录”)的过程。Vue.js 实现了一个内容分发 API,该 API 模仿了当前的 Web Components 规范草案,使用特殊的 <slot> 元素作为原始内容的分发出口。

编译范围

在我们深入研究 API 之前,让我们先澄清一下内容是在哪个范围内编译的。想象一下这样的模板

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

msg 应该绑定到父组件的数据还是子组件的数据?答案是父组件。关于组件范围的一个简单经验法则:

父模板中的所有内容都在父组件范围内编译;子模板中的所有内容都在子组件范围内编译。

一个常见的错误是试图在父模板中将指令绑定到子组件的属性/方法

<!-- does NOT work -->
<child-component v-show="someChildProperty"></child-component>

假设 someChildProperty 是子组件上的一个属性,上面的例子将无法按预期工作。父组件的模板不应该了解子组件的状态。

如果你需要在组件根节点上绑定子组件范围的指令,你应该在子组件自己的模板中这样做

Vue.component('child-component', {
// this does work, because we are in the right scope
template: '<div v-show="someChildProperty">Child</div>',
data: function () {
return {
someChildProperty: true
}
}
})

同样,分发的内容将在父组件范围内编译。

单个插槽

父组件内容将被**丢弃**,除非子组件模板包含至少一个 <slot> 出口。当只有一个没有属性的插槽时,整个内容片段将被插入到 DOM 中的其位置,替换插槽本身。

最初位于 <slot> 标签内的任何内容都被认为是**回退内容**。回退内容在子组件范围内编译,并且只有在托管元素为空且没有要插入的内容时才会显示。

假设我们有一个具有以下模板的组件

<div>
<h1>This is my component!</h1>
<slot>
This will only be displayed if there is no content
to be distributed.
</slot>
</div>

使用该组件的父组件标记

<my-component>
<p>This is some original content</p>
<p>This is some more original content</p>
</my-component>

渲染结果将是

<div>
<h1>This is my component!</h1>
<p>This is some original content</p>
<p>This is some more original content</p>
</div>

命名插槽

<slot> 元素有一个特殊的属性 name,它可以用来进一步定制内容应该如何分发。你可以有多个具有不同名称的插槽。命名插槽将匹配内容片段中具有对应 slot 属性的任何元素。

仍然可以有一个未命名的插槽,它是**默认插槽**,作为任何不匹配内容的万能出口。如果没有默认插槽,不匹配的内容将被丢弃。

例如,假设我们有一个 multi-insertion 组件,其模板如下

<div>
<slot name="one"></slot>
<slot></slot>
<slot name="two"></slot>
</div>

父组件标记

<multi-insertion>
<p slot="one">One</p>
<p slot="two">Two</p>
<p>Default A</p>
</multi-insertion>

渲染结果将是

<div>
<p slot="one">One</p>
<p>Default A</p>
<p slot="two">Two</p>
</div>

内容分发 API 是设计用于组合在一起的组件时非常有用的机制。

动态组件

你可以使用相同的挂载点,并通过使用保留的 <component> 元素并动态绑定到其 is 属性来动态地在多个组件之间切换

new Vue({
el: 'body',
data: {
currentView: 'home'
},
components: {
home: { /* ... */ },
posts: { /* ... */ },
archive: { /* ... */ }
}
})
<component :is="currentView">
<!-- component changes when vm.currentview changes! -->
</component>

keep-alive

如果你想让切换出的组件保持活动状态,以便你可以保留其状态或避免重新渲染,你可以添加一个 keep-alive 指令参数

<component :is="currentView" keep-alive>
<!-- inactive components will be cached! -->
</component>

activate 钩子

切换组件时,传入的组件可能需要执行一些异步操作,然后才能将其交换进来。为了控制组件交换的时机,在传入的组件上实现 activate 钩子

Vue.component('activate-example', {
activate: function (done) {
var self = this
loadDataAsync(function (data) {
self.someData = data
done()
})
}
})

请注意,activate 钩子只在动态组件交换或静态组件的初始渲染期间生效 - 它不会影响使用实例方法的手动插入。

transition-mode

transition-mode 参数属性允许你指定两个动态组件之间的过渡应该如何执行。

默认情况下,传入和传出组件的过渡同时发生。此属性允许你配置另外两种模式

示例

<!-- fade out first, then fade in -->
<component
:is="view"
transition="fade"
transition-mode="out-in">
</component>
.fade-transition {
transition: opacity .3s ease;
}
.fade-enter, .fade-leave {
opacity: 0;
}

其他

组件和 v-for

你可以直接在自定义组件上使用 v-for,就像任何普通元素一样

<my-component v-for="item in items"></my-component>

但是,这不会将任何数据传递给组件,因为组件拥有自己的独立作用域。为了将迭代数据传递到组件中,我们也应该使用 props

<my-component
v-for="item in items"
:item="item"
:index="$index">
</my-component>

不自动将 item 注入组件的原因是,这会使组件与 v-for 的工作方式紧密耦合。明确其数据来源,使组件在其他情况下可重用。

编写可重用组件

编写组件时,要牢记你是否打算稍后在其他地方重用此组件。对于一次性组件,彼此之间存在一些紧密耦合是可以的,但可重用组件应该定义一个干净的公共接口。

Vue.js 组件的 API 本质上分为三个部分 - props、事件和插槽

使用 v-bindv-on 的专用简写语法,可以在模板中清晰简洁地表达意图

<my-component
:foo="baz"
:bar="qux"
@event-a="doThis"
@event-b="doThat">
<!-- content -->
<img slot="icon" src="...">
<p slot="main-text">Hello!</p>
</my-component>

异步组件

在大型应用程序中,我们可能需要将应用程序划分为更小的块,并且只有在实际需要时才从服务器加载组件。为了使这更容易,Vue.js 允许你将组件定义为一个异步解析组件定义的工厂函数。Vue.js 只有在组件实际需要渲染时才会触发工厂函数,并将结果缓存起来以供将来重新渲染。例如

Vue.component('async-example', function (resolve, reject) {
setTimeout(function () {
resolve({
template: '<div>I am async!</div>'
})
}, 1000)
})

工厂函数接收一个 resolve 回调函数,当从服务器检索到组件定义时,应该调用该回调函数。你也可以调用 reject(reason) 来表示加载失败。这里的 setTimeout 只是为了演示;如何检索组件完全取决于你。一种推荐的方法是将异步组件与 Webpack 的代码拆分功能 结合使用

Vue.component('async-webpack-example', function (resolve) {
// this special require syntax will instruct webpack to
// automatically split your built code into bundles which
// are automatically loaded over ajax requests.
require(['./my-async-component'], resolve)
})

资产命名约定

一些资产,例如组件和指令,以 HTML 属性或 HTML 自定义标签的形式出现在模板中。由于 HTML 属性名和标签名是**不区分大小写**的,我们经常需要使用 kebab-case 而不是 camelCase 来命名我们的资产,这可能有点不方便。

Vue.js 实际上支持使用 camelCase 或 PascalCase 命名你的资产,并在模板中自动将它们解析为 kebab-case(类似于 props 的名称转换)

// in a component definition
components: {
// register using camelCase
myComponent: { /*... */ }
}
<!-- use dash case in templates -->
<my-component></my-component>

这与 ES6 对象字面量简写 很好地配合使用

// PascalCase
import TextBox from './components/text-box';
import DropdownMenu from './components/dropdown-menu';
export default {
components: {
// use in templates as <text-box> and <dropdown-menu>
TextBox,
DropdownMenu
}
}

递归组件

组件可以在其自己的模板中递归调用自身,但是,只有当它具有 name 选项时才能这样做

var StackOverflow = Vue.extend({
name: 'stack-overflow',
template:
'<div>' +
// recursively invoke self
'<stack-overflow></stack-overflow>' +
'</div>'
})

像上面的组件将导致“最大堆栈大小超出”错误,因此请确保递归调用是有条件的。当你使用 Vue.component() 全局注册组件时,全局 ID 会自动设置为组件的 name 选项。

片段实例

当你使用 template 选项时,模板的内容将替换 Vue 实例挂载的元素。因此,建议在模板中始终只有一个根级别的普通元素。

不要使用这样的模板

<div>root node 1</div>
<div>root node 2</div>

更喜欢这样

<div>
I have a single root node!
<div>node 1</div>
<div>node 2</div>
</div>

有多种情况会导致 Vue 实例变成**片段实例**

  1. 模板包含多个顶级元素。
  2. 模板只包含纯文本。
  3. 模板只包含另一个组件(它可能本身就是一个片段实例)。
  4. 模板包含一个元素指令,例如 <partial> 或 vue-router 的 <router-view>
  5. 模板根节点有一个流控制指令,例如 v-ifv-for

原因是以上所有情况都会导致实例具有未知数量的顶级元素,因此它必须将 DOM 内容作为片段进行管理。片段实例仍然会正确渲染内容。但是,它**不会**有根节点,并且它的 $el 将指向一个“锚节点”,它是一个空的文本节点(或在调试模式下是一个注释节点)。

更重要的是,非流控制指令、非道具属性和组件元素上的过渡将被忽略,因为没有根元素可以绑定它们。

<!-- doesn't work due to no root element -->
<example v-show="ok" transition="fade"></example>
<!-- props work -->
<example :prop="someData"></example>
<!-- flow control works, but without transitions -->
<example v-if="ok"></example>

当然,片段实例有其有效的用例,但通常来说,为你的组件模板提供一个单一的、普通的根元素是一个好主意。这确保了组件元素上的指令和属性被正确地传递,并且也导致了略微更好的性能。

内联模板

当子组件上存在inline-template特殊属性时,组件将使用其内部内容作为其模板,而不是将其视为分布式内容。这允许更灵活的模板编写。

<my-component inline-template>
<p>These are compiled as the component's own template</p>
<p>Not parent's transclusion content.</p>
</my-component>

然而,inline-template使你的模板范围更难理解,并且使组件的模板编译不可缓存。作为最佳实践,最好使用template选项在组件内部定义模板。