目录
本篇文章主要是学习完官方文档中 深入组件部分后做的笔记,深入组件这个部分主要讲的是父组件与子组件之间的数据交换,本文主要分为父组件主动传递数据和子组件主动更新数据两个部分,其余的知识点放在最前面或是最后面。
1. 组件基础
1.1 组件名规范
这里先给出一个在任何情况下都不会出错的例子,如果你不想纠结命名的话,只需要像下面的代码一样,在模板定义中和DOM中均遵循小写字母加连字符(W3C规范-自定义标签)的规范即可。
// 定义文件中
app.component('my-component-name', {
/* ... */
})
<!-- DOM中 -->
<my-component-name></my-component-name>
如果你想多了解一些,那就先不急着跳到下一小节。
在字符串模板或单文件组件中定义组件时,定义组件名的方式有两种:
-
kebab-case: 小写字母加连接符 (
my-component-name
) -
PascalCase: 单词的首字母均大写 (
MyComponentName
)
当使用 kebab-case形式定义组件名时,引用该自定义元素也必须使用该形式;当像下面这样使用PascalCase 定义组件名时,在字符串模板中我们仍可继续使用 PascalCase形式,但在 DOM 中我们只能使用 kebab-case 形式。
// 模板定义
app.component('MyComponentName', {
/* ... */
})
// 字符串模板中可以使用PascalCase
app.component('MySubComponentName', {
template: `
<MyComponentName></MyComponentName>
`,
/* ... */
})
<!-- 正确 -->
<my-component-name></my-component-name>
<!-- 报错 -->
<MyComponentName></MyComponentName>
1.2 父组件与子组件
app.component('child-component', {
data: {
/* ... */
},
props: ["title"],
template: `
<div>
我是子组件
</div>
`
})
app.component('my-component', {
data: {
/* ... */
},
props: ["title"],
template: `
<div>
{{title}}
<!-- 这里的child-component相对于chlid-component内的template定义中的元素是父组件 -->
<child-component></child-component>
</div>
`
})
<!-- DOM -->
<!-- 这里的my-component组件对于my-component内的template定义中的元素是父组件 -->
<my-component title="Data from your father!"></my-compoennt>
1.3 字符串模板与单文件组件
字符串模板(my-component.js):
// 创建一个Vue 应用
const app = Vue.createApp({})
// 定义一个名为 button-counter 的新全局组件
app.component('button-counter', {
data() {
return {
count: 0
}
},
template: `
<button @click="count++">
You clicked me {{ count }} times.
</button>`
})
单文件组件(my-component.vue):
<script>
export default {
data() {
return {
greeting: 'Hello World!'
}
}
}
</script>
<template>
<p class="greeting">{{ greeting }}</p>
</template>
<style>
.greeting {
color: red;
font-weight: bold;
}
</style>
1.4 组件注册
组件注册分为局部注册和全局注册。
全局注册
像下面这样就是全局注册,所有组件都被挂载到app上了,这三个组件可以相互使用。
const app = Vue.createApp({...}).component('my-component-name', {
// ... 选项 ...
})
app.component('component-a', {})
app.component('component-b', {})
局部注册
如果你不一定要使用全部组件,可以使用局部注册。
const ComponentA = {
/* ... */
}
const ComponentB = {
/* ... */
}
const ComponentC = {
/* ... */
}
const app = Vue.createApp({
// 如果要引用组件ComponentA,则使用<component-a>
components: {
'component-a': ComponentA,
ComponentB // ES2015+可以省略
}
})
注意局部注册的组件在其子组件中不可用。例如,如果你希望 ComponentA 在
ComponentB 中可用,则你需要这样写:
// 未使用模块系统的局部注册
const ComponentB = {
components: {
'component-a': ComponentA
}
// ...
}
// 在模块系统中的局部注册
import ComponentA from './ComponentA.vue'
export default {
components: {
ComponentA
}
// ...
}
2. 父组件主动传递数据
首先要说明的是,在DOM中使用props中定义的变量名字时,我们应该将其转化为等价的 kebab-case 形式。
const app = Vue.createApp({})
app.component('blog-post', {
// 在 JavaScript 中使用 camelCase
props: ['postTitle'],
template: '<h3>{{ postTitle }}</h3>'
})
<!-- 在 HTML 中使用 kebab-case -->
<blog-post post-title="hello!"></blog-post>
2.1 Props
传递静态&动态Prop
<!-- 父组件传入静态值 -->
<blog-post title="My journey with Vue"></blog-post>
<!-- 父组件将当前作用域的post.title及其他一些数据处理后绑定到子组件内prop定义的title -->
<blog-post :title="post.title + ' by ' + post.author.name"></blog-post>
<!-- 对于数字,布尔值,数组以及对象,即使我们传入静态值,我们也需要通过 `v-bind` 来告诉 Vue 这是一个JS表达式而不是一个字符串-->
<blog-post :likes="42"></blog-post>
<blog-post is-published></blog-post>
<blog-post :is-published="false"></blog-post>
<blog-post :comment-ids="[234, 266, 273]"></blog-post>
<blog-post
:author="{
name: 'Veronica',
company: 'Veridian Dynamics'
}"
></blog-post>
<!-- 对于布尔值,只要在父组件 Attribute 列表内定义了该属性(如下面的is-published),包含prop没有值的情况在内,都意味着 `true`;如果没有在 props 中把 is-published 设为 Boolean 类型,则这里的值为空字符串,而不是 `true` -->
<blog-post is-published></blog-post>
<!-- 如果想要将一个对象的所有 property 都作为 prop 传入,可以使用不带参数的 v-bind (用 v-bind 代替 :prop-name) -->
<!-- 例如下面的模板(对象author在上面一点) -->
<blog-post v-bind="post"></blog-post>
<!-- 等价于: -->
blog-post v-bind:id="post.id" v-bind:title="post.title"></blog-post>
Prop验证
除了以字符串数组形式定义prop,我们还可以以对象形式定义,并且具有更多的可选项,代码如下。
app.component('my-component', {
props: {
title: {
type: String, // 可选值:String, Number, Boolean, Array, Object, Date, Function, Symbol, 或是自定义的其他构造函数
required: true, // 是否必须
default: "我的组件", // 默认值:对象或数组的默认值必须从一个工厂函数返回 default() {return {/* ... */)}}
// 验证值的函数,返回true/false
validator(value) {
if (value.length() < 4) {
return false;
}
else {
return true;
}
}
}
}
})
如果type
为数组,则要求值类型为数组中的任意一个类型。
prop 会在一个组件实例创建之前进行验证,所以实例的 property (如
data
、computed
等) 在 default
或 validator
函数中是不可用的。
单向数据流
父级 prop的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。这样的特性意味着我们不应该在子组件内改变prop,无视单向数据绑定可能会导致意料之外的结果。
2.2 Slot
插槽内容
<!-- todo-button 组件模板 -->
<button class="btn-primary">
<!-- `<todo-button>Hello</todo-button>` 中的Hello将会替换<slot>这行。实际上,不仅仅是字符串,包括HTML在内的模板代码都可以被插槽slot替换。如果没有定义slot,自定义组件间的内容将会被抛弃!-->
<slot>默认内容</slot>
<!-- slot间可以定义默认内容 -->
</button>
::: note
插槽的作用域与模板其余部分相同的实例 property 相同。
:::
具名插槽
如果我们在模板中需要使用多个 slot ,我们可以使用 slot 元素中特殊的
attribute :name。一个不带 name 的 slot 带有隐含的名字 “default”。
<!-- 模板定义 -->
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
</div>
<!-- 模板使用 -->
<!-- v-slot 只能添加在 template 上 -->
<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>
</base-layout>
作用域插槽
有时我们需要将数据从组件内部传到使用组建的地方,那么我们可以使用以下方法。
<!-- <todo-list> 模板定义 -->
<ul>
<li v-for="( item, index ) in items">
<slot :item="item" :index="index" :another-attribute="anotherAttribute"></slot>
</li>
</ul>
<!-- 模板使用 -->
<todo-list>
<!-- 为default插槽定义插槽prop的名字,如果只有一个插槽,可以省略 `:default` -->
<template v-slot:default="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</template>
</todo-list>
::: tip
作用域插槽的内部工作原理是将你的插槽内容包括在一个传入单个参数的函数里,这意味着v-slot
的值实际上可以是任何能够作为函数定义中的参数的
JavaScript 表达式。因此你也可以使用 ES2015 解构 来传入具体的插槽 prop。
:::
动态插槽名
<!-- 模板使用 -->
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
</base-layout>
具名插槽的缩写
<!-- v-slot 可以缩写为 # -->
<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>
</base-layout>
<!-- 然而,和其它指令一样,该缩写只在其有参数的时候才可用。这意味着以下语法是无效的: -->
<todo-list #="{ item }">
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</todo-list>
2.3 Provide / Inject
使用provide和inject,父组件不关心谁会接收,子组件不关心数据从何而来,数据无需逐级传递,而是直接传递到需要的地方。
app.component('todo-list', {
data() {
return {
todos: ['Feed a cat', 'Buy tickets']
}
},
provide: {
user: 'John Doe',
},
// 如果我们尝试在此处 provide 一些组件的实例 property,我们应该将 provide 转换为返回对象的函数。
/*
provide: {
return {
todoLength: this.todos.length
}
}
*/
template: `
<div>
{{ todos.length }}
<!-- 模板的其余部分 -->
</div>
`
})
app.component('todo-list-statistics', {
inject: ['user'],
created() {
console.log(`Injected property: ${this.user}`) // > 注入的 property: John Doe
}
})
默认情况下,provide/inject绑定不是响应式的,但我们可以通过传递一个 ref
property 或 reactive 对象给 provide 来改变这种行为。
app.component('todo-list', {
// ...
provide() {
return {
todoLength: Vue.computed(() => this.todos.length)
}
}
})
app.component('todo-list-statistics', {
inject: ['todoLength'],
created() {
console.log(`Injected property: ${this.todoLength.value}`) // > 注入的 property: 5
}
})
3. 子组件主动更新数据
3.1 Emits
与组件和 prop一样,事件名提供了自动的大小写转换。如果在子组件中触发一个以 camelCase命名的事件,你将可以在父组件中添加一个 kebab-case 的监听器。
this.$emit('myEvent')
<my-component @my-event="doSomething"></my-component>
与 props 的命名一样,当你使用 DOM 模板时,我们建议使用 kebab-case
事件监听器。如果你使用的是字符串模板,这个限制就不适用。
定义emits
可以通过 emits 选项在组件上定义发出的事件。
app.component('custom-form', {
emits: ['inFocus', 'submit', 'click'] // 自定义事件将会覆盖原生事件
})
验证抛出的事件
app.component('custom-form', {
emits: {
// 没有验证
click: null,
// 验证 submit 事件
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
},
methods: {
submitForm(email, password) {
this.$emit('submit', { email, password })
}
}
})
v-model参数
默认情况下,组件上的 v-model 使用 modelValue 作为 prop 和update:modelValue 作为事件。我们可以通过向 v-model传递参数来修改这些名称:
<my-component v-model:title="bookTitle"></my-component>
app.component('my-component', {
props: {
title: String
},
emits: ['update:title'],
template: `
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)">
`
})
v-model修饰符
v-model
只内置了三个修饰符:.trim
, .number
, .lazy
但我们可以自定义修饰符。
自定义修饰符时,我们需要在props中新增一个 modelModifiers ,其中的 model 即为 v-model
绑定的参数名,如果没有绑定参数,则其名称就是
modelModifiers。如果该实例使用了自定义修饰符,则其在props中的
modelModifiers 值为true。
下面自定义了一个 .capitalize
修饰符,可以在输入时保持首字母大写。
<div id="app">
<my-component v-model.capitalize="myText"></my-component>
{{ myText }}
</div><my-component v-model:description.capitalize="myText"></my-component>
app.component('my-component', {
props: {
modelValue: String,
modelModifiers: {
default: () => ({})
}
},
emits: ['update:modelValue'],
methods: {
emitValue(e) {
let value = e.target.value
if (this.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
this.$emit('update:modelValue', value)
}
},
template: `<input
type="text"
:value="modelValue"
@input="emitValue">`
})app.component('my-component', {
props: ['description', 'descriptionModifiers'],
emits: ['update:description'],
template: `
<input type="text"
:value="description"
@input="$emit('update:description', $event.target.value)">
`,
created() {
console.log(this.descriptionModifiers) // { capitalize: true }
}
})
4. 其他知识点
4.1 动态组件 & 异步组件
动态组件
我们之前曾经在一个多标签的界面中使用 is attribute 来切换不同的组件:
<component :is="currentTabComponent"></component>
如果我们在切换组件的时候想要保存这些组件的状态,以避免反复渲染导致的性能问题,我们可以将其包裹在<keep-alive>
元素中。
<!-- 失活的组件将会被缓存!-->
<keep-alive>
<component :is="currentTabComponent"></component>
</keep-alive>
异步组件
在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了实现这个效果,Vue 有一个defineAsyncComponent 方法:
const { createApp, defineAsyncComponent } = Vue
const app = createApp({})
const AsyncComp = defineAsyncComponent(
() =>
new Promise((resolve, reject) => {
resolve({
template: '<div>I am async!</div>'
})
})
// webpack 2 及以上版本和 ES2015 语法相结合后,我们还可以这样使用动态地导入:(将new Promise换为import)
import('./components/AsyncComponent.vue')
)
app.component('async-example', AsyncComp)
局部注册组件用法相同,根据之前所学知识替换即可。
4.2 模板引用
尽管存在 prop 和事件,但有时你可能仍然需要在 JavaScript 中直接访问子组件。为此,可以使用 ref attribute 为子组件或 HTML 元素指定引用 ID。例如:
const app = Vue.createApp({})
app.component('base-input', {
template: `
<input ref="input" />
`,
methods: {
focusInput() {
this.$refs.input.focus()
}
},
mounted() {
this.focusInput()
}
})
此外,还可以向组件本身添加另一个 ref,并使用它从父组件触发 focusInput
事件:
<base-input ref="usernameInput"></base-input>
this.$refs.usernameInput.focusInput()
::: warning
$refs 只会在组件渲染完成之后生效。这仅作为一个用于直接操作子元素的"逃生舱"------你应该避免在模板或计算属性中访问 $refs。
:::
4.3 处理边界情况
强制更新
当你处于一种非常罕见的情况下时,你可能必须手动强制更新,那么你可以使用forceUpdate
。
低级静态组件与 v-once
在 Vue 中渲染纯 HTML元素的速度非常快,但有时你可能有一个包含很多静态内容的组件。在这些情况下,可以通过向根元素添加 v-once 指令来确保只对其求值一次,然后进行缓存,如下所示:
app.component('terms-of-service', {
template: `
<div v-once>
<h1>Terms of Service</h1>
... a lot of static content ...
</div>
`
})
官方也是千叮咛万嘱咐不要过度使用这种模式,因为这很容易导致因开发人员对代码的不熟悉而引起难以弄清楚模板没有正确更新。
4.4 非 Prop的 Attribute
默认情况下,我们在父组件上定义的 非 prop 的 Attribute 会继承到模板的根节点。
<!-- 具有非 prop 的 attribute 的 date-picker 组件-->
<date-picker data-status="activated"></date-picker>
<!-- 渲染后的 date-picker 组件, data-status自动继承 -->
<div class="date-picker" data-status="activated">
<input type="datetime-local" />
</div>
如果要禁止这种默认行为,可以在组件的选项中设置inhenritAttrs: false
。禁用这一默认行为的常见场景是需要将 Attribute 应用于根节点之外的元素。
app.component('date-picker', {
inheritAttrs: false,
template: `
<div class="date-picker">
<!-- 利用 v-bind 将 attr 应用于 input 元素 -->
<input type="datetime-local" v-bind="$attrs" />
</div>
`
})
与单个根节点组件不同,具有多个根节点的组件不具有自动 attribute
fallthrough (隐式贯穿) 行为。如果未显式绑定 $attrs,将发出运行时警告。