Vue 2 组件
Vue 中的组件是页面中的一部分,通过层层拼装和复用,最终形成了一个完整的页面。
使用组件的一般场景:
- 代码复用
- 功能分类管理(每个组件实现一个功能)
组件必须先注册以便 Vue 应用能够识别,有两种组件的注册类型:
- 全局注册
- 局部注册
然后就可以使用组件,它类似标签一样,通过尖括号包裹组件名称 <component-name>
来使用组件。
Tip
由于 HTML 不区分大小写,如果在组件名定义时 name
使用驼峰式命名法,在 HTML 中使用组件时需要改为采用连字号 -
,因此建议组件名称采用全小写,对于多个单词组成的名字,使用连字符号 -
分隔
Tip
组件的 data 选项必须是函数,从中 return
对象
// 定义全局组件
Vue.component('myComponent', {
data() {
return {
name: 'Ben'
}
}
})
<!-- 使用组件 -->
<body>
<div id="app">
<my-component>
</div>
</body>
全局组件
使用 Vue 原型 prototype 上的方法 Vue.component()
注册一个全局组件,第一个参数 name
是组件名称,第二个参数是一个对象包含组件相关的选项
Vue.component('name', {
options,
template: `...`
})
Warning
==需要在 new Vue({})
之前进行全局组件的注册==,之后它可以在任何地方(根组件或其他子组件的模板中)使用。
Tip
如果有大量基础组件,同时使用 webpack 工具,可以在入口文件进行配置,使用 require.context
实现自动引入并进行全局注册这些组件。
局部组件
通过一个普通的 JavaScript 对象来定义组件(提供组件的 options),然后在 Vue 实例的 components
选项中进行注册,之后它只能在该父级组件内使用
const ComponentA = { /* ... */ }
const ComponentB = { /* ... */ }
const ComponentC = { /* ... */ }
new Vue({
el: '#app',
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})
全局组件虽然可以在整个项目的所有其他组件(包括自己)中都可用,但是这可能造成构建项目时体积增大,用户下载 JavaScript 的无谓增加,因此不能滥用全局组件,视情况而定,如果组件只在特定父级中使用,应该将其注册为局部组件。
动态组件
基于标签 <component>
的 is
属性值(组件名),动态在不同组件之间进行切换
Tip
有时候为了在切换时,保存动态组件的状态,例如组件中的输入框的值,可以用标签 <keep-alive></keep-alive>
包裹动态组件。
Tip
属性 is
还可以用于解决 HTML 元素嵌套的规则限制,将它应用到原生的 HTML 标签上,它的值就是组件名,这样原生标签实际渲染出来的内容就是组件。因为对于 <ul>
、<ol>
、<table>
和 <select>
这些元素,其内部允许放置的直接子元素是有严格限制的,如果嵌入其他元素会被视为无效的内容,而提升到外部造成最终渲染问题。
<!-- 这样做是有必要的,因为组件 `<my-row>` 放在一个 `<table>` 内可能无效而被放置到外面 -->
<table>
<tr is="my-row"></tr>
</table>
但以上限制只是在 HTML 中直接使用 Vue 模板时才会遇到,如果是在以下情况的模板中就没有这种限制:
- 字符串,例如
template: '...'
- 单文件组件
.vue
<script type="text/x-template">
组件间数据传递
- 父组件将需要传递的数据绑定到子组件的属性上,子组件内部通过 props 接收。
- 子组件可以通过抛出事件
$emit(eventName, value)
同时传递数据,父组件监听事件并接收数据
props
props 用于接收父组件传递的数据,需要在子组件的选项 props
中预先设置会有哪些 prop,可以以字符串数组形式列出,也可以以对象形式列出(可指定 prop 接收的数据类型、默认值、是否必须、验证条件等)。
需要注意以下几点:
- ==如果在父组件中传入的数据是非字符串的,需要通过绑定
v-bind:propName
的方式来传递(即使传递纯数字这一类的静态值)==;否则直接传递数据都会转换为字符串。 - 如果 prop 是 Boolean 类型,但在使用组件中没有设置该 prop 时,则实例化后该组件的这个 prop 预设为
false
;而使用组件时,设置了该 prop 但没有提供 true/false 值(no value 的情况),就会设置为true
。由于布尔类型不是字符串,正确传递方式是使用v-bind
来传递非字符串的数据
Play Video - prop 应该遵循==单向数据流==,即不应该在子组件中修改 prop 值(因为这些数据是在父层的),如果希望从子组件触发 prop 值的修改,可以通过子组件
$emit('eventName', value)
抛出事件,同时传递需要更新的数据,然后在父级组件中监听事件,在回调函数中做修改。:IconCustom{name="mdi:youtube" iconClass="text-red-500"} 或者以 prop 值作为初始值或响应依赖:- 以 prop 传递来的数据作为一个初始值,在子组件接下来,拷贝到自己 data 的一个变量中
- 基于 prop 定义一个计算属性,实现对原始的值的转换
Play Video
如果希望将一个对象的所有属性「拆分」为多个 prop 分别传入,可以使用不带参数的 v-bind
(即 v-bind="obj"
,而非 v-bind:propName="obj"
),:IconCustom{name="mdi:youtube" iconClass="text-red-500"} 所以可以将大量的 prop 包装进一个对象,一次绑定到组件上
可以对 prop 进行限制,确保传递进来的值符合要求。有多个属性可以进行设置
// ...
props: {
propA: {
type: String,
required: true,
default: 'abc',
validator: func() // 自定义验证函数,返回 true/false
}
}
Warning
prop 会在一个组件实例创建之前进行验证,所以该组件的 data
或 computed
数据在 validator
验证函数中无法进行访问
emit
当组件需要修改父层控管的数据(这些数据通过 props 传递进来)时,基于单向数据流原则不能在组件内进行修改,而是需要通过 $emit('eventName', value)
向外抛出自定义的事件,通知父层进行数据修改,其中 value
一般是向外传递的需要变动的数据。
Tip
自定义事件名推荐使用 kebab-case 方式(:IconCustom{name="mdi:youtube" iconClass="text-red-500"} 小写单词之间用连字符 -
连接,因为与组件和 prop 名称不同,Vue 不会对事件名进行任何大小写转换),以便于 HTML 正确识别
==Vue 对于具有表单元素 <input>
的输入型组件,支持使用 v-model
实现类似「双向绑定」的效果==。但在该组件内部需要进行 input
事件监听,并手动 $emit('change', $event.target.value)
抛出该事件和用户输入的数据,让变量数据的修改仍在父层完成。
- 在外部(引用该组件时,在组件的标签上)通过指令
v-model
双向绑定需要同步的变量,它的数据作为 prop 传递到组件内(因此记得在组件内部需要在props
选项里声明value
作为 prop),作为子组件内部表单元素的value
属性的值 - 在组件内使用指令
v-bind
将表单元素表单元素的value
属性绑定到以上声明的 prop 变量上,即v-bind:value="value"
(这样就可以将外部传进来的值作为表单的值,实现与外部数据的「同步」) - 在组件内使用指令
v-on
监听表单元素change
输入事件,并通过抛出事件$emit('input', $event.target.value)
向外传递相应的数据。这样在父组件中通过指令v-model
绑定的变量就会基于抛出的数据进行改变 (即数据的修改的操作还是在父层完成)
// component
Vue.component('base-checkbox', {
props: ['value'],
template: `
<input
type="text"
v-bind:value="value"
v-on:change="$emit('input', $event.target.value)"
>
`
})
<base-input v-model="inputText"></base-input>
通过遵守这种设置,在使用输入型的组件时仅需简单地以 v-model
的方式,方便地实现数据从父组件传递给子组件,并将用户输入改动 $emit
给父组件进行修改,实现类似「双向绑定」的效果。
Tip
如果输入型组件需要绑定的 prop
名称不是 value
,且抛出的事件不是 input
,:IconCustom{name="mdi:youtube" iconClass="text-red-500"} 可以在组件中选项 model
进行配置,例如表单类型为 checkbox
时,需要绑定的是属性是 checked
,抛出的事件是 change
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)"
>
`
})
<base-checkbox v-model="lovingVue"></base-checkbox>
这里的 lovingVue
的值将会传入子组件,作为 prop checked
的值;而当 <base-checkbox>
抛出 change
事件并附带一个新的值时,这个所绑定的变量 lovingVue
将会被更新。
sync
Vue 还为一般的组件提供 .sync
修饰符,和一个以特定前缀 update
命名的事件,让 prop 变量实现类似于「双向绑定」的效果,更方便地从子组件中「修改」父层传入的数据。
在使用组件设置 prop 时,==如果在绑定变量时 v-bind:propName.sync="variable"
添加 .sync
修饰符,就可以省略在父层设置监听事件和回调函数这一步==。
Vue 会自动监听子组件抛出的以特殊形式命名的事件 this.$emit('update:propName', newValue)
(事件以特定模式 update:propName
命名),然后在父层对 prop 绑定的变量进行更新。
Tip
而且支持以 v-bind.sync="obj"
的形式同时设置多个具有「双向绑定」效果的 prop,其中对象 obj 的每一个 property 都作为一个独立的 prop 传进去,然后可以在组件内部使用 this.$emit('update:propName', newValue)
抛出事件来更新数据。
非 prop 属性
非 prop 的 attribute 是指未在组件的 props
选项中显式声明的,但在引用组件时传递给子组件的 attribute,:IconCustom{name="mdi:youtube" iconClass="text-red-500"} 这些 attribute 会默认被添加到这个组件的根元素上(即在组件的模板中作为容器的第一层的 <tag>
上。
如果组件的根节点 <tag>
上已有预设了相应的 attribute,:IconCustom{name="mdi:youtube" iconClass="text-red-500"} 该属性就会被新传入的值覆盖;如果这个 attribute 是 class
或 style
,会将它们与组件根元素上的 class
或 style
合并。
Tip
如果希望 attribute 添加到组件的特定元素上,:IconCustom{name="mdi:youtube" iconClass="text-red-500"} 可以设定组件的选项 inheritAttrs: false
,然后将特殊的变量 $attrs
(这是一个包含所有非 prop 的 attribute 的对象)绑定 v-bind="$attrs"
到指定的节点上,但 class
和 style
不能指定到组件的非根元素上。也可指定某个 attribute 绑定 :attributeName="$attrs.propertyName"
监听事件
有时候希望在使用组件时才添加事件监听,这样就会将事件监听添加到组件的根元素上,:IconCustom{name="mdi:youtube" iconClass="text-red-500"} 但此时监听的事件都被认为是自定义事件(即监听从子组件手动 $emit
出来的事件),如果希望监听 JS 预设的原生事件,如 click
、focus
等,需要在其后添加 .native
修饰符。
如果希望在引用组件时添加事件监听,但又不是添加到组件的根元素上,而是添加到组件内的特定元素上,可以在组件中配置选项 inheritAttrs: false
,:IconCustom{name="mdi:youtube" iconClass="text-red-500"} 并在模板非根元素的标签上使用 v-on="$listener"
,这样在使用组件时才设置的事件监听器,都指向该特定节点上。
Vue 提供了一个 $listeners
property,它是一个对象,里面包含了作用在这个组件上的所有监听器。例如:
{
focus: function (event) { /* ... */ }
input: function (value) { /* ... */ },
}
官方的例子实现了将外部添加的事件监听器(通过 $listeners
获取),和内部为了配合 v-model
设置的 input
事件监听器合并,再一起绑定到组件的 <input>
元素上。
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>
`
})
插槽
插槽 slot 是 Vue 实现的一套内容分发的 API,一般用于组件作为布局 layout 使用。
在子组件的模板中使用标签 <slot>
作为「占位符」预留位置,在使用组件时,内嵌在组件 <component-name>content</component-name>
中的内容 content
(插入的内容可以是 HTML,也包含模板代码或其他组件) 会替代 <slot>
标签,渲染出来。
Tip
可以在组件模板的插槽中设置默认内容,但是只要在使用组件时,有提供插入内容就会替换默认内容。如果组件定义模板时没有包含一个 <slot>
元素,则使用该组件时,即使组件起始标签和结束标签之间有内容,都会被抛弃。
具名插槽
可以在组件定义的模板中设置多个插槽,并为它们设置属性 name
,它们称为具名插槽 <slot name="slotName"></slot>
。
在使用组件时,使用 <template>
元素作为容器(该标签本身不会被渲染到页面上),并以==带参数的指令 v-slot:slotName
的形式 <template v-slot:slotName>content</template>
将提供的内容分发到相应名称的插槽==。
Tip
和其他指令一样,v-slot:slotName
指令有缩写形式 #slotName
当模板中存在多个具名插槽 <slot>
时,可以有一个不具名插槽,它作为默认插槽(实际上带有隐含的名字 v-slot:default
)。在使用组件时,任何没有被包裹在带有指令 v-slot:slotName
的 <template>
中的内容都会被视为默认插槽的内容。
作用域插槽
由于使用组件时,是在父层指定插入的内容,所以这些内容现在父级作用域进行编译,它们只能访问父层的数据。
如果希望可以访问组件中才有的数据,需要使用作用域插槽 <slot v-bind:variable="value">
,即在定义组件时,在插槽中预先将允许访问的数据绑定到变量上,这些绑定到 <slot>
的属性称为插槽 props
然后在使用组件时,可以==为指令 v-slot
设置值 <template v-slot:slotName="objName">
,该值是一个对象,它包含所有在组件内绑定的插槽 props==,然后就可以通过 objName.variable
的方式来读取子作用域才有的数据。除了使用 objName
「接收」内层抛出的所有 props,还可以使用 ES6 解构的方式直接 <template v-slot:slotName="{ variable }">
获取单个 variable
,便于后面调用。
<!-- 子组件 current-user 的模板 -->
<span>
<slot v-bind:user="user">
{{ user.firstName}} {{ user.lastName }}
</slot>
</span>
<!-- 使用组件 current-user -->
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
</current-user>
<!-- 解构的方式接收特定的 prop -->
<current-user v-slot="{ user }">
{{ user.firstName }}
</current-user>
Tip
如果组件只有默认插槽时,组件的标签可以被当作插槽的模板来使用,即可以省略 <template>
标签,直接在组件标签上设置 v-slot="objName"
来接收内层抛出的 props
<current-user v-slot="slotProps">
{{ slotProps.user.firstName }}
</current-user>
混入
混入 mixin 是指将组件选项中可复用的部分「抽取」出来成为一个对象(可以包含 computed
、method
、watch
等),以供其他组件复用。:IconCustom{name="mdi:youtube" iconClass="text-red-500"} 然后在组件(包括根组件)的选项 mixins
中使用该对象即可复用。
在组件或 Vue 实例中添加选项 mixins: [mixinName]
引入预设的选项,它们将被「混合」进入该组件本身的选项。
// 定义一个混入对象
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}
// 定义一个使用混入对象的组件
var Component = Vue.extend({
mixins: [myMixin]
})
var component = new Component() // => "hello from mixin!"
Tip
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行 「合并」:
- 同名生命周期钩子函数,如
created
、mounted
等,将合并为一个数组都将被调用,而且混入对象的钩子函数优先级更高,将在组件自身钩子函数之前调用。 - 当选项的值为对象,如
methods
、components
,将被合并为同一个对象。如果合并时两个对象键名冲突,取组件对象的键值对 - 如果 希望(针对相同选项)自定义混入时合并策略,可以向
Vue.config.optionMergeStrategies
添加一个函数jsVue.config.optionMergeStrategies.myOption = function (toVal, fromVal) { // 返回合并后的值 }
Play Video
可以通过 Vue 原型 prototype 上的方法 Vue.mixin({options})
设置 global mixin 全局混入,这样在所有组件中都会引入该 mixin(不需要手动以 mixins: [mixinName]
的方式添加)⚠️ 由于全局混入会影响每个单独创建的 Vue 实例(包括第三方组件),可能会导致一些冲突。