title: vue3学习笔记(三)
date: 2023-03-25 13:40:50
tags:
- vue
- js
top_img: false
urlname: vue3学习笔记(三)
组件注册
全局注册
我们可以使用 Vue 应用实例的 app.component()
方法,让组件在当前 Vue 应用中全局可用。
全局注册的组件可以在此应用的任意组件的模板中使用:
import { createApp } from 'vue'
const app = createApp({})
app.component(
// 注册的名字
'MyComponent',
// 组件的实现
{
/* ... */
}
)
单文件
import MyComponent from './App.vue'
app.component('MyComponent', MyComponent)
多文件
app
.component('ComponentA', ComponentA)
.component('ComponentB', ComponentB)
.component('ComponentC', ComponentC)
局部注册
全局注册虽然很方便,但有以下几个问题:
- 全局注册,但并没有被使用的组件无法在生产打包时被自动移除 (也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 JS 文件中。
- 全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性。
相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好。
请注意:局部注册的组件在后代组件中并不可用。在这个例子中,ComponentA
注册后仅在当前组件可用,而在任何的子组件或更深层的子组件中都不可用。
组件名格式
在整个指引中,我们都使用 PascalCase 作为组件名的注册格式,这是因为:
- PascalCase 是合法的 JavaScript 标识符。这使得在 JavaScript 中导入和注册组件都很容易,同时 IDE 也能提供较好的自动补全。
- `` 在模板中更明显地表明了这是一个 Vue 组件,而不是原生 HTML 元素。同时也能够将 Vue 组件和自定义元素 (web components) 区分开来。
在单文件组件和内联字符串模板中,我们都推荐这样做。但是,PascalCase 的标签名在 DOM 模板中是不可用的,详情参见 DOM 模板解析注意事项。
为了方便,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。这意味着一个以 MyComponent
为名注册的组件,在模板中可以通过 或
引用。这让我们能够使用同样的 JavaScript 组件注册代码来配合不同来源的模板。
<script>
import ComponentA from './ComponentA.vue'
export default {
components: {
ComponentA
}
}
</script>
<template>
<ComponentA />
</template>
Props
Props 声明
一个组件需要显式声明它所接受的 props,这样 Vue 才能知道外部传入的哪些是 props,哪些是透传 attribute
props 需要使用props
选项来定义:
<template>
<h2>props使用</h2>
<h2>{{ age }}</h2>
<h2>{{ message }}</h2>
</template>
<script>
export default {
props: {
message: {
// 类型
type: String,
// 默认值
default: '你好'
}
},
data() {
return {
age: 10
}
}
}
</script>
传递 prop 的细节
Prop 名字格式
如果一个 prop 的名字很长,应使用 camelCase 形式,因为它们是合法的 JavaScript 标识符,可以直接在模板的表达式中使用,也可以避免在作为属性 key 名时必须加上引号。
静态 动态 Prop
<!-- 父组件 -->
<template>
<div>
<!-- 根据一个变量的值静态传入 -->
<BlogPost greetMessage="My journey with Vue" />
<!-- 根据一个变量的值动态传入 -->
<PropsUser :greetMessage="greetMessage"/>
<!-- 根据一个更复杂表达式的值动态传入 -->
<PropsUser :greetMessage="greetMessage + ' by ' + kcldh" />
</div>
</template>
<script>
import PropsUser from "./components/PropsUser.vue";
export default {
components: {
PropsUser
},
data() {
return {
greetMessage: "hello world"
}
}
}
</script>
<!-- 子组件 -->
<template>
<h2>props使用</h2>
<h2>{{ message }}</h2>
</template>
<script>
export default {
props: {
greetMessage: {
type: String,
default: '你好'
}
}
}
</script>
单向数据流
所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。
另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop。若你这么做了,Vue 会在控制台上向你抛出警告:
export default {
props: ['foo'],
created() {
// ❌ 警告!prop 是只读的!
this.foo = 'bar'
}
}
导致你想要更改一个 prop 的需求通常来源于以下两种场景:
-
prop 被用于传入初始值;而子组件想在之后将其作为一个局部数据属性。在这种情况下,最好是新定义一个局部数据属性,从 props 上获取初始值即可:
export default { props: ['initialCounter'], data() { return { // 计数器只是将 this.initialCounter 作为初始值 // 像下面这样做就使 prop 和后续更新无关了 counter: this.initialCounter } } }
-
需要对传入的 prop 值做进一步的转换。在这种情况中,最好是基于该 prop 值定义一个计算属性:
export default { props: ['size'], computed: { // 该 prop 变更时计算属性也会自动更新 normalizedSize() { return this.size.trim().toLowerCase() } } }
Prop 校验
Vue 组件可以更细致地声明对传入的 props 的校验要求。比如我们上面已经看到过的类型声明,如果传入的值不满足类型要求,Vue 会在浏览器控制台中抛出警告来提醒使用者。这在开发给其他开发者使用的组件时非常有用。
要声明对 props 的校验,你可以向 props
选项提供一个带有 props 校验选项的对象,例如:
export default {
props: {
// 基础类型检查
//(给出 `null` 和 `undefined` 值则会跳过任何类型检查)
propA: Number,
// 多种可能的类型
propB: [String, Number],
// 必传,且为 String 类型
propC: {
type: String,
required: true
},
// Number 类型的默认值
propD: {
type: Number,
default: 100
},
// 对象类型的默认值
propE: {
type: Object,
// 对象或者数组应当用工厂函数返回。
// 工厂函数会收到组件所接收的原始 props
// 作为参数
default(rawProps) {
return { message: 'hello' }
}
},
// 自定义类型校验函数
propF: {
validator(value) {
// The value must match one of these strings
return ['success', 'warning', 'danger'].includes(value)
}
},
// 函数类型的默认值
propG: {
type: Function,
// 不像对象或数组的默认,这不是一个
// 工厂函数。这会是一个用来作为默认值的函数
default() {
return 'Default function'
}
}
}
}
一些补充细节:
- 所有 prop 默认都是可选的,除非声明了
required: true
。 - 除
Boolean
外的未传递的可选 prop 将会有一个默认值undefined
。 Boolean
类型的未传递 prop 将被转换为false
。这可以通过为它设置default
来更改——例如:设置为default: undefined
将与非布尔类型的 prop 的行为保持一致。- 如果声明了
default
值,那么在 prop 的值被解析为undefined
时,无论 prop 是未被传递还是显式指明的undefined
,都会改为default
值。
当 prop 的校验失败后,Vue 会抛出一个控制台警告 (在开发模式下)。
组件事件
触发与监听事件
在组件的模板表达式中,可以直接使用 $emit
方法触发自定义事件 (例如:在 v-on
的处理函数中):
<!-- MyComponent -->
<button @click="$emit('someEvent')">click me</button>
$emit()
方法在组件实例上也同样以 this.$emit()
的形式可用:
export default {
methods: {
submit() {
this.$emit('someEvent')
}
}
}
父组件可以通过 v-on
(缩写为 @
) 来监听事件:
<MyComponent @some-event="callback" />
同样,组件的事件监听器也支持 .once
修饰符:
<MyComponent @some-event.once="callback" />
像组件与 prop 一样,事件的名字也提供了自动的格式转换。注意这里我们触发了一个以 camelCase 形式命名的事件,但在父组件中可以使用 kebab-case 形式来监听。与 prop 大小写格式一样,在模板中我们也推荐使用 kebab-case 形式来编写监听器。
父组件
<template>
<div>
<Hello @injectMsg="getChildMsg"></Hello>
</div>
</template>
<script>
import Hello from './Hello.vue'
export default {
name: "Content",
components: {
Hello
},
data() {
return {
msg: '',
content: 'content'
}
},
methods: {
getChildMsg: function (value) {
this.msg = value
}
}
}
</script>
子组件
<template>
<div>
<p> {{ msg }}</p>
</div>
<button @click="sendParent">提交数据给父组件</button>
</template>
<script>
export default {
data() {
return {
msg: 'world'
}
},
methods: {
sendParent: function () {
// this.$emit('事件名称', '发送的事件参数')
this.$emit('injectMsg', this.msg)
}
}
}
</script>
插槽 Slots
插槽内容与出口
在之前的章节中,我们已经了解到组件能够接收任意类型的 JavaScript 值作为 props,但组件要如何接收模板内容呢?在某些场景中,我们可能想要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。
举例来说,这里有一个 `` 组件,可以像这样使用:
<FancyButton>
Click me! <!-- 插槽内容 -->
</FancyButton>
而 `` 的模板是这样的:
<button class="fancy-btn">
<slot></slot> <!-- 插槽出口 -->
</button>
`` 元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VxMBwA1Y-1679897350564)(null)]
最终渲染出的 DOM 是这样:
<button class="fancy-btn">Click me!</button>
渲染作用域
插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的。举例来说:
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>
这里的两个 {{ message }}
插值表达式渲染的内容都是一样的。
插槽内容无法访问子组件的数据。Vue 模板中的表达式只能访问其定义时所处的作用域,这和 JavaScript 的词法作用域规则是一致的。换言之:
父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。
默认内容
在外部没有提供任何内容的情况下,可以为插槽指定默认内容。比如有这样一个 `` 组件:
<button type="submit">
<slot></slot>
</button>
如果我们想在父组件没有提供任何插槽内容时在 内渲染“Submit”,只需要将“Submit”写在
标签之间来作为默认内容:
<button type="submit">
<slot>
Submit <!-- 默认内容 -->
</slot>
</button>
现在,当我们在父组件中使用 `` 且没有提供任何插槽内容时:
<SubmitButton />
“Submit”将会被作为默认内容渲染:
<button type="submit">Submit</button>
但如果我们提供了插槽内容:
<SubmitButton>Save</SubmitButton>
那么被显式提供的内容会取代默认内容:
<button type="submit">Save</button>
使用示例:
父组件
<script>
import Content from "./components/Content.vue";
export default {
data() {
return {}
},
components: {
Content
},
provide: {
message: "hello"
}
}
</script>
<template>
<Content>
<!-- 使用子组件的数据来渲染 -->
<template v-slot:default="obj">
<ul>
<li v-for="item in obj.list">
{{ item }}
</li>
</ul>
</template>
</Content>
</template>
子组件
<template>
<div>
<h2>content组件</h2>
<div>
<!-- 传值给父组件 -->
<slot :list="list"></slot>
</div>
</div>
</template>
<script>
export default {
data() {
return {
list: [1, 2, 3, 4, 5]
}
}
}
</script>
具名插槽
有时在一个组件中包含多个插槽出口是很有用的。举例来说,在一个 `` 组件中,有如下模板:
<div class="container">
<header>
<!-- 标题内容放这里 -->
</header>
<main>
<!-- 主要内容放这里 -->
</main>
<footer>
<!-- 底部内容放这里 -->
</footer>
</div>
对于这种场景,`` 元素可以有一个特殊的 attribute name
,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
这类带 name
的插槽被称为具名插槽 (named slots)。没有提供 name
的 `` 出口会隐式地命名为“default”。
在父组件中使用 `` 时,我们需要一种方式将多个插槽内容传入到各自目标插槽的出口。此时就需要用到具名插槽了:
要为具名插槽传入内容,我们需要使用一个含 v-slot
指令的 ` 元素,并将目标插槽的名字传给该指令:
<BaseLayout>
<template v-slot:header>
<!-- header 插槽的内容放这里 -->
</template>
</BaseLayout>
v-slot
有对应的简写 #
,因此 可以简写为
。其意思就是“将这部分模板片段传入子组件的 header 插槽中”。
下面我们给出完整的、向 `` 传递插槽内容的代码,指令均使用的是缩写形式:
<BaseLayout>
<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>
</BaseLayout>
当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 ` 节点都被隐式地视为默认插槽的内容。所以上面也可以写成:
<BaseLayout>
<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>
</BaseLayout>
现在 ` 元素中的所有内容都将被传递到相应的插槽。最终渲染出的 HTML 如下:
<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>
使用 JavaScript 函数来类比可能更有助于你来理解具名插槽:
// 传入不同的内容给不同名字的插槽
BaseLayout({
header: `...`,
default: `...`,
footer: `...`
})
// <BaseLayout> 渲染插槽内容到对应位置
function BaseLayout(slots) {
return `<div class="container">
<header>${slots.header}</header>
<main>${slots.default}</main>
<footer>${slots.footer}</footer>
</div>`
}
使用示例:
父组件
<script>
import Content from "./components/Content.vue";
export default {
components: {
Content
}
}
</script>
<template>
<Content>
<template v-slot:abc>
<button>按钮abc</button>
</template>
<template v-slot:bcd>
<button>按钮bcd</button>
</template>
</Content>
</template>
子组件
<template>
<div>
<h2>content组件</h2>
<div>
<!-- 多个slot可以使用name -->
<slot name="abc"></slot>
<slot name="bcd"></slot>
</div>
</div>
</template>
参考文档
参考链接:vue官网