4.组件
4.1 组件的含义
组件是可复用的 Vue 实例,且带有一个名字。例如下面定义了一个 <button-counter>
组件:
// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
组件是可复用的 Vue 实例,所以它们与
new Vue
接收相同的选项,例如data
、computed
、watch
、methods
以及生命周期钩子等。
每用一次组件,就会有一个它的新实例被创建,因此下面定义的三个按钮会各自维护它独立的count
:
<div id="components-demo">
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
</div>
一个组件的 data
选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝:
data: function () {
return {
count: 0
}
}
如果 Vue 没有这条规则,点击一个按钮就可能会像如下代码一样影响到其它所有实例:
注意每个组件必须只有一个根元素。
4.2 组件的命名
当直接在 DOM 中使用一个组件 (而不是在字符串模板或单文件组件) 的时候,我们强烈推荐遵循 W3C 规范中的自定义组件名 (字母全小写且必须包含一个连字符)。这会帮助你避免和当前以及未来的 HTML 元素相冲突。
定义组件名的方式有两种:
短横线——当使用 kebab-case (短横线分隔命名) 定义一个组件时,引用该元素时必须也使用 kebab-case,例如
<my-component-name>
。Vue.component('my-component-name', { /* ... */ })
驼峰法(首字母大写)——当使用 PascalCase (首字母大写命名) 定义一个组件时,引用该元素时两种命名法都可以使用。
Vue.component('MyComponentName', { /* ... */ })
<my-component-name>
和<MyComponentName>
都可以使用。不过,直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的。
4.3 组件的组织
通常一个应用会以一棵嵌套的组件树的形式来组织,例如,你可能会有页头、侧边栏、内容区等组件,每个组件又包含了其它的像导航链接、博文之类的组件。
为了能在模板中使用,这些组件必须先注册以便 Vue 能够识别。有两种组件的注册类型:
全局注册
全局注册通过
Vue.component
执行:Vue.component('my-component-name', { // ... options ... })
全局注册的组件可以用在其被注册之后的任何 (通过
new Vue
) 新创建的 Vue 根实例,也包括其组件树中的所有子组件的模板中。
局部注册
局部注册通过一个普通的js对象来定义组件
var ComponentA = { /* ... */ } var ComponentB = { /* ... */ } var ComponentC = { /* ... */ } new Vue({ el: '#app', components: { 'component-a': ComponentA, 'component-b': ComponentB } })
对于
components
对象中的每个 property 来说,其 property 名就是自定义元素的名字,其 property 值就是这个组件的选项对象。局部注册的组件在子组件(意即非根组件?)中不可用
例如,如果你希望
ComponentA
在ComponentB
中可用,则你需要这样写:var ComponentA = { /* ... */ } var ComponentB = { components: { 'component-a': ComponentA }, // ... }
4.4 模块系统
通过 import
/require
可以使用一个模块系统。
局部注册
往往我们推荐创建一个components
目录,并将每个组件放置在其各自的文件中,然后在局部注册之前通过import语句导入每个你想使用的组件。
例如,在一个ComponentC.js
或ComponentC.vue
文件中:
import ComponentA from './ComponentA'
import ComponentB from './ComponentB'
export default {
components: {
ComponentA,
ComponentB
},
// ...
}
此时 ComponentA
和 ComponentB
便可以在 ComponentC
的模板中使用了。
全局注册
有些组件会被各个组件频繁地使用,它们有时被称为“基础组件”,因此为了方便,可能通过webpack的require.context
方法对组件进行全局注册。
比如,以下在src/main.js
这样的应用入口文件中全局导入基础组件:
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'
const requireComponent = require.context(
// 其组件目录的相对路径
'./components',
// 是否查询其子目录
false,
// 匹配基础组件文件名的正则表达式
/Base[A-Z]\w+\.(vue|js)$/
)
requireComponent.keys().forEach(fileName => {
// 获取组件配置
const componentConfig = requireComponent(fileName)
// 获取组件的 PascalCase 命名
const componentName = upperFirst(
camelCase(
// 获取和目录深度无关的文件名
fileName
.split('/')
.pop()
.replace(/\.\w+$/, '')
)
)
// 全局注册组件
Vue.component(
componentName,
// 如果这个组件选项是通过 `export default` 导出的,
// 那么就会优先使用 `.default`,
// 否则回退到使用模块的根。
componentConfig.default || componentConfig
)
})
记住全局注册的行为必须在根 Vue 实例 (通过
new Vue
) 创建之前发生。
4.5 通过props
选项向子组件传递数据
props
是在组件上注册的一些自定义 attribute,当一个值传递给一个 props
attribute 的时候,它就变成了那个组件实例的一个 property。
例如,对于一个博文组件blog-post
,在应用中引用该组件时为了给博文传递一个标题,可以在该组件的 props
选项中设置一个title
属性:
Vue.component('blog-post', {
props: ['title'],
template: '<h3>{{ title }}</h3>'
})
此后便可以在博文组件blog-post
中访问外层应用通过props
传递的title
值,就像访问组件自身 data
中的值一样。
对于外层应用而言,一个博文子组件中的 prop 被注册之后,就可以像这样把数据作为一个自定义 attribute 传递给子组件:
<blog-post title="My journey with Vue"></blog-post>
<blog-post title="Blogging with Vue"></blog-post>
<blog-post title="Why Vue is so fun"></blog-post>
有时我们需要给子组件传递一个js对象的多个属性值,此时可以选择直接将该对象传给子组件,这样同时也使得如果在外层给该js对象添加了一个新的property,那么它会自动地在子组件中用。
4.5.1 prop的大小写
HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你在 DOM 中使用模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名:
组件
Vue.component('blog-post', { // 在 JavaScript 中是 camelCase 的 props: ['postTitle'], template: '<h3>{{ postTitle }}</h3>' })
应用示例
<!-- 在 HTML 中是 kebab-case 的 --> <blog-post post-title="hello!"></blog-post>
4.5.2 prop的类型
通常你还希望每个 prop 都有指定的值类型,这时便可以以对象形式列出 prop,这些 property 的名称和值分别是 prop 各自的名称和类型:
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise // or any other constructor
}
这样不仅为你的组件提供了文档,还会在它们遇到错误的类型时从浏览器的 JavaScript 控制台提示用户。
4.5.3 给prop传值
静态与动态传值:
静态传值:传过去的实际是一个字符串
<blog-post title="My journey with Vue"></blog-post>
动态赋值——通过
v-bind
,虽然也使用了引号,但实际传过去的是js表达式<!-- 动态赋予一个变量的值 --> <blog-post v-bind:title="post.title"></blog-post> <!-- 动态赋予一个复杂表达式的值 --> <blog-post v-bind:title="post.title + ' by ' + post.author.name" ></blog-post>
<!-- 即便 `42` 是静态的,使用 `v-bind` 告诉了 Vue 这是一个 js 表达式而不是一个字符串 --> <blog-post v-bind:likes="42"></blog-post> <!-- 即便 `false` 是静态的,使用 `v-bind` 告诉了 Vue 这是一个 js 表达式而不是一个字符串 --> <blog-post v-bind:is-published="false"></blog-post>
当prop是布尔类型时,只要该prop出现在html标签内但没有显式赋值,都意味着其值为true。
<!-- 该 prop 没有值时,意味着 `true` -->
<blog-post is-published></blog-post>
当要传入一个对象的所有property时,可以使用不带参数的v-bind
。例如,对于一个给定的对象 post
:
post: {
id: 1,
title: 'My Journey with Vue'
}
下面的模板:
<blog-post v-bind="post"></blog-post>
等价于:
<blog-post
v-bind:id="post.id"
v-bind:title="post.title"
></blog-post>
4.5.4 prop的单向数据流
所有的 prop 都会在其父子 prop 之间形成一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。
这样的设计可能防止在子组件中意外变更父级组件的状态,从而导致应用的数据流向难以理解。此外,如果你在一个子组件内部改变引用的父组件的 prop,Vue 会在浏览器的控制台中发出警告。
对于以下两种场景:
使用 prop 传递一个初始值,子组件接下来希望将其作为一个本地的数据来使用。此时最好定义一个本地的 data property 并将这个 prop 用作其初始值,例如如下组件
props: ['initialCounter'], data: function () { return { counter: this.initialCounter } }
这个 prop 以一种原始的值传入且需要进行转换。此时最好使用这个 prop 的值来定义一个计算属性:
props: ['size'], computed: { normalizedSize: function () { return this.size.trim().toLowerCase() } }
4.5.5 prop验证
prop 的传值还可以进行更深层次的验证,例如:
Vue.component('my-component', {
props: {
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
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 ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
})
当 prop 验证失败的时候,(开发环境构建版本的) Vue 将会产生一个控制台的警告。
对于上述的type字段,它可以是
原生构造函数:
String
Number
Boolean
Array
Object
Date
Function
Symbol
自定义的构造函数:
如,给定下列现成的构造函数:
function Person (firstName, lastName) { this.firstName = firstName this.lastName = lastName }
你可以使用下述来验证
author
prop 的值是否是通过new Person
创建的:Vue.component('blog-post', { props: { author: Person } })
4.5.6 非prop的attribue?
......这说的什么玩意啊,官方文档也太烂了吧......
4.6 监听子组件事件
父级组件可以像处理 native DOM 事件一样通过 v-on
监听子组件实例的任意事件,同时子组件可以通过调用内建的 $emit
方法并传入事件名称来触发一个事件:
引用了子组件
blog-post
的外层应用(父组件):<blog-post ... v-on:enlarge-text="postFontSize += 0.1" ></blog-post>
子组件
blog-post
的定义代码:<button v-on:click="$emit('enlarge-text')"> Enlarge text </button>
有了这个 v-on:enlarge-text="postFontSize += 0.1"
监听器,父级组件就会接收该事件并更新 postFontSize
的值。
4.6.1 使用事件抛出一个值
有时我们需要用一个事件来抛出一个特定的值,例如我们可能想让子组件 <blog-post>
决定它的文本要放大多少,这时可以使用 $emit
的第二个参数来提供这个值,然后在父级组件监听该事件时通过 $event
访问到被抛出的这个值:
子组件
blog-post
:<button v-on:click="$emit('enlarge-text', 0.1)"> Enlarge text </button>
父组件(外层应用):
<blog-post ... v-on:enlarge-text="postFontSize += $event" ></blog-post>
或者,如果这个事件处理函数是一个方法,那么这个值将会作为第一个参数传入这个方法:
<template>
<blog-post
// ...
v-on:enlarge-text="onEnlargeText"
></blog-post>
</template>
<script>
// ...
methods: {
onEnlargeText: function (enlargeAmount) {
this.postFontSize += enlargeAmount
}
}
</script>
4.6.2 在组件上使用v-model
对于自定义事件,它也可以用于创建支持v-model
的“自定义输入组件”。
实际上,
<input v-model="searchText">
等价于
<input
v-bind:value="searchText"
v-on:input="searchText = $event.target.value"
>
当用在组件上时,v-model
则会这样:
<custom-input v-bind:value="searchText" v-on:input="searchText = $event"></custom-input>
为了让这样的组件正常工作,这个组件内的 <input>
必须:
- 将其
value
attribute 绑定到一个名叫value
的 prop 上 - 在其
input
事件被触发时,将新的值通过自定义的input
事件抛出
写成代码之后如下:
Vue.component('custom-input', {
props: ['value'],
template: `
<input
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
`
})
此时 v-model
就可以在这个组件上正常运行了:
<custom-input v-model="searchText"></custom-input>
4.7 插槽:分发内容
4.7.1 定义
Vue 将<slot>
元素作为承载分发内容的出口。
比如,对于一个 <navigation-link>
组件:
Vue.component('navigation-link', {
// ...
template: `
<a v-bind:href="url" class="nav-link">
<slot></slot>
</a>
`
})
该组件可以如下使用:
<navigation-link url="/profile">
Your Profile
</navigation-link>
当上述代码中的组件渲染时,组件源代码中 <slot></slot>
对应的内容会被替换为“Your Profile”。反之,假设组件 <navigation-link>
的 template
中没有包含一个 <slot>
元素,那么意味着该组件起始标签和结束标签之间的任何内容都会被舍弃。
实际上,除文本外,插槽内可以包含任何模板代码,即:
- 文本
- HTML
- 其他组件
4.7.2 插槽默认值
对于一个含插槽的组件,在<slot></slot>
闭合标签内设置的内容会在该组件不含内容时作为默认值自动渲染出来。
例如在一个 <submit-button>
组件中,我们可能希望其中定义的 <button>
内绝大多数情况下都渲染文本“Submit”,此时可以将"Submit"放在 <slot>
标签内即可,它将被视为默认值:
<submit-button>
组件:<button type="submit"> <slot>Submit</slot> </button>
使用
<submit-button>
子组件的父组件:<submit-button></submit-button>
上述中默认值“Submit”将会被自动渲染,该代码相当于:
<button type="submit"> Submit </button>
4.7.3 具名插槽
<slot>
元素有一个name
属性,该属性可以用来定义具名插槽(不带 name
的 <slot>
拥有默认名“default”)。
例如,下面是一个含具名插槽的 base-layout
组件定义:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
在使用含具名插槽的组件时,向其中一个具名插槽分发内容需要使用一个 <template>
元素,并在其上结合 v-slot
指令提供插槽的名称:
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<!-- 这些没有被包裹在带有`v-slot`的`<template>`中的内容都会被视为默认插槽的内容 -->
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
上述代码渲染的效果即为:
<div class="container">
<header>
<h1>Here might be a page title</h1>
</header>
<main>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</main>
<footer>
<p>Here's some contact info</p>
</footer>
</div>
当然,如果希望对默认插槽使用更明确的表示,可以使用
v-slot:default
:<base-layout> <template v-slot:header> <h1>Here might be a page title</h1> </template> <template v-slot:default> <p>A paragraph for the main content.</p> <p>And another one.</p> </template> <template v-slot:footer> <p>Here's some contact info</p> </template> </base-layout>
// TO READ
4.7.4 插槽的作用域
插槽跟组件模板的其它地方一样可以访问相同的实例 property ,但不能访问 <navigation-link>
的作用域。例如上述的 url
在插槽中是访问不到的:
<navigation-link url="/profile">
Logged in as {{ user.name }}
Clicking here will send you to: {{ url }}
<!--
这里的 `url` 会是 undefined,因为其 (指该插槽的) 内容是
_传递给_ <navigation-link> 的而不是
在 <navigation-link> 组件内部定义的。
-->
</navigation-link>
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
如果确实需要让插槽的内容能够访问组件中的数据,可以将该数据作为<slot>
元素的一个属性绑定上去,此时该属性被称为插槽prop。比如,对于一个 <current-user>
组件:
Vue.component('current-user', {
// ...
template: `
<span>
<slot>{{ user.lastName }}</slot>
</span>
`
})
如果不使用模板的默认值,即希望使用firstName时,下述代码并不会正常工作,因为只有 <current-user>
组件可以访问到 user
,而我们提供的内容是在父级渲染的:
<current-user>
{{ user.firstName }}
</current-user>
为了让 user
在父级的插槽内容中可用,可以将 user
作为 <slot>
元素的一个 attribute 绑定上去:
<span>
<slot v-bind:user="user">
{{ user.lastName }}
</slot>
</span>
如此一来我们便可以使用带值的 v-slot
来定义一个插槽 prop 的名字——比如下例中将包含所有插槽 prop 的对象命名为 slotProps
:
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
</current-user>
简写
当只有默认插槽时(如上例),可以把表示插槽prop的v-slot
直接用在组件上:
<current-user v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</current-user>
上面的default可以进一步省略——不带参数的 v-slot
被假定对应默认插槽:
<current-user v-slot="slotProps">
{{ slotProps.user.firstName }}
</current-user>
注意默认插槽的缩写语法不能和具名插槽混用,这会导致作用域不明确:
<!-- 无效,会导致警告 -->
<current-user v-slot="slotProps">
{{ slotProps.user.firstName }}
<template v-slot:other="otherSlotProps">
slotProps is NOT available here
</template>
</current-user>
因此,只要出现多个插槽,应该始终为所有的插槽使用完整的基于 <template>
的语法:
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
<template v-slot:other="otherSlotProps">
...
</template>
</current-user>
4.7.5 插槽prop的原理(说的什么玩意儿......)
插槽prop的内部原理是将你的插槽内容包裹在一个拥有单个参数的函数里:
function (slotProps) {
// 插槽内容
}
这意味着 v-slot
的值实际上可以是任何能够作为函数定义中的参数的 js 表达式。
所以在支持的环境下 (单文件组件或现代浏览器)也可以使用 ES2015 解构来传入具体的插槽 prop,如下:
<current-user v-slot="{ user }">
{{ user.firstName }}
</current-user>
这可以使模板更简洁,尤其是在该插槽提供了多个 prop 的时候。它同样开启了 prop 重命名等其它可能,比如将 user
重命名为 person
:
<current-user v-slot="{ user: person }">
{{ person.firstName }}
</current-user>
你甚至可以定义默认内容用于插槽 prop 是 undefined 的情形:
<current-user v-slot="{ user = { firstName: 'Guest' } }">
{{ user.firstName }}
</current-user>
4.7.6 其他
动态插槽名
动态指令参数也可以用在 v-slot
上,来定义动态的插槽名:
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
</base-layout>
具名插槽的缩写
跟 v-on
和 v-bind
一样,v-slot
也有缩写,即把参数之前的所有内容 (v-slot:
) 替换为字符 #
。
例如 v-slot:header
可以被重写为 #header
:
<base-layout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template #footer>
<p>Here's some contact info</p>
</template>
</base-layout>
此外,和其它指令一样,该缩写只在其有参数的时候才可用,因此以下语法是无效的:
<!-- 这样会触发一个警告 -->
<current-user #="{ user }">
{{ user.firstName }}
</current-user>
花样用法
插槽 prop 允许我们将插槽转换为可复用的模板,这些模板可以基于输入的 prop 渲染出不同的内容。
这在设计封装数据逻辑同时允许父级组件自定义部分布局的可复用组件时是最有用的,例如,我们要实现一个 <todo-list>
组件,它是一个列表且包含布局和过滤逻辑:
<ul>
<li
v-for="todo in filteredTodos"
v-bind:key="todo.id"
>
{{ todo.text }}
</li>
</ul>
我们可以将每个 todo 作为父级组件的插槽,以此通过父级组件对其进行控制,然后将 todo
作为一个插槽 prop 进行绑定:
<ul>
<li
v-for="todo in filteredTodos"
v-bind:key="todo.id"
>
<!--
我们为每个 todo 准备了一个插槽,
将 `todo` 对象作为一个插槽的 prop 传入。
-->
<slot name="todo" v-bind:todo="todo">
<!-- 后备内容 -->
{{ todo.text }}
</slot>
</li>
</ul>
现在当我们使用 <todo-list>
组件的时候,我们可以选择为 todo 定义一个不一样的 <template>
作为替代方案,并且可以从子组件获取数据:
<todo-list v-bind:todos="todos">
<template v-slot:todo="{ todo }">
<span v-if="todo.isComplete">✓</span>
{{ todo.text }}
</template>
</todo-list>
这只是作用域插槽用武之地的冰山一角,想了解更多现实生活中的作用域插槽的用法,我们推荐浏览诸如 Vue Virtual Scroller、Vue Promised 和 Portal Vue 等库。
4.8 组件的v-model
一个组件上的 v-model
默认会利用该组件名为 value
的 prop 和名为 input
的事件,但是像单选框、复选框等类型的输入控件可能会将 value
attribute 用于不同的目的。model
选项可以用来避免这样的冲突:
Vue.component('base-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
>
`
})
现在在这个组件上使用 v-model
的时候:
<base-checkbox v-model="lovingVue"></base-checkbox>
这里的 lovingVue
的值将会传入这个名为 checked
的 prop,同时当 <base-checkbox>
触发一个 change
事件并附带一个新的值的时候,这个 lovingVue
的 property 将会被更新。
注意你仍然需要在组件的
props
选项里声明checked
这个 prop。
4.9 事件
4.9.1 自定义事件的命名
对于自定义事件,始终推荐使用 kebab-case 的事件名。
不同于组件和 prop,事件名不会被用作一个 JavaScript 变量名或 property 名,所以就没有理由使用 camelCase 或 PascalCase 了。并且
v-on
事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以v-on:myEvent
将会变成v-on:myevent
——导致myEvent
不可能被监听到。
4.9.2 $listeners
属性
当需要在一个组件的根元素上直接监听一个原生事件时,你当然可以使用 v-on
的 .native
修饰符:
<base-input v-on:focus.native="onFocus"></base-input>
此时,当监听的是一个类似 <input>
的非常特定的元素时,实际情况可能并不如意。比如上述 <base-input>
组件可能做了如下重构——根元素实际上是一个 <label>
元素:
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
</label>
这种情况下父级的 .native
监听器将静默失败——它不会产生任何报错,但是 onFocus
处理函数不会被调用。
为了解决这个问题,Vue 提供了一个 $listeners
property,它是一个对象,里面包含了作用在这个组件上的所有监听器,即:
{
focus: function (event) { /* ... */ }
input: function (value) { /* ... */ },
}
有了$listeners
,就可以配合 v-on="$listeners"
将所有的事件监听器指向这个组件的某个特定的子元素。
比如,对于类似 <input>
、希望它可以配合 v-model
来工作的组件来说,为这些监听器创建一个类似下述 inputListeners
的计算属性通常是非常有用的:
Vue.component('base-input', {
inheritAttrs: false,
props: ['label', 'value'],
computed: {
inputListeners: function () {
var vm = this
// `Object.assign` 将所有的对象合并为一个新对象
return Object.assign({},
// 我们从父级添加所有的监听器
this.$listeners,
// 然后我们添加自定义监听器,
// 或覆写一些监听器的行为
{
// 这里确保组件配合 `v-model` 的工作
input: function (event) {
vm.$emit('input', event.target.value)
}
}
)
}
},
template: `
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on="inputListeners"
>
</label>
`
})
这样 <base-input>
组件是一个完全透明的包裹器了,也就是说它可以完全像一个普通的 <input>
元素一样来使用:所有跟它相同的 attribute 和监听器都可以工作,不必再使用 .native
监听器。
4.9.3 .sync
修饰符
在有些情况下,我们可能需要对一个 prop 进行“双向绑定”,通常推荐以 update:myPropName
的模式触发事件来实现。举个例子,在一个包含 title
prop 的假设的组件中,我们可以用以下方法表达对其赋新值的意图:
this.$emit('update:title', newTitle)
然后父组件可以监听那个事件并根据需要更新一个本地的数据 property,比如:
<text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
></text-document>
故而,为了方便起见,我们为这种模式提供一个缩写,即 .sync
修饰符:
<text-document v-bind:title.sync="doc.title"></text-document>
当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync
修饰符和 v-bind
配合使用:
<text-document v-bind.sync="doc"></text-document>
这样会把 doc
对象中的每一个 property (如 title
) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on
监听器。
注意:
- 带有
.sync
修饰符的v-bind
不能和表达式一起使用 (例如v-bind:title.sync=”doc.title + ‘!’”
是无效的)。取而代之的是,你只能提供你想要绑定的 property 名,类似v-model
。- 将
v-bind.sync
用在一个字面量的对象上,例如v-bind.sync=”{ title: doc.title }”
,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。
4.10 访问元素/组件
在绝大多数情况下,我们最好不要触达另一个组件实例内部或手动操作 DOM 元素,不过也确实在一些情况下做这些事情是合适的。
4.10.1 访问根实例:$root
在每个 new Vue
实例的子组件中,其根实例可以通过 $root
property 进行访问。例如,对于一个根实例
// Vue 根实例
new Vue({
data: {
foo: 1
},
computed: {
bar: function () { /* ... */ }
},
methods: {
baz: function () { /* ... */ }
}
})
所有的子组件都可以将这个实例作为一个全局 store 来访问或使用:
// 获取根组件的数据this.$root.foo
// 写入根组件的数据this.$root.foo = 2
// 访问根组件的计算属性this.$root.bar
// 调用根组件的方法this.$root.baz()
注:对于 demo 或非常小型的有少量组件的应用来说这是很方便的。不过这个模式扩展到中大型应用来说就不然了。因此在绝大多数情况下,我们强烈推荐使用 Vuex 来管理应用的状态。
4.10.2 访问父实例:$parent
和 $root
类似,$parent
property 可以用来从一个子组件访问父组件的实例。$parent
提供了一种可以在后期随时触达父级组件的机会,以替代将数据以 prop 的方式传入子组件的方式。
在绝大多数情况下,触达父级组件会使得你的应用更难调试和理解,尤其是当你变更了父级组件的数据的时候——当我们稍后回看那个组件时,很难找出那个变更是从哪里发起的。
另外在一些可能适当的时候,你需要特别地共享一些组件库。举个例子,在和 JavaScript API 进行交互而不渲染 HTML 的抽象组件内,诸如这些假设性的 Google 地图组件:
<google-map>
<google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map>
这个 <google-map>
组件可以定义一个 map
property,所有的子组件都需要访问它。在这种情况下 <google-map-markers>
可能想要通过类似 this.$parent.getMap
的方式访问那个地图,以便为其添加一组标记。
请留意,尽管如此,通过这种模式构建出来的那个组件的内部仍然是容易出现问题的。比如,设想一下我们添加一个新的 <google-map-region>
组件,当 <google-map-markers>
在其内部出现的时候,只会渲染那个区域内的标记:
<google-map>
<google-map-region v-bind:shape="cityBoundaries">
<google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map-region>
</google-map>
那么在 <google-map-markers>
内部你可能发现自己需要一些类似这样的 hack:
var map = this.$parent.map || this.$parent.$parent.map
很快它就会失控。这也是我们针对需要向任意更深层级的组件提供上下文信息时推荐依赖注入的原因。
4.10.3 访问子组件实例/子元素:$ref
尽管存在 prop 和事件,有的时候你可能仍需要在 js 里直接访问一个子组件。为了达到这个目的,你可以通过 ref
这个 attribute 为子组件赋予一个 ID 引用。例如:
<base-input ref="usernameInput"></base-input>
现在,在你已经定义了这个 ref
的组件里,你可以使用:
this.$refs.usernameInput
来访问这个 <base-input>
实例,以便不时之需。
示例:比如程序化地从一个父级组件聚焦这个输入框。在刚才那个例子中,该 <base-input>
组件也可以使用一个类似的 ref
提供对内部这个指定元素的访问,例如:
<input ref="input">
甚至可以通过其父级组件定义方法:
methods: {
// 用来从父级组件聚焦输入框
focus: function () {
this.$refs.input.focus()
}
}
这样就允许父级组件通过下面的代码聚焦 <base-input>
里的输入框:
this.$refs.usernameInput.focus()
当 ref
和 v-for
一起使用的时候,你得到的 ref 将会是一个包含了对应数据源的这些子组件的数组。
$refs
只会在组件渲染完成之后生效,并且它们不是响应式的。这仅作为一个用于直接操作子组件的“逃生舱”——你应该避免在模板或计算属性中访问 $refs
。
4.10.4 依赖注入
使用 $parent
无法很好地扩展到更深层级的父级嵌套组件上,这便是依赖注入的用武之地了。依赖注入用到了两个新的实例选项:provide
和 inject
。
provide
选项允许我们指定我们想要提供给后代组件的数据/方法。inject
选项用于在后代组件里接收指定的 property:inject: ['getMap']
比如,之前在我们描述访问父级组件实例的时候,展示过一个类似这样的例子:
<google-map>
<google-map-region v-bind:shape="cityBoundaries">
<google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map-region>
</google-map>
在这个组件里,所有 <google-map>
的后代都需要访问一个 getMap
方法,以便知道要跟哪个地图进行交互。此时provide
和inject
分别如下:
provide: function () {
return {
getMap: this.getMap
}
}
inject: ['getMap']
相比
$parent
来说,这个用法可以让我们在任意后代组件中访问getMap
,而不需要暴露整个<google-map>
实例。这允许我们更好的持续研发该组件,而不需要担心我们可能会改变/移除一些子组件依赖的东西。同时这些组件之间的接口是始终明确定义的,就和props
一样。
不过,依赖注入还是有负面影响的:
它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。
同时所提供的 property 是非响应式的。
这是出于设计的考虑,因为使用它们来创建一个中心化规模化的数据跟使用
$root
做这件事都是不够好的。如果你想要共享的这个 property 是你的应用特有的,而不是通用化的,或者如果你想在祖先组件中更新所提供的数据,那么这意味着你可能需要换用一个像 Vuex 这样真正的状态管理方案了。
4.11 组件的循环引用
4.11.1 递归组件
组件是可以在它们自己的模板中调用自身的,不过它们只能通过 name
选项来做这件事:
name: 'unique-name-of-my-component'
当使用 Vue.component
全局注册一个组件时,这个全局的 ID 会自动设置为该组件的 name
选项:
Vue.component('unique-name-of-my-component', {
// ...
})
稍有不慎,递归组件就可能导致无限循环:
name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'
类似上述的组件将会导致“max stack size exceeded”错误,所以请确保递归调用是条件性的 (例如使用一个最终会得到 false
的 v-if
)。
4.11.2 组件之间的循环引用
......好像没必要单独记录
4.12 动态组件
有的时候,在不同组件之间进行动态切换是非常有用的,比如在一个多标签的界面里:
上述内容可以通过 Vue 的 <component>
元素加一个特殊的 is
attribute 来实现:
<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component v-bind:is="currentTabComponent"></component>
<!-- `currentTabComponent` 可以包括“已注册组件的名字”或“一个组件的选项对象”。 -->
需要注意的是,这个 attribute 也可以用于常规 HTML 元素,但此时这些元素将被视为组件——这意味着所有的 attribute 都会作为 DOM attribute 被绑定。对于像 value
这样的 property,若想让其如预期般工作,你需要使用 .prop
修饰器。
默认情况下,在这些组件之间切换时,新标签会重新渲染——Vue创建了一个新的currentTabComponent
实例,如果希望这些标签的组件实例能够在它们第一次被创建的时候就缓存下来,可以用一个<keep-alive>
元素将动态组件包裹起来:
<!-- 失活的组件将会被缓存!-->
<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>
注意这个
<keep-alive>
要求被切换到的组件都有自己的名字,不论是通过组件的name
选项还是局部/全局注册。
4.13 异步组件
在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为应对这种场景,Vue 允许你以一个工厂函数的方式定义组件,该工厂函数在解析你的组件定义时会异步执行,Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。
例如,下面的示例中工厂函数会收到一个 resolve
回调,这个回调函数会在你从服务器得到组件定义的时候被调用(你也可以调用 reject(reason)
来表示加载失败):
Vue.component('async-example', function (resolve, reject) {
setTimeout(function () {
// 向 `resolve` 回调传递组件定义
resolve({
template: '<div>I am async!</div>'
})
}, 1000)
})
上面的 setTimeout
是为了演示用的,如何获取组件取决于程序员,一个推荐的做法是将异步组件和 webpack 的 code-splitting 功能一起配合使用:
Vue.component('async-webpack-example', function (resolve) {
// 这个特殊的 `require` 语法将会告诉 webpack
// 自动将你的构建代码切割成多个包,这些包
// 会通过 Ajax 请求加载
require(['./my-async-component'], resolve)
})
此外,你也可以在工厂函数中返回一个 Promise
。因而把 webpack 2 和 ES2015 语法加在一起的话,可以这样使用动态导入:
Vue.component(
'async-webpack-example',
// 这个动态导入会返回一个 `Promise` 对象。
() => import('./my-async-component')
)
当使用局部注册的时候,你也可以直接提供一个返回
Promise
的函数:new Vue({ // ... components: { 'my-component': () => import('./my-async-component') } })
4.14 函数式组件
一个函数式组件无状态(没有响应式数据),无实例(没有this上下文),它的定义如下:
Vue.component('my-component', {
functional: true,
// Props 是可选的
props: {
// ...
},
// 为了弥补缺少的实例
// 提供第二个参数作为上下文
render: function (createElement, context) {
// ...
}
})
在 2.5.0 及以上版本中,如果你使用了单文件组件,那么基于模板的函数式组件可以这样声明:
<template functional>
</template>
函数式组件需要的一切都是通过 context
参数传递,它是一个包括如下字段的对象:
props
:提供所有 prop 的对象children
:VNode 子节点的数组slots
:一个函数,返回了包含所有插槽的对象scopedSlots
:(2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。data
:传递给组件的整个数据对象,作为createElement
的第二个参数传入组件parent
:对父组件的引用listeners
:(2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是data.on
的一个别名。injections
:(2.3.0+) 如果使用了inject
选项,则该对象包含了应当被注入的 property。
在添加 functional: true
之后,需要更新我们的锚点标题组件的渲染函数,为其增加 context
参数,并将 this.$slots.default
更新为 context.children
,然后将 this.level
更新为 context.props.level
。
因为函数式组件只是函数,所以渲染开销也低很多。
在作为包装组件时函数式组件可能非常有用,比如:
- 程序化地在多个组件中选择一个来代为渲染;
- 在将
children
、props
、data
传递给子组件之前操作它们。
下面是一个 smart-list
组件的例子,它能根据传入 prop 的值来代为渲染更具体的组件:
var EmptyList = { /* ... */ }
var TableList = { /* ... */ }
var OrderedList = { /* ... */ }
var UnorderedList = { /* ... */ }
Vue.component('smart-list', {
functional: true,
props: {
items: {
type: Array,
required: true
},
isOrdered: Boolean
},
render: function (createElement, context) {
function appropriateListComponent () {
var items = context.props.items
if (items.length === 0) return EmptyList
if (typeof items[0] === 'object') return TableList
if (context.props.isOrdered) return OrderedList
return UnorderedList
}
return createElement(
appropriateListComponent(),
context.data,
context.children
)
}
})
向子元素/子组件中传递attribute和事件
在普通组件中,没有被定义为 prop 的 attribute 会自动添加到组件的根元素上,将已有的同名 attribute 进行替换或与其进行智能合并。然而函数式组件要求你显式定义该行为:
Vue.component('my-functional-button', {
functional: true,
render: function (createElement, context) {
// 完全透传任何 attribute、事件监听器、子节点等。
return createElement('button', context.data, context.children)
}
})
通过向 createElement
传入 context.data
作为第二个参数,我们就把 my-functional-button
上面所有的 attribute 和事件监听器都传递下去了。
如果你使用基于模板的函数式组件,则还需要手动添加 attribute 和监听器。因为我们可以访问到其独立的上下文内容,所以我们可以使用 data.attrs
传递任何 HTML attribute,也可以使用 listeners
(即 data.on
的别名) 传递任何事件监听器。
<template functional>
<button
class="btn btn-primary"
v-bind="data.attrs"
v-on="listeners"
>
<slot/>
</button>
</template>
slots()
和 children
对比
为什么同时需要 slots()
和 children
呢,slots().default
不是和 children
类似吗?在一些场景中的确如此,但如果是如下的带有子节点的函数式组件,则不以为然:
<my-functional-component>
<p v-slot:foo>
first
</p>
<p>second</p>
</my-functional-component>
对于这个组件,children
会给你两个段落标签,而 slots().default
只会传递第二个匿名段落标签——slots().foo
会传递第一个具名段落标签。同时拥有 children
和 slots()
意味着你可以选择让组件感知某个插槽机制,亦或是简单地通过传递 children
移交给其它组件去处理。
4.15 单文件组件
文件扩展名为 .vue
的 single-file components (单文件组件) 可以获得:
此外,它还可以使用 webpack 或 Browserify 等构建工具,以及可以使用预处理器来构建简洁和功能更丰富的组件,比如 Pug,Babel (with ES2015 modules)、 Stylus。
即便你不喜欢单文件组件,你也可以把 JavaScript、CSS 分离成独立的文件然后做到热重载和预编译。
<!-- my-component.vue -->
<template>
<div>This will be pre-compiled</div>
</template>
<script src="./my-component.js"></script>
<style src="./my-component.css"></style>
4.16 注意事项
有些 HTML 元素对于哪些元素可以出现在其内部是有严格限制的,如 <ul>
、<ol>
、<table>
和 <select>
;而有些元素,如 <li>
、<tr>
和 <option>
,只能出现在其它某些特定的元素内部。这会导致我们使用这些有约束条件的元素时遇到一些问题,例如:
<table>
<blog-post-row></blog-post-row>
</table>
这个自定义组件 <blog-post-row>
会被作为无效的内容提升到外部,并导致最终渲染结果出错。
幸好前述特殊的 is
attribute 给了我们一个变通的办法:
<table>
<tr is="blog-post-row"></tr>
</table>
不过,如果我们从以下来源使用模板的话,这条限制是不存在的:
- 字符串 (例如:
template: '...'
) - 单文件组件 (
.vue
) <script type="text/x-template">