本文为本人在学习vue时所记录的有关组件的笔记,其中详细介绍了vue中的组件及其使用,其中大部分内容来自vue官方中文文档,对于其中的一些知识也加入了一些自己的理解。
什么是组件?
组件允许我们将 UI 划分为独立的、可重用的部分,就像我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。或者说每一个组件都像是一个函数一样,是独立的,可以进行复用。
我们一般会将 Vue 组件定义在一个单独的 vue文件中,这被叫做单文件组件(简称 SFC),例如:
要使用一个子组件,我们需要在父组件中导入它。假设我们把计数器组件放在了一个叫做 ButtonCounter.vue 的文件中,这个组件将会以默认导出的形式被暴露给外部。例如:
Here is a child component!
组件可以被重用任意多次,并且每个组件之中的内容都是独立,不会被影响。例如:
Here is a child component!
一个 Vue 组件在使用前需要先被“注册”,这样 Vue 才能在渲染模板时找到其对应的实现。组件注册有两种方式:全局注册和局部注册。
我们可以使用 Vue 应用实例的 .component() 方法,让组件在当前 Vue 应用中全局可用。例如:
import { createApp } from 'vue' const app = createApp({}) app.component( // 注册的名字 'MyComponent', // 组件的实现 { /* ... */ } )
如果使用单文件组件,你可以注册被导入的 .vue 文件:
import MyComponent from './App.vue' app.component('MyComponent', MyComponent)
.component() 方法可以被链式调用:
app .component('ComponentA', ComponentA) .component('ComponentB', ComponentB) .component('ComponentC', ComponentC)
全局注册的组件可以在此应用的任意组件的模板中使用:
全局注册虽然很方便,但有以下几个问题:
相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好。
局部注册需要使用 components 选项:
对于每个 components 对象里的属性,它们的 key 名就是注册的组件名,而值就是相应组件的实现。
首先什么是props?
Props 是一种特别的 attributes,我个人理解props就是一种用于从父组件向子组件传递数据的机制。具体来说,props 是子组件接收父组件传递的数据的一种方式。子组件通过 props 属性来声明自己接收哪些数据,并可以在其模板中使用这些数据。
props在使用时需要先被显式声明,这样vue才知道你用了哪些props。
首先定义props可以用props选项来定义,例如我想传给该组件一个info信息,那我就必须在该组件内声明一下:
export default { props: ['info'], created() { console.log(this.info) } }
除此之外,还可以使用对象式声明的方式,例如,假设我们有一个博文组件 BlogPost.vue,父组件想要向这个博文组件传递标题、内容和作者信息。我们可以这样声明 props:
{{ title }}
{{ content }}
Written by: {{ author }}
在这个例子中:
父组件可以这样使用 BlogPost组件:
上面的例子中props的值都是静态的,实际上也可以是动态的,可以使用v-bind或:来进行动态绑定,其次他还可以是多种数据类型(String,Number,Boolean,Array,Object),例如:
name: 'Veronica', company: 'Veridian Dynamics' }" />
一个对象内还可以绑定多个prop,例如
export default { data() { return { post: { id: 1, title: 'My Journey with Vue' } } } }
注意:所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。
另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop。若你这么做了,Vue 会在控制台上向你抛出警告:例如:
export default { props: ['foo'], created() { // ❌ 警告!prop 是只读的! this.foo = 'bar' } }
也即是说爸爸让你改你才能改,子组件不能擅作主张自己更改自己的props。
Vue 组件可以更细致地声明对传入的 props 的校验要求。比如我们上面已经看到过的类型声明,如果传入的值不满足类型要求,Vue 会在浏览器控制台中抛出警告来提醒使用者。要声明对 props 的校验,你可以向 props 选项提供一个带有 props 校验选项的对象,校验选项中的 type 可以是下列这些原生构造函数:String、Number、Boolean、Array、Object、Date、Function、Symbol、Error。例如:
export default { props: { // 基础类型检查 //(给出 `null` 和 `undefined` 值则会跳过任何类型检查) propA: Number, // 多种可能的类型 propB: [String, Number], // 必传,且为 String 类型 propC: { type: String, required: true }, // 必传但可为 null 的字符串 propD: { type: [String, null], required: true }, // Number 类型的默认值 propE: { type: Number, default: 100 }, // 对象类型的默认值 propF: { type: Object, // 对象或者数组应当用工厂函数返回。 // 工厂函数会收到组件所接收的原始 props // 作为参数 default(rawProps) { return { message: 'hello' } } }, // 自定义类型校验函数 // 在 3.4+ 中完整的 props 作为第二个参数传入 propG: { validator(value, props) { // The value must match one of these strings return ['success', 'warning', 'danger'].includes(value) } }, // 函数类型的默认值 propH: { type: Function, // 不像对象或数组的默认,这不是一个 // 工厂函数。这会是一个用来作为默认值的函数 default() { return 'Default function' } } } }
另外,type 也可以是自定义的类或构造函数,Vue 将会通过 instanceof 来检查类型是否匹配。例如下面这个类:
class Person { constructor(firstName, lastName) { this.firstName = firstName this.lastName = lastName } }
export default { props: { author: Person } }
一些补充细节:
当 prop 的校验失败后,Vue 会抛出一个控制台警告 (在开发模式下)。
校验选项中的 type 可以是下列这些原生构造函数:String、Number、Boolean、Array、Object、Date、Function、Symbol、Error
为了更贴近原生 boolean attributes 的行为,声明为 Boolean 类型的 props 有特别的类型转换规则。以带有如下声明的 组件为例
export default { props: { disabled: Boolean } }
该组件可以被这样使用:
当一个 prop 被声明为允许多种类型时Boolean 的转换规则也将被应用。然而,当同时允许 String 和 Boolean 时,有一种边缘情况——只有当 Boolean出现在 String 之前时,Boolean 转换规则才适用:
// disabled 将被转换为 true export default { props: { disabled: [Boolean, Number] } } // disabled 将被转换为 true export default { props: { disabled: [Boolean, String] } } // disabled 将被转换为 true export default { props: { disabled: [Number, Boolean] } } // disabled 将被解析为空字符串 (disabled="") export default { props: { disabled: [String, Boolean] } }
可以使用$emit方法触发自定义事件,例如在一个按钮中
e m i t ( ) 方法在组件实例上也同样以 t h i s . emit() 方法在组件实例上也同样以 this. emit()方法在组件实例上也同样以this.emit() 的形式可用:
export default { methods: { submit() { this.$emit('someEvent') } } }
父组件可以通过 v-on (缩写为 @) 来监听事件:
同样,组件的事件监听器也支持 .once 修饰符,也就是修饰了只会触发一次:
事件参数
有时事件在触发时需要携带一些参数,举例来说,我们想要 组件来管理文本会缩放得多大。我们可以在按钮上绑定一个“increaseBy”事件,并传递一个额外的参数1。
然后我们在父组件中监听事件,我们可以先简单写一个内联的箭头函数作为监听器,此函数会接收到事件附带的参数:
count += n" />
这样在每次点击按钮后,count的值就会加一。
或者,也可以用一个组件方法来作为事件处理函数:
再写个方法来接收传递的参数
methods: { increaseCount(n) { this.count += n } }
v-model 可以在组件上使用以实现双向绑定
例如现有组件,其界面为让输入框内的文字和参数modelValue保持一致,输入框内更新,参数值也会更新。
那么在使用该组件时就可以用v-model来和组件中的值进行绑定,例如:
{{ message }}
这样message的值就和输入框中的值就行了绑定,最后呈现的效果为在输入框后面的文字回合输入框中保持一致。
此外,v-model还可以接收一个参数。例如现在有一个标题参数title
子组件中使用 title prop 和 update:title 事件来更新父组件的值,而非默认的 modelValue prop 和 update:modelValue事件:。例如:
上面的都是一个v-model的,一个组件上还可以绑定多个有参数的v-model
例如:
v-model 有一些内置的修饰符,例如 .trim,.number 和 .lazy。在组件中还可以自定义一些修饰符。
例如:创建一个自定义的修饰符 capitalize,它会自动将 v-model 绑定输入的字符串值第一个字母转为大写:
父组件中代码如下
子组件:每次输入更新时都会触发emitValue方法,方法中会将第一个单词变为大写
另外,在使用修饰符的同时加上参数也是可以的
例如:
“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props 或emits的 attribute 或者 v-on 事件监听器。最常见的例子就是 class、style 和id 。
class继承
例如现有模板MyButton中一个按钮:
在父组件中使用时加上了class样式,如:
最后渲染出的结果就是
父组件中的样式将会传递给模板中按钮,倘若子组件中已有class,则会把样式进行合并。
例如子组件中有按钮:
最后的结果就会变为
v-on继承
这样的规则也适用于v-on事件监听器中,如果父组件中有个点击事件,那么模板中的元素被点击时也会触发父组件的事件,同样,如果子组件中已有事件,那么点击后子组件的事件和父组件的事件都会被触发。
深层组件继承
如果子组件中又使用了另一个子组件,例如组件中根节点,那么在使用
时传递的attribute会直接传递给
禁用继承
这种继承关系有时可能我们不想让它生效,此时可以自己在组件选项中设置inheritAttrs:false
例如:
多根节点的Attribute继承
如果一个组件中有多个根节点,vue并不会自动透传所有的根节点,此时需要使用$attrs进行显示绑定,否则vue不知道要把attribute透传到哪里。例如在组件中,这样父组件中的attribute就会精准透传到中
... .. ...
js中访问attribute
在js中可以在
例如:
插槽可以让接收模板内容,为子组件传递一些模块片段,并让子组件渲染这些片段。
例如当前有一个组件如下:
在使用时如下:
请点击我
最终渲染如下
使用时中的“请点击我”就会替换掉,实现如上效果,插槽中的内容可以是任意合法的模板内容,并且插槽内容可以访问到父组件的数据作用域,因此可以传入多个元素或者是组件,例如有一个点击后数值加一的按钮,还有个组件,内容很简单只有一个❤如下:
❤️
在使用时就可以把它也传入:
{{ count }} 点一下加一,当前为:{{ count }} 点我!
最终效果如下:
在外部没有提供任何内容的情况下,可以为插槽指定默认内容,比如有这样一个 组件:
在使用时如果不传入内容就会默认展示默认内容,有新内容则展示新内容
最终效果如下:
### 具名插槽
一个组件中还可以包含多个插槽出口,每一个元素可以有一个特殊的 attribute name,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容:
例如现有组件如下:
这类带 name 的插槽被称为具名插槽 (named slots)。没有提供 name 的 出口会隐式地命名为“default”。
在父组件中使用该组件时,可以将多个插槽内容传入到各自目标插槽的出口。具体做法为,我们需要使用一个含 v-slot (可以简写为#)指令的 元素,并将目标插槽的名字传给该指令:
例如:
Here might be a page title
A paragraph for the main content.
And another one.
Here's some contact info
这样就做到了具体的替换。
最终渲染效果如下:
Here might be a page title
A paragraph for the main content.
And another one.
有时需要根据插槽是否存在来渲染某些内容,这时可以结合使用$slot属性与v-if来实现。
在下面的示例中,我们定义了一个卡片组件,它拥有三个条件插槽:header、footer 和 default。 当 header、footer 或 default 存在时,我们希望包装它们以提供额外的样式,也就是会将div中符合v-if的提供额外的样式。
动态指令参数在 v-slot 上也是有效的,即可以定义下面这样的动态插槽名:
... ...
如果想要同时使用父组件域内和子组件域内的数据。要做到这一点,我们可以像对组件传递 props 那样,向一个插槽的出口上传递 attributes:
当需要接收插槽 props 时,默认插槽和具名插槽的使用方式有一些小区别。下面通过子组件标签上的 v-slot
指令,直接接收到了一个插槽 props 对象:
{{ slotProps.text }} {{ slotProps.count }}
这样在子组件域内就拿到了父组件中的text和count
具名作用域插槽
具名作用域插槽的工作方式也是类似的,插槽 props 可以作为 v-slot
指令的值被访问到:v-slot:name=“slotProps”。当使用缩写时是这样:
{{ headerProps }}
向具名插槽中传入 props:
这里要特别注意一点:
插槽上的 name 是一个 Vue 特别保留的 attribute,不会作为 props 传递给插槽。因此最终 headerProps 的结果是 { message: ‘hello’ }。
如果你同时使用了具名插槽与默认插槽,则需要为默认插槽使用显式的 标签。尝试直接为组件添加 v-slot
指令将导致编译错误。这是为了避免因默认插槽的 props 的作用域而困惑。举例:
{{ message }}
{{ message }}
为默认插槽使用显式的 标签有助于更清晰地指出
message
属性在其他插槽中不可用:
{{ message }}
Here's some contact info
高级列表组件示例
下面展示一个作用域插槽的应用场景,来看一个 `组件的例子。它会渲染一个列表,并同时会封装一些加载远端数据的逻辑、使用数据进行列表渲染、或者是像分页或无限滚动这样更进阶的功能。然而我们希望它能够保留足够的灵活性,将对单个列表元素内容和样式的控制权留给使用它的父组件。
{{ body }}
by {{ username }} | {{ likes }} likes
- Loading...
-
最终效果如下:
通常情况下,当我们需要从父组件向子组件传递数据时,会使用 props。想象一下这样的结构:有一些多层级嵌套的组件,形成了一棵巨大的组件树,而某个深层的子组件需要一个较远的祖先组件中的部分数据。在这种情况下,如果仅使用 props 则必须将其沿着组件链逐级传递下去,这会非常麻烦。
provide 和inject 可以帮助我们解决这一问题[1]。一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入中父组件提供给整条链路的依赖。
可以使用provide()为组件后代提供数据,例如
如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值。
在一些场景中,默认值可能需要通过调用一个函数或初始化一个类来取得。为了避免在用不到默认值的情况下进行不必要的计算或产生副作用,我们可以使用工厂函数来创建默认值:
const value = inject('key', () => new ExpensiveClass(), true)
第三个参数表示默认值应该被当作一个工厂函数。
在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue 提供了defineAsynccomponent 方法来实现此功能。例如:
import { defineAsyncComponent } from 'vue' const AsyncComp = defineAsyncComponent(() => { return new Promise((resolve, reject) => { // ...从服务器获取组件 resolve(/* 获取到的组件 */) }) })
defineAsynccomponent方法接收一个返回 Promise 的加载函数。这个 Promise的 resolve 回调方法应该在从服务器获得组件定义时调用。也可以调用 reject(reason)表明加载失败。
最后得到的 AsyncComp 是一个外层包装过的组件,仅在负面需要它渲染时才会调用加载内部实际组件的函数。它会将接收到的props 和插槽传给内部组件,所以你可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载。
与普通组件一样,异步组件可以使用app.component全局注册,例如
app.component('MyComponent', defineAsyncComponent(() => import('./components/MyComponent.vue') ))
或者在父组件中直接定义
加载和错误
异步操作不可避免地会涉及到加载和错误状态,因此 defineAsynccomponent()也支持在高级选项中处理这些状态,例如:
const AsyncComp = defineAsyncComponent({ // 加载函数 loader: () => import('./Foo.vue'), // 加载异步组件时使用的组件 loadingComponent: LoadingComponent, // 展示加载组件前的延迟时间,默认为 200ms delay: 200, // 加载失败后展示的组件 errorComponent: ErrorComponent, // 如果提供了一个 timeout 时间限制,并超时了 // 也会显示这里配置的报错组件,默认值是:Infinity timeout: 3000 })
是一个抽象的组件,用于动态地渲染不同的组件或元素。
通过绑定 is 属性可以实现动态组件的切换和渲染。
和 提供了在 Vue.js 中实现过渡和动画效果的功能。
通过定义过渡的 CSS 类名,可以控制元素在进入或离开 DOM 时的动画效果。
当一个 组件中的元素被插入或移除时,会发生下面这些事情:
使用例子:最后的效果为点击一次按钮后,下面的文字会出现或者消失。实现的方法为定义好元素进入时和消失时的状态,在写好过渡时的效果。
我的名字
过程如下:
v-enter-from
被移除的同时),在过渡或动画完成之后移除。v-leave-from
被移除的同时),在过渡或动画完成之后移除。是一个抽象组件,用于保持组件状态或避免多次渲染。
当组件被 包裹时,其状态将会被缓存,而不是每次切换时重新渲染。
例如:下方引用了两个组件A和B
页面中渲染效果如下:点击上面的选项会切换下面的内容,不过由于组件使用了KeepAlive,因此每次切换时不会重复渲染,会保留之前的值。
允许你将 DOM 元素渲染到应用的任何地方,而不受当前 DOM 结构的限制。这在需要在应用中动态移动元素时非常有用,例如在模态框中渲染弹出内容。
Hello from the modal!
是 Vue.js 3.x 中新增的组件,用于处理异步组件的加载和状态。它可以在异步组件加载完成之前显示占位内容,并处理加载状态和错误。例如:
Loading...