Front-end Dev Engineer

0%

Vue官方文档笔记2——深入了解组件

Vue官方文档笔记2——深入了解组件

Vue官方文档笔记2——深入了解组件

1.组件注册

1.1 组件名

在全局注册的时候,组件名就是 Vue.component 的第一个参数,组件命名采用驼峰式命名法或者短横线分隔命名法

1
Vue.component('my-component-name', { /* ... */ })

1.2 全局注册

通过 Vue.component 来创建组件,在注册之后可以用在任何新创建的 Vue 根实例 (new Vue) 的模板中;

1
2
Vue.component('component-a', { /* ... */ })
new Vue({ el: '#app' })
1
2
3
<div id="app">
<component-a></component-a>
</div>

1.3 局部注册

通过一个普通的 JavaScript 对象来定义组件,然后在 components 选项中定义你想要使用的组件; components 对象中的每个属性来说,其属性名就是自定义元素的名字,其属性值就是这个组件的选项对象,例如:

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.4 模块系统

在模块系统中局部注册:

首先需要在局部注册之前导入每个你想使用的组件,然后再加入到组件属性中,例如在ComponentB中引入ComponentA和ComponentC 组件,进行逻辑处理后在 export 出去;

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

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

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

可以使用 require.context 只全局注册这些非常通用的基础组件;在应用入口文件 (比如 src/main.js) 中全局导入基础组件的示例代码(加载顺序以及正则的相关知识):

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
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.replace(/^\.\/(.*)\.\w+$/, '$1')
)
)
// 全局注册组件
Vue.component(
componentName,
// 如果这个组件选项是通过 `export default` 导出的,
// 那么就会优先使用 `.default`,
// 否则回退到使用模块的根。
componentConfig.default || componentConfig
)
})

注意:全局注册的行为必须在根 Vue 实例 (通过 new Vue) 创建之前发生;

2.Prop

2.1 Prop的大小写

HTML 中的特性名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 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>

如果你使用字符串模板,那么这个限制就不存在了;

2.2 Prop 类型

可以以字符串数组形式或是以对象形式(对象中可以指定的值类型);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
props: ['title', 'likes', 'isPublished']
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object
}
props: {
title: {
type:String,
default: '',
},
...
}

2.3 传递静态或动态 Prop

静态:<blog-post title="My journey with Vue"></blog-post>

动态:使用 v-bind 指令动态赋值,prop可以接受任何类型的值;

1
2
3
4
5
<!-- 动态赋予一个变量的值 -->
<blog-post v-bind:title="post.title"></blog-post>

<!-- 动态赋予一个复杂表达式的值 -->
<blog-post v-bind:title="post.title + ' by ' + post.author.name"></blog-post>

不仅可以传字符串,还可以传入数字、布尔值、数组、对象、一个对象的所有属性(使用不带参数的 v-bind (取代 v-bind:prop-name))等等;

2.4 单向数据流

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定(父组件传向子组件),每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值,若在子组件中修改props值,Vue 代码会出现警告;

我们一般通过两种方式接收父组件向子组件传递的 prop 属性的值:

  • 方法1: prop 用来传递一个初始值,子组件将其作为一个本地的 prop 数据来使用,则定义一个本地的 data 属性并将这个 prop 用作其初始值:
1
2
3
4
5
6
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
  • 方法2:prop 以一种原始的值传入且需要进行转换,则使用这个 prop 的值来定义一个计算属性:
1
2
3
4
5
6
7
props: ['size'],
computed: {
normalizedSize: function () {
// trim() 方法去除字符序列左边和右边的空格;
return this.size.trim().toLowerCase()
}
}

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

2.5 Prop 验证:为组件的 prop 指定验证要求

可以为 props 中的值提供一个带有验证需求的对象,而不是一个字符串数组,例如:

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` 匹配任何类型)
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 Symboltype 还可以是一个自定义的构造函数,并且通过 instanceof 来进行检查确认;

2.6 非 Prop 的特性

一个非 prop 特性是指传向一个组件,但是该组件并没有相应 prop 定义的特性;

  • 替换/合并已有的特性;
  • 禁用特性继承,如果你不希望组件的根元素继承特性,你可以设置在组件的选项中设置 :
1
2
3
4
Vue.component('my-component', {
inheritAttrs: false,
// ...
})

3.自定义事件

3.1 事件名

事件名不会存在自动化的大小写转换,而是触发的事件名需要完全匹配监听这个事件所用的名称(如:子组件通过发布订阅模式向父组件传递数据$emit()方式);

推荐事件的命名方式为 kebab-case中划线命名 方式;

3.2 自定义组件的 v-model

组件的 v-model 默认会利用名为 valueprop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件的 value 特性用于不同的目的;model 选项可以用来避免这样的冲突;例:

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)"
>
`
})
1
<base-checkbox v-model="lovingVue"></base-checkbox>

这里的 lovingVue 的值将会传入这个名为 checked 的 prop。同时当 <base-checkbox>触发一个 change 事件并附带一个新的值的时候,这个 lovingVue 的属性将会被更新;

3.3 将原生事件绑定到组件(有点儿难)

在一个组件的根元素上直接监听一个原生事件,使用 v-on.native 修饰符,例:

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

当监听一个类似 <input> 的非常特定的元素时,该元素不是根元素,此时父级的 .native 监听器将静默失败,也不会报错,但是 onFocus 处理函数不会如你预期地被调用;不过Vue 提供了一个 $listeners属性解决这个问题, $listeners 它是一个对象,里面包含了作用在这个组件上的所有监听器。例如:

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

可以配合 v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素。对于类似 <input> 的你希望它也可以配合 v-model 工作的组件来说,为这些监听器创建一个类似下述 inputListeners 的计算属性通常是非常有用的:

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

3.4 .sync 修饰符

对prop进行双向绑定,推荐以 update:myPropName 的模式触发事件取而代之。举个例子,在一个包含 title prop 的假设的组件中,我们可以用以下方法表达对其赋新值的意图:
this.$emit('update:title', newTitle);然后父组件可以监听那个事件并根据需要更新一个本地的数据属性;

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

这种模式提供一个缩写,即 .sync 修饰符:

1
2
<!-- 上面代码等价于: -->
<text-document v-bind:title.sync="doc.title"></text-document>

注意:带有 .sync 修饰符的 v-bind 不能和表达式一起使用 (例如 v-bind:title.sync=”doc.title + ‘!’” 是无效的)。

4. 插槽(有点儿难)

4.1 插槽内容

<slot> 元素作为承载分发内容的出口。

4.2 具名插槽

<slot> 元素有一个特殊的特性:name。这个特性可以用来定义额外的插槽;

插槽用法:例如,一个假设的 <base-layout> 组件的模板如下:

1
2
3
4
5
6
7
8
9
10
11
<div class="container">
<header>
<!-- 我们希望把页头放这里 -->
</header>
<main>
<!-- 我们希望把主要内容放这里 -->
</main>
<footer>
<!-- 我们希望把页脚放这里 -->
</footer>
</div>
  • 我们可以在一个父组件的 <template> 元素上使用 slot 特性:
1
2
3
4
5
6
7
8
9
10
11
12
<base-layout>
<template slot="header">
<h1>Here might be a page title</h1>
</template>

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

<template slot="footer">
<p>Here's some contact info</p>
</template>
</base-layout>
  • 另一种 slot 特性的用法是直接用在一个普通的元素上:
1
2
3
4
5
6
7
8
<base-layout>
<h1 slot="header">Here might be a page title</h1>

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

<p slot="footer">Here's some contact info</p>
</base-layout>
  • 上述两个示例渲染出来的 HTML 都将会是:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <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>

4.3 插槽的默认内容

<slot> 标签内部指定默认的内容;(如果父组件为这个插槽提供了内容,则默认的内容会被替换掉。)

4.4 编译作用域

父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。

4.5 作用域插槽(不怎么明白)

1
2
3
4
5
6
7
8
<ul>
<li
v-for="todo in todos"
v-bind:key="todo.id"
>
{{ todo.text }}
</li>
</ul>

若希望每个独立的待办项渲染出和 todo.text不太一样的东西。这也是作用域插槽的用武之地。为了让这个特性成为可能,你需要做的全部事情就是将待办项内容包裹在一个 <slot> 元素上,然后将所有和其上下文相关的数据传递给这个插槽:

1
2
3
4
5
6
7
8
9
10
11
12
13
<ul>
<li
v-for="todo in todos"
v-bind:key="todo.id"
>
<!-- 我们为每个 todo 准备了一个插槽,-->
<!-- 将 `todo` 对象作为一个插槽的 prop 传入。-->
<slot v-bind:todo="todo">
<!-- 回退的内容 -->
{{ todo.text }}
</slot>
</li>
</ul>

当使用 <todo-list> 组件的时候,可以选择为待办项定义一个不一样的 <template> 作为替代方案,并且可以通过 slot-scope 特性从子组件获取数据:

1
2
3
4
5
6
7
8
9
<todo-list v-bind:todos="todos">
<!-- 将 `slotProps` 定义为插槽作用域的名字 -->
<template slot-scope="slotProps">
<!-- 为待办项自定义一个模板,-->
<!-- 通过 `slotProps` 定制每个待办项。-->
<span v-if="slotProps.todo.isComplete"></span>
{{ slotProps.todo.text }}
</template>
</todo-list>

在 2.5.0+,slot-scope 不再限制在 <template> 元素上使用,而可以用在插槽内的任何元素或组件上。

解构 slot-scope: 如果一个 JavaScript 表达式在一个函数定义的参数位置有效,那么这个表达式实际上就可以被 slot-scope 接受,可以在支持的环境下 (单文件组件或现代浏览器),在这些表达式中使用 ES2015 解构语法,例如:

1
2
3
4
5
6
<todo-list v-bind:todos="todos">
<template slot-scope="{ todo }">
<span v-if="todo.isComplete"></span>
{{ todo.text }}
</template>
</todo-list>

5. 动态组件和异步组件

5.1 在动态组件上使用 keep-alive

曾经在一个多标签的界面中使用 is 特性来切换不同的组件:
<component v-bind:is="currentTabComponent"></component>
当在这些组件之间切换的时候,你有时会想保持这些组件的状态,以避免反复重渲染导致的性能问题。

解决方法:可以用一个 <keep-alive> 元素将其动态组件包裹起来,将那些标签的组件实例在它们第一次被创建的时候缓存下来;

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

5.2 异步组件(不明白)

Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。

5.2.1 处理加载状态

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

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

注意:若在 Vue Router的路由组件中使用上述语法的话,你必须使用 Vue Router 2.4.0+ 版本。

6. 处理边界情况(有点儿难)

6.1 访问元素&组件

在绝大多数情况下,我们最好不要触达另一个组件实例内部或手动操作 DOM 元素。不过也确实在一些情况下做这些事情是合适的。

  1. 访问根实例:在每个 new Vue 实例的子组件中,其根实例可以通过 $root 属性进行访问;所有的子组件都可以将这个实例作为一个全局 store 来访问或使用:this.$root.foo // 获取根组件的数据
  2. 访问父级组件实例:$parent 属性可以用来从一个子组件访问父组件的实例;它提供了一种机会,可以在后期随时触达父级组件,以替代将数据以 prop 的方式传入子组件的方式。
  3. 访问子组件实例或子元素:可以通过 ref 特性为这个子组件赋予一个 ID 引用;
  4. 依赖注入:provide 选项允许我们指定我们想要提供给后代组件的数据/方法;然后在任何后代组件里,我们都可以使用 inject 选项来接收指定的我们想要添加在这个实例上的属性;
1
2
3
4
5
6
7
provide: function () {
return {
getMap: this.getMap
}
}

inject: ['getMap']
  1. 6.2 程序化的事件侦听器

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

注意:如果你发现自己不得不在单个组件里做很多建立和清理的工作,最好的方式通常还是创建更多的模块化组件,更多程序化侦听器的内容,请查阅实例方法 / 事件相关的 API;

6.3 循环引用

6.3.1 递归组件

组件是可以在它们自己的模板中调用自身的。不过它们只能通过 name 选项来做这件事:

1
name: 'unique-name-of-my-component'

当你使用 Vue.component 全局注册一个组件时,这个全局的 ID 会自动设置为该组件的 name 选项:

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

注意:稍有不慎,递归组件就可能导致无限循环,要确保递归调用是条件性的 (例如使用一个最终会得到 falsev-if)。

1
2
name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'

6.3.2 组件之间的循环引用

若需要构建一个文件目录树,像访达或资源管理器那样的。你可能有一个 <tree-folder> 组件,模板是这样的:

1
2
3
4
<p>
<span>{{ folder.name }}</span>
<tree-folder-contents :children="folder.children"/>
</p>

还有一个 <tree-folder-contents> 组件,模板是这样的:

1
2
3
4
5
6
<ul>
<li v-for="child in children">
<tree-folder v-if="child.children" :folder="child"/>
<span v-else>{{ child.name }}</span>
</li>
</ul>

若使用 Vue.component 全局注册组件的时候,可以解决这两个组件的父子组件关系;或者是经过其中一个组件而完全解析出另一个组件。为了解决这个问题,我们需要给模块系统一个点,在例子中,把 <tree-folder> 组件设为了那个点,那个产生悖论的子组件是 <tree-folder-contents> 组件,所以会等到生命周期钩子 beforeCreate 时去注册它:

1
2
3
beforeCreate: function () {
this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default
}

或者,在本地注册组件的时候,你可以使用 webpack异步 import

1
2
3
components: {
TreeFolderContents: () => import('./tree-folder-contents.vue')
}

6.4 模板定义的替代品

6.4.1 内联模板

inline-template 这个特殊的特性出现在一个子组件上时,这个组件将会使用其里面的内容作为模板,而不是将其作为被分发的内容;

1
2
3
4
5
6
<my-component inline-template>
<div>
<p>These are compiled as the component's own template.</p>
<p>Not parent's transclusion content.</p>
</div>
</my-component>

inline-template 会让你模板的作用域变得更加难以理解。所以作为最佳实践,请在组件内优先选择 template 选项或 .vue 文件里的一个 <template> 元素来定义模板;

6.4.2 X-Templates

另一个定义模板的方式是在一个 <script> 元素中,并为其带上 text/x-template 的类型,然后通过一个 id 将模板引用过去。例如:

1
2
3
4
5
6
7
<script type="text/x-template" id="hello-world-template">
<p>Hello hello hello</p>
</script>

Vue.component('hello-world', {
template: '#hello-world-template'
})

6.5 控制更新

Vue 的响应式系统始终知道何时进行更新;

6.5.1 强制更新

你可能还没有留意到数组或对象的变更检测注意事项,或者你可能依赖了一个未被 Vue 的响应式系统追踪的状态。

然而,如果你已经做到了上述的事项仍然发现在极少数的情况下需要手动强制更新,那么你可以通过 $forceUpdate 来做这件事

6.5.2 通过 v-once 创建低开销的静态组件

渲染普通的 HTML 元素在 Vue 中是非常快速的,但有的时候你可能有一个组件,这个组件包含了大量静态内容。在这种情况下,你可以在根元素上添加 v-once 特性以确保这些内容只计算一次然后缓存起来,就像这样:

1
2
3
4
5
6
7
8
Vue.component('terms-of-service', {
template: `
<div v-once>
<h1>Terms of Service</h1>
... a lot of static content ...
</div>
`
})
-------------    本文结束  感谢您的阅读    -------------

赞赏一下吧~ 还可以关注公众号订阅最新内容