Vue2复习_深入了解组件

组件注册

组件名

推荐遵循 W3C 规范中的自定义组件名 (字母全小写且必须包含一个连字符)。这会帮助你避免和当前以及未来的 HTML 元素相冲突。

组件名大小写

定义组件名的两种方式

1
2
3
4
// kebab-case(短横线分隔命名) 
Vue.component('my-component-name', { /* ... */ })
// PascalCase(首字母大写命名)
Vue.component('MyComponentName', { /* ... */ })

当使用PascalCase定义组件后,引用组件时,两种命名风格均可使用。

全局注册

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

当直接在Vue上注册组件时,该组件是全局注册的,在任何新创建的 Vue 根实例 (new Vue) 的模板中均可以使用。

局部注册

当使用webpack这样的构建系统时,全局注册的方法会导致当不再使用该组件,仍包含在构建结果中。

可以通过定义一个普通的js对象定义组件,之后在需要使用时引用该对象。

1
2
3
4
5
6
7
8
9
var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
new Vue({
el: '#app',
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})

模块系统

在模块系统中局部注册

1
2
3
4
5
6
7
8
9
10
11
// 在ComponentB中
import ComponentA from './ComponentA'
import ComponentC from './ComponentC'

export default {
components: {
ComponentA,
ComponentC
},
// ...
}

基础组件的自动化全局注册

思路:通过目录+文件名识别出基础组件,将其统一注册为全局组件,详见:基础组件的自动化全局注册

Prop

Prop的大小写(camelCase vs kebab-case)

HTML 中的 attribute 名是大小写不敏感的,因此向组件传递值时,使用kebab-case形式。

1
2
3
4
5
Vue.component('blog-post', {
// 在 JavaScript 中是 camelCase 的
props: ['postTitle'],
template: '<h3>{{ postTitle }}</h3>'
})
1
2
<!-- 在 HTML 中是 kebab-case 的 -->
<blog-post post-title="hello!"></blog-post>

Prop类型

1
2
3
4
5
6
7
8
9
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise // or any other constructor
}

传递静态或动态Prop

使用v-bind向Prop传值,意味着动态传入,具体内容看后面的js表达式;不使用v-bind,则直接传入的是字符串。

传入数字

1
2
3
4
5
6
<!-- 即便 `42` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:likes="42"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:likes="post.likes"></blog-post>

传入布尔值

1
2
3
4
5
6
7
8
9
<!-- 包含该 prop 没有值的情况在内,都意味着 `true`。-->
<blog-post is-published></blog-post>

<!-- 即便 `false` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:is-published="false"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:is-published="post.isPublished"></blog-post>

传入数组

1
2
3
4
5
6
<!-- 即便数组是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:comment-ids="[234, 266, 273]"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:comment-ids="post.commentIds"></blog-post>

传入对象

1
2
3
4
5
6
7
8
9
10
11
<!-- 即便对象是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post
v-bind:author="{
name: 'Veronica',
company: 'Veridian Dynamics'
}"
></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:author="post.author"></blog-post>

传入对象的所有属性

将一个对象的所有属性传入prop,可以不用将所有参数单独传递。

1
2
3
4
post: {
id: 1,
title: 'My Journey with Vue'
}
1
2
3
4
5
6
<blog-post v-bind="post"></blog-post>
<!-- 等价于 -->
<blog-post
v-bind:id="post.id"
v-bind:title="post.title"
></blog-post>

单向数据流

prop形成了从父组件到子组件的单向数据传输,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。不应该在子组件中直接变更prop的值,当需要变更prop时,可以采用以下两种操作:

1.将传输的值作为本地数据使用:定义一个本地的data

1
2
3
4
5
6
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}

2.需要对prop传输的值进行一定转换:使用计算属性

1
2
3
4
5
6
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}

注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变变更这个对象或数组本身将会影响到父组件的状态。

Prop验证

除了定义prop的类型,还可以定制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
32
33
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'].includes(value)
}
}
}
})

注意 prop 会在一个组件实例创建之前进行验证,所以实例的 property (如 datacomputed 等) 在 defaultvalidator 函数中是不可用的。(不能动态设置验证规则)

类型检查

type 可以是下列原生构造函数中的一个:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol

额外的,type 还可以是一个自定义的构造函数,并且通过 instanceof 来进行检查确认。例如,给定下列现成的构造函数:

1
2
3
4
function Person (firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}
1
2
3
4
5
6
// 验证 author prop 的值是否是通过 new Person 创建的。
Vue.component('blog-post', {
props: {
author: Person
}
})

非Prop的Attribute

替换/合并已有Attribute

由prop接收的Attribute可以直接使用,未接收的Attribute将绑定到根元素(显示在HTML内容中)上且替换掉已有内容(style、class除外,会合并内容)

禁用Attribute继承

希望组件的根元素继承 attribute,可以在组件的选项中设置 inheritAttrs: false,此时这些Attribute将绑定到实例的$attrs属性上,通过调动该属性进一步获取相关内容。

自定义事件

事件名

不同于组件和 prop,事件名不存在任何自动化的大小写转换;

并且 v-on 事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的);

因此,推荐始终使用 kebab-case 的事件名

自定义组件的v-model

组件上v-model语法糖默认利用了名为 value 的 prop 和名为 input 的事件。但单选框、复选框等组件的value有可能用于其他目的,此时在组件中配置model选项可以避免这样的冲突。

理解:对于单选框、复选框等组件,数据双向绑定的用处通常为改变选中状态。该属性在元素中由checked控制,但v-model默认使用value属性绑定值,因此想要用v-model控制是否勾选,就要将原本绑定到value属性的值绑定到checked上,且状态改变时也要将checked的值回传。

因此使用model属性声明传入的prop用于checked,触发的事件为change。同时规定仍需要在组件的props中声明传入的值给到了checked。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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-on.native 修饰符:

1
<base-input v-on:focus.native="onFocus"></base-input>

预期为子组件为input,当其获取焦点时,触发onFocus方法。但当子组件被重构为非input类型元素时,该方法将无法被调用。

1
2
3
4
5
6
7
8
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
</label>

为了解决这个问题,Vue 提供了一个 $listeners property,它是一个对象,里面包含了作用在这个组件上的所有监听器。如:

1
2
3
4
{
focus: function (event) { /* ... */ }
input: function (value) { /* ... */ },
}

将其绑定到子组件上,即可不使用.native方法也能监听各种事件。

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
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>
`
})

此时,父组件想要监听子组件的Focus方法,直接监听即可,因为子组件会通过$listeners将父组件想要监听的focus事件绑定到自身。父组件等待触发即可。

1
<base-input v-on:focus="onFocus"></base-input>

.sync 修饰符

在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以变更父组件,且在父组件和子组件两侧都没有明显的变更来源。

此时推荐以 update:myPropName 的模式触发事件取而代之。

举个例子,在一个包含 title prop 的假设的子组件中,我们可以用以下方法表达对其赋新值的意图:

1
this.$emit('update:title', newTitle)

之后父组件监听update:title事件并根据获取的值更新本地数据:

1
2
3
4
<text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
></text-document>

为了方便起见,为这种模式在父组件中提供一个缩写,即 .sync 修饰符:

1
<text-document v-bind:title.sync="doc.title"></text-document>

插槽

插槽内容

引用子组件时,组件名中间的内容会替换子组件中的<slot></slot>。若子组件中未定义<slot></slot>,则传递的内容将被舍弃。

1
2
3
4
5
6
7
8
9
10
<!-- 父组件中 -->
<navigation-link url="/profile">
Your Profile
</navigation-link>
<!-- 子组件中 -->
<a v-bind:href="url" class="nav-link">
<slot></slot>
</a>
<!-- 最终渲染为 -->
Your Profile

编译作用域

动态传递内容时,不同组件不能直接互相读取对方的内容。

父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

后备内容

子组件中,<slot></slot>中间可以填入默认内容,当父组件未传递内容时,将显示默认内容;传递内容时,该默认内容将被替换掉。

具名插槽

当需要多个插槽时,通过slot元素的name属性定义额外的插槽

1
2
3
4
5
6
7
8
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
</div>

一个不带 name<slot> 出口会带有隐含的名字“default”。

使用时在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称

1
2
3
4
5
6
7
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</base-layout>

此时,含有v-slot:header属性的template中的内容将替换组件中的<slot name="header"></slot>

未设置该属性的,将替换掉组件中的<slot></slot>

注意 v-slot 只能添加在 <template>

作用域插槽

为了让插槽能够访问到子组件中的数据,可以将子组件数据绑定到<slot></slot>上,进而在父组件中接收该值并使用。

1
2
3
4
5
6
7
<!-- 子组件 -->
<span>
<slot v-bind:user="user">
{{ user.lastName }}
</slot>
</span>
<!--data中有user对象的数据,且与v-bind中的两个user对应-->
1
2
3
4
5
6
7
<!-- 父组件 -->
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
</current-user>
<!--slotProps为父组件中的自定义名,接收子组件v-bind:user绑定的user属性的值-->

独占默认插槽的缩写

当被只有默认插槽时,父组件中可以省略<template>,直接将 v-slot 用在组件上:

1
2
3
<current-user v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</current-user>

进一步简写,可以省去default

1
2
3
<current-user v-slot="slotProps">
{{ slotProps.user.firstName }}
</current-user>

但当存在多个插槽时,仍需为所有的插槽采用完整的基于 <template> 的语法:

1
2
3
4
5
6
7
8
9
10
<current-user>
<!-- 默认插槽提供的数据 -->
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
<!-- 具名插槽(name="other")提供的数据 -->
<template v-slot:other="otherSlotProps">
{{ otherSlotProps.user.lastName }}
</template>
</current-user>
1
2
3
4
5
6
7
8
9
10
<template>
<span>
<slot v-bind:user="user">
{{ user.firstName }}
</slot>
<slot v-bind:user="user" name="other">
{{ user.lastName }}
</slot>
</span>
</template>

解构插槽Prop

传回父组件的插槽内容可以用ES6解构:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 独占默认插槽简写 -->
<current-user v-slot="slotProps">
{{ slotProps.user.firstName }}
</current-user>
<!-- 独占默认插槽简写+解构 -->
<current-user v-slot="{ user }">
{{ user.firstName }}
</current-user>
<!-- 独占默认插槽简写+解构+重命名 -->
<current-user v-slot="{ user: person }">
{{ person.firstName }}
</current-user>

甚至可以定义后备内容,在子组件传回undefined时的情形:

1
2
3
4
<!-- 子组件插槽中传回的内容中无法解析user属性时,使用默认值 -->
<current-user v-slot="{ user = { firstName: 'Guest' } }">
{{ user.firstName }}
</current-user>

动态插槽名

1
2
3
4
5
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
</base-layout>

具名插槽的缩写

使用 #代替v-slot,此时后面必须跟插槽名(包括默认插槽)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<base-layout>
<template #header>
<h1>Here might be a page title</h1>
</template>

<template #default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>

<template #footer>
<p>Here's some contact info</p>
</template>
</base-layout>

动态组件&异步组件

在动态组件上使用 keep-alive

在多标签页面使用is切换不同组件时,组件会经历创造和销毁的过程。

1
<component v-bind:is="currentTabComponent"></component>

当在这些组件之间切换的时候,有时会想保持这些组件的状态,以避免反复重新渲染导致的性能问题。

为了解决这个问题,可以用一个 <keep-alive> 元素将其动态组件包裹起来。

1
2
3
4
<!-- 失活的组件将会被缓存!-->
<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>

注意这个 <keep-alive> 要求被切换到的组件都有自己的名字,不论是通过组件的 name 选项还是局部/全局注册。

异步组件

在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。例如:

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)
})

一个推荐的做法是将异步组件和 webpack 的 code-splitting 功能一起配合使用:

1
2
3
4
5
6
Vue.component('async-webpack-example', function (resolve) {
// 这个特殊的 `require` 语法将会告诉 webpack
// 自动将你的构建代码切割成多个包,这些包
// 会通过 Ajax 请求加载
require(['./my-async-component'], resolve)
})

也可以在工厂函数中返回一个 Promise,所以把 webpack 2 和 ES2015 语法加在一起,可以这样使用动态导入:

1
2
3
4
5
Vue.component(
'async-webpack-example',
// 这个动态导入会返回一个 `Promise` 对象。
() => import('./my-async-component')
)

当使用局部注册的时候,你也可以直接提供一个返回 Promise 的函数:

1
2
3
4
5
6
new Vue({
// ...
components: {
'my-component': () => import('./my-async-component')
}
})

处理加载状态

异步组件工厂函数也可以返回一个如下格式的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
const AsyncComponent = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import('./MyComponent.vue'),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
})

边界情况

访问元素&组件

访问根实例

在每个 new Vue 实例的子组件中,其根实例可以通过 $root property 进行访问。

1
2
3
4
5
6
7
8
9
10
11
// 获取根组件的数据
this.$root.foo

// 写入根组件的数据
this.$root.foo = 2

// 访问根组件的计算属性
this.$root.bar

// 调用根组件的方法
this.$root.baz()

在绝大多数情况下,强烈推荐使用 Vuex来管理应用的状态。

访问父组件实例

$root 类似,$parent property 可以用来从一个子组件访问父组件的实例。它提供了一种机会,可以在后期随时触达父级组件,以替代将数据以 prop 的方式传入子组件的方式。

访问子组件或子元素

为子组件或元素定义ref属性,之后通过this.$refs.NAME获取到该组件或元素。

$refs 只会在组件渲染完成之后生效,并且它们不是响应式的。

依赖注入

prop$parent提供了父子组件间的数据传递,但对于更深层级的组件来说,数据传递仍然比较困难。通过provideinject能够跨越层级向下传递数据。

1
2
3
4
5
6
// 与data属性类似的用法
provide: function () {
return {
getMap: this.getMap
}
}

在任何后代中,都可以使用inject进行接收

1
2
// 与prop属性类似的用法
inject: ['getMap']

实际上,可以把依赖注入看作一部分“大范围有效的 prop”,除了:

  • 祖先组件不需要知道哪些后代组件使用它提供的 property
  • 后代组件不需要知道被注入的 property 来自哪里

然而,依赖注入还是有负面影响的。它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的 property 是非响应式的。

程序化的事件监听器

除了$emit可以被v-on监听外, Vue 实例同时在其事件接口中提供了其它的方法。我们可以:

  • 通过 $on(eventName, eventHandler) 侦听一个事件
  • 通过 $once(eventName, eventHandler) 一次性侦听一个事件
  • 通过 $off(eventName, eventHandler) 停止侦听一个事件

当你需要在一个组件实例上手动侦听事件时,它们是派得上用场的,比如下面的实例集成了一个第三方库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 一次性将这个日期选择器附加到一个输入框上
// 它会被挂载到 DOM 上。
mounted: function () {
// Pikaday 是一个第三方日期选择器的库
this.picker = new Pikaday({
field: this.$refs.input,
format: 'YYYY-MM-DD'
})
},
// 在组件被销毁之前,
// 也销毁这个日期选择器。
beforeDestroy: function () {
this.picker.destroy()
}

这里有两个潜在的问题:

  • 它需要在这个组件实例中保存这个 picker,如果可以的话最好只有生命周期钩子可以访问到它。这并不算严重的问题,但是它可以被视为杂物。
  • 我们的建立代码独立于我们的清理代码,这使得我们比较难于程序化地清理我们建立的所有东西。

通过一个程序化的侦听器解决这两个问题:

1
2
3
4
5
6
7
8
9
10
mounted: function () {
var picker = new Pikaday({
field: this.$refs.input,
format: 'YYYY-MM-DD'
})

this.$once('hook:beforeDestroy', function () {
picker.destroy()
})
}

此时,每当元素/实例销毁,会自动调用第三方库的清理程序。甚至可以让多个元素同时使用不同的 Pikaday,每个新的实例都程序化地在后期清理它自己:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mounted: function () {
this.attachDatepicker('startDateInput')
this.attachDatepicker('endDateInput')
},
methods: {
attachDatepicker: function (refName) {
var picker = new Pikaday({
field: this.$refs[refName],
format: 'YYYY-MM-DD'
})

this.$once('hook:beforeDestroy', function () {
picker.destroy()
})
}
}