VUE详解——组件篇

11 篇文章 0 订阅
3 篇文章 0 订阅

前言

本文为本人在学习vue时所记录的有关组件的笔记,其中详细介绍了vue中的组件及其使用,其中大部分内容来自vue官方中文文档,对于其中的一些知识也加入了一些自己的理解。

基础

什么是组件?

组件允许我们将 UI 划分为独立的、可重用的部分,就像我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。或者说每一个组件都像是一个函数一样,是独立的,可以进行复用。

我们一般会将 Vue 组件定义在一个单独的 vue文件中,这被叫做单文件组件(简称 SFC),例如:

<script>
export default {
  data() {
    return {
      count: 0
    }
  }
}
</script>
<template>
  <button @click="count++">You clicked me {{ count }} times.</button>
</template>

要使用一个子组件,我们需要在父组件中导入它。假设我们把计数器组件放在了一个叫做 ButtonCounter.vue 的文件中,这个组件将会以默认导出的形式被暴露给外部。例如:

<script>
import ButtonCounter from './ButtonCounter.vue'

export default {
  components: {
    ButtonCounter
  }
}
</script>

<template>
  <h1>Here is a child component!</h1>
  <ButtonCounter />
</template>

组件可以被重用任意多次,并且每个组件之中的内容都是独立,不会被影响。例如:

<h1>Here is a child component!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />

注册

一个 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)

全局注册的组件可以在此应用的任意组件的模板中使用:

<!-- 这在当前应用的任意组件中都可用 -->
<ComponentA/>
<ComponentB/>
<ComponentC/>

局部注册

全局注册虽然很方便,但有以下几个问题:

  1. 全局注册,但并没有被使用的组件无法在生产打包时被自动移除 (也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 JS 文件中。
  2. 全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性。

相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好。

局部注册需要使用 components 选项:

<script>
import ComponentA from './ComponentA.vue'
export default {
  components: {
    ComponentA
  }
}
</script>
<template>
  <ComponentA />
</template>

对于每个 components 对象里的属性,它们的 key 名就是注册的组件名,而值就是相应组件的实现。

props

声明

首先什么是props?

Props 是一种特别的 attributes,我个人理解props就是一种用于从父组件向子组件传递数据的机制。具体来说,props 是子组件接收父组件传递的数据的一种方式。子组件通过 props 属性来声明自己接收哪些数据,并可以在其模板中使用这些数据。

props在使用时需要先被显式声明,这样vue才知道你用了哪些props。

首先定义props可以用props选项来定义,例如我想传给该组件一个info信息,那我就必须在该组件内声明一下:

export default {
  props: ['info'],
  created() {
    console.log(this.info)
  }
}

除此之外,还可以使用对象式声明的方式,例如,假设我们有一个博文组件 BlogPost.vue,父组件想要向这个博文组件传递标题、内容和作者信息。我们可以这样声明 props:

<!-- BlogPost.vue -->
<template>
  <div class="blog-post">
    <h2>{{ title }}</h2>
    <p>{{ content }}</p>
    <p>Written by: {{ author }}</p>
  </div>
</template>
<script>
export default {
  props: {
    title: {
      type: String,
      required: true
    },
    content: {
      type: String,
      default: 'No content available'
    },
    author: {
      type: String,
      default: 'Anonymous'
    }
  }
}
</script>

在这个例子中:

  • title 属性必须是一个字符串,并且是必需的。如果父组件未传递 title,Vue.js 将会发出警告。
  • content 属性是一个可选的字符串,默认值为 No content available。如果父组件未传递 content,则显示默认内容。
  • author 属性是一个可选的字符串,默认值为 Anonymous。如果父组件未传递 author,则显示作者为匿名。

父组件可以这样使用 BlogPost组件:

<!-- ParentComponent.vue -->
<template>
  <div>
    <BlogPost
      title="Vue.js Props Example"
      content="This is an example of using props in Vue.js."
      author="John Doe"
    />
  </div>
</template>
<script>
import BlogPost from './BlogPost.vue';
export default {
  components: {
    BlogPost
  }
}
</script>

上面的例子中props的值都是静态的,实际上也可以是动态的,可以使用v-bind或:来进行动态绑定,其次他还可以是多种数据类型(String,Number,Boolean,Array,Object),例如:

<BlogPost :title="post.title" />
<BlogPost :title="post.title + ' by ' + post.author.name" />
<BlogPost :likes="42" />
<BlogPost :is-published="false" />
<BlogPost :comment-ids="[234, 266, 273]" />
<BlogPost
  :author="{
    name: 'Veronica',
    company: 'Veridian Dynamics'
  }"
 />
<!-- 根据一个变量的值动态传入 -->
<BlogPost :author="post.author" />

一个对象内还可以绑定多个prop,例如

export default {
  data() {
    return {
      post: {
        id: 1,
        title: 'My Journey with Vue'
      }
    }
  }
}
<BlogPost v-bind="post" />
<!-- 等价于 -->
<BlogPost :id="post.id" :title="post.title" />

注意:所有的 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 默认都是可选的,除非声明了 required: true。
  • 除 Boolean外的未传递的可选 prop 将会有一个默认值 undefined。
  • Boolean 类型的未传递 prop 将被转换为 false。这可以通过为它设置 default 来更改——例如:设置为 default: undefined 将与非布尔类型的 prop 的行为保持一致。
  • 如果声明了 default 值,那么在 prop 的值被解析为 undefined 时,无论 prop 是未被传递还是显式指明的 undefined,都会改为 default 值。

当 prop 的校验失败后,Vue 会抛出一个控制台警告 (在开发模式下)。

校验选项中的 type 可以是下列这些原生构造函数:String、Number、Boolean、Array、Object、Date、Function、Symbol、Error

为了更贴近原生 boolean attributes 的行为,声明为 Boolean 类型的 props 有特别的类型转换规则。以带有如下声明的 组件为例

export default {
  props: {
    disabled: Boolean
  }
}

该组件可以被这样使用:

<!-- 等同于传入 :disabled="true" -->
<MyComponent disabled />
<!-- 等同于传入 :disabled="false" -->
<MyComponent />

当一个 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方法触发自定义事件,例如在一个按钮中

<button @click="$emit('要触发的事件')">Click Me</button>

e m i t ( ) 方法在组件实例上也同样以 t h i s . emit() 方法在组件实例上也同样以 this. emit()方法在组件实例上也同样以this.emit() 的形式可用:

export default {
  methods: {
    submit() {
      this.$emit('someEvent')
    }
  }
}

父组件可以通过 v-on (缩写为 @) 来监听事件:

同样,组件的事件监听器也支持 .once 修饰符,也就是修饰了只会触发一次:

<MyComponent @some-event="callback" />
<MyComponent @some-event.once="callback" />

事件参数

有时事件在触发时需要携带一些参数,举例来说,我们想要 组件来管理文本会缩放得多大。我们可以在按钮上绑定一个“increaseBy”事件,并传递一个额外的参数1。

<button @click="$emit('increaseBy', 1)">Increase by 1</button>

然后我们在父组件中监听事件,我们可以先简单写一个内联的箭头函数作为监听器,此函数会接收到事件附带的参数:

<MyButton @increase-by="(n) => count += n" />

这样在每次点击按钮后,count的值就会加一。

或者,也可以用一个组件方法来作为事件处理函数:

<MyButton @increase-by="increaseCount" />

再写个方法来接收传递的参数

methods: {
  increaseCount(n) {
    this.count += n
  }
}

v-model

v-model 可以在组件上使用以实现双向绑定

例如现有组件,其界面为让输入框内的文字和参数modelValue保持一致,输入框内更新,参数值也会更新。

<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
}
</script>
<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

那么在使用该组件时就可以用v-model来和组件中的值进行绑定,例如:

<script>
import CustomInput from './CustomInput.vue'
export default {
  components: { CustomInput },
  data() {
    return {
      message: '初始值'
    }
  }
}
</script>
<template>
  <CustomInput v-model="message" /> {{ message }}
</template>

这样message的值就和输入框中的值就行了绑定,最后呈现的效果为在输入框后面的文字回合输入框中保持一致。
请添加图片描述

此外,v-model还可以接收一个参数。例如现在有一个标题参数title

<MyComponent v-model:title="bookTitle" />

子组件中使用 title prop 和 update:title 事件来更新父组件的值,而非默认的 modelValue prop 和 update:modelValue事件:。例如:

<script>
export default {
  props: ['title'],
  emits: ['update:title']
}
</script>
<template>
  <input
    type="text"
    :value="title"
    @input="$emit('update:title', $event.target.value)"
  />
</template>

上面的都是一个v-model的,一个组件上还可以绑定多个有参数的v-model

例如:

<UserName v-model:first-name="first"  v-model:last-name="last"/>
<script>
export default {
  props: {
    firstName: String,
    lastName: String
  },
  emits: ['update:firstName', 'update:lastName']
}
</script>
<template>
  <input
    type="text"
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    type="text"
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>

v-model 有一些内置的修饰符,例如 .trim,.number 和 .lazy。在组件中还可以自定义一些修饰符。

例如:创建一个自定义的修饰符 capitalize,它会自动将 v-model 绑定输入的字符串值第一个字母转为大写:

父组件中代码如下

<MyComponent v-model.capitalize="myText" />

子组件:每次输入更新时都会触发emitValue方法,方法中会将第一个单词变为大写

<script>
export default {
  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)
    }
  }
}
</script>
<template>
  <input type="text" :value="modelValue" @input="emitValue" />
</template>

另外,在使用修饰符的同时加上参数也是可以的

例如:

<UserName
  v-model:first-name.capitalize="first"
  v-model:last-name.uppercase="last"
/>
<script>
export default {
  props: {
    firstName: String,
    lastName: String,
    firstNameModifiers: {
      default: () => ({})
    },
    lastNameModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:firstName', 'update:lastName'],
  created() {
    console.log(this.firstNameModifiers) // { capitalize: true }
    console.log(this.lastNameModifiers) // { uppercase: true }
  }
}
</script>

透传Attributes

“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props 或emits的 attribute 或者 v-on 事件监听器。最常见的例子就是 class、style 和id 。

class继承

例如现有模板MyButton中一个按钮:

<button>我是按钮</button>

在父组件中使用时加上了class样式,如:

<MyButton class="large" />

最后渲染出的结果就是

<button class="large">我是按钮</button>

父组件中的样式将会传递给模板中按钮,倘若子组件中已有class,则会把样式进行合并。

例如子组件中有按钮:

<button class="small">我是按钮</button>

最后的结果就会变为

<button class="small large">我是按钮</button>

v-on继承

这样的规则也适用于v-on事件监听器中,如果父组件中有个点击事件,那么模板中的元素被点击时也会触发父组件的事件,同样,如果子组件中已有事件,那么点击后子组件的事件和父组件的事件都会被触发。

深层组件继承

如果子组件中又使用了另一个子组件,例如组件中根节点,那么在使用

时传递的attribute会直接传递给

禁用继承

这种继承关系有时可能我们不想让它生效,此时可以自己在组件选项中设置inheritAttrs:false

例如:

<template>
  <div>
    <!-- 组件内容 -->
  </div>
</template>
<script>
export default {
  inheritAttrs: false, // 禁止继承父组件的属性到根元素
  // 其他组件选项
};
</script>

多根节点的Attribute继承

如果一个组件中有多个根节点,vue并不会自动透传所有的根节点,此时需要使用$attrs进行显示绑定,否则vue不知道要把attribute透传到哪里。例如在组件中,这样父组件中的attribute就会精准透传到中

<name v-bind:"$attrs">...</name>
<age>..</age>
<weight>...</weight>

js中访问attribute

在js中可以在

例如:

<script setup>
import { useAttrs } from 'vue'
const attrs  userAttrs()
</script>

插槽Slots

插槽内容

插槽可以让接收模板内容,为子组件传递一些模块片段,并让子组件渲染这些片段。

例如当前有一个组件如下:

<template>
  <button class="fancy-btn">
  	<slot/>
	</button>
</template>

<style>
.fancy-btn {
  color: #fff;
  background: linear-gradient(315deg, #42d392 25%, #647eff);
  border: none;
  padding: 5px 10px;
  margin: 5px;
  border-radius: 8px;
  cursor: pointer;
}
</style>

在使用时如下:

<template>
  <FancyButton>
    请点击我 <!-- slot content -->
 	</FancyButton>
</template>

最终渲染如下
请添加图片描述
使用时中的“请点击我”就会替换掉,实现如上效果,插槽中的内容可以是任意合法的模板内容,并且插槽内容可以访问到父组件的数据作用域,因此可以传入多个元素或者是组件,例如有一个点击后数值加一的按钮,还有个组件,内容很简单只有一个❤如下:


<template>❤️</template>

在使用时就可以把它也传入:

<script>
import FancyButton from './FancyButton.vue'
import AwesomeIcon from './AwesomeIcon.vue'  
export default {
  components: { FancyButton, AwesomeIcon },
    data() {
       return {
          count: 1
       }
    }
}
</script>
<template>
   <span >{{ count }}</span>
   <FancyButton @click="count++">点一下加一,当前为:{{ count }}</FancyButton>
  <FancyButton>
    <span style="color:cyan">点我! </span>
    <AwesomeIcon />
  </FancyButton>
</template>

最终效果如下:
请添加图片描述
在外部没有提供任何内容的情况下,可以为插槽指定默认内容,比如有这样一个 组件:

<template>
  <button type="submit">
	  <slot>
    	默认内容
  	</slot>
	</button>
</template>

在使用时如果不传入内容就会默认展示默认内容,有新内容则展示新内容

最终效果如下:
请添加图片描述### 具名插槽

一个组件中还可以包含多个插槽出口,每一个元素可以有一个特殊的 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 #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>

这样就做到了具体的替换。

最终渲染效果如下:

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

条件插槽

有时需要根据插槽是否存在来渲染某些内容,这时可以结合使用$slot属性与v-if来实现。

在下面的示例中,我们定义了一个卡片组件,它拥有三个条件插槽:header、footer 和 default。 当 header、footer 或 default 存在时,我们希望包装它们以提供额外的样式,也就是会将div中符合v-if的提供额外的样式。

<template>
  <div class="card">
    <div v-if="$slots.header" class="card-header">
      <slot name="header" />
    </div>
    <div v-if="$slots.default" class="card-content">
      <slot />
    </div>
    <div v-if="$slots.footer" class="card-footer">
      <slot name="footer" />
    </div>
  </div>
</template>

动态插槽名

动态指令参数在 v-slot 上也是有效的,即可以定义下面这样的动态插槽名:

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

  <!-- 缩写为 -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

作用域插槽

如果想要同时使用父组件域内和子组件域内的数据。要做到这一点,我们可以像对组件传递 props 那样,向一个插槽的出口上传递 attributes:

<div>
  <slot :text="欢迎光临" :count="1"></slot>
</div>

当需要接收插槽 props 时,默认插槽和具名插槽的使用方式有一些小区别。下面通过子组件标签上的 v-slot 指令,直接接收到了一个插槽 props 对象:

<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

这样在子组件域内就拿到了父组件中的text和count
请添加图片描述
具名作用域插槽

具名作用域插槽的工作方式也是类似的,插槽 props 可以作为 v-slot 指令的值被访问到:v-slot:name=“slotProps”。当使用缩写时是这样:

<MyComponent>
  <template #header="headerProps">
    {{ headerProps }}
  </template>
</MyComponent>

向具名插槽中传入 props:

<slot name="header" message="hello"></slot>

这里要特别注意一点:
插槽上的 name 是一个 Vue 特别保留的 attribute,不会作为 props 传递给插槽。因此最终 headerProps 的结果是 { message: ‘hello’ }。

如果你同时使用了具名插槽与默认插槽,则需要为默认插槽使用显式的 标签。尝试直接为组件添加 v-slot 指令将导致编译错误。这是为了避免因默认插槽的 props 的作用域而困惑。举例:

<div>
  <slot :message="hello"></slot>
  <slot name="footer" />
</div>
<!-- 该模板无法编译 -->
<MyComponent v-slot="{ message }">
  <p>{{ message }}</p>
  <template #footer>
    <!-- message 属于默认插槽,此处不可用 -->
    <p>{{ message }}</p>
  </template>
</MyComponent>

为默认插槽使用显式的 <template> 标签有助于更清晰地指出 message 属性在其他插槽中不可用:

<MyComponent>
  <!-- 使用显式的默认插槽 -->
  <template #default="{ message }">
    <p>{{ message }}</p>
  </template>
  <template #footer>
    <p>Here's some contact info</p>
  </template>
</MyComponent>

高级列表组件示例

下面展示一个作用域插槽的应用场景,来看一个 `组件的例子。它会渲染一个列表,并同时会封装一些加载远端数据的逻辑、使用数据进行列表渲染、或者是像分页或无限滚动这样更进阶的功能。然而我们希望它能够保留足够的灵活性,将对单个列表元素内容和样式的控制权留给使用它的父组件。

<template>
  <FancyList api-url="url" :per-page="10">
    <template #item="{ body, username, likes }">
      <div class="item">
        <p>{{ body }}</p>
        <p class="meta">by {{ username }} | {{ likes }} likes</p>
      </div>
    </template>
  </FancyList>
</template>
<script>
export default {
  props: ['api-url', 'per-page'],
  data() {
    return {
      items: []
    }
  },
  mounted() {
    setTimeout(() => {
      this.items = [
        { body: 'Scoped Slots Guide', username: 'Evan You', likes: 20 },
        { body: 'Vue Tutorial', username: 'Natalia Tepluhina', likes: 10 }
      ]
    }, 1000)
  }
}
</script>

<template>
  <ul>
    <li v-if="!items.length">
      Loading...
    </li>
    <li v-for="item in items">
      <slot name="item" v-bind="item"/>
    </li>
  </ul>
</template>

最终效果如下:
请添加图片描述

依赖注入

通常情况下,当我们需要从父组件向子组件传递数据时,会使用 props。想象一下这样的结构:有一些多层级嵌套的组件,形成了一棵巨大的组件树,而某个深层的子组件需要一个较远的祖先组件中的部分数据。在这种情况下,如果仅使用 props 则必须将其沿着组件链逐级传递下去,这会非常麻烦。
请添加图片描述provide 和inject 可以帮助我们解决这一问题[1]。一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入中父组件提供给整条链路的依赖。

请添加图片描述

provide

可以使用provide()为组件后代提供数据,例如

<script setup>
import { provide } from 'vue'
provide('注入名',)
<script>

import { provide } from 'vue'
export default {
   setup() {
      provide('注入名',)
   }
}

provide()会接收两个参数,第一个是注入名,可以是字符串或是一个Symbol。第二个参数就是要提供的值,可以是任意类型的数据。

除了在一个组件中提供依赖,我们还可以在整个应用层面提供依赖,例如

import { createApp } from 'vue'
const app = createApp({})
app.provide('message',1)

在应用级别提供的数据在该应用内的所有组件中都可以注入。

Inject

要注入上层组件提供的数据,需使用inject()函数,例如:

<script setup>
import {inject } from 'vue'
const message = inject('message')
</script>

如果提供的值是一个 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')
))

或者在父组件中直接定义

<script setup>
import { defineAsyncComponent } from 'vue'
const AdminPage = defineAsyncComponent(() =>import('./components/AdminPageComponent.vue')
)
</script>
<template>
  <AdminPage />
</template>

加载和错误

异步操作不可避免地会涉及到加载和错误状态,因此 defineAsynccomponent()也支持在高级选项中处理这些状态,例如:

const AsyncComp = defineAsyncComponent({
  // 加载函数
  loader: () => import('./Foo.vue'),
  // 加载异步组件时使用的组件
  loadingComponent: LoadingComponent,
  // 展示加载组件前的延迟时间,默认为 200ms
  delay: 200,
  // 加载失败后展示的组件
  errorComponent: ErrorComponent,
  // 如果提供了一个 timeout 时间限制,并超时了
  // 也会显示这里配置的报错组件,默认值是:Infinity
  timeout: 3000
})

内置组件

component

是一个抽象的组件,用于动态地渲染不同的组件或元素。

通过绑定 is 属性可以实现动态组件的切换和渲染。

<component :is="currentComponent"></component>

transition

和 提供了在 Vue.js 中实现过渡和动画效果的功能。

通过定义过渡的 CSS 类名,可以控制元素在进入或离开 DOM 时的动画效果。

当一个 组件中的元素被插入或移除时,会发生下面这些事情:

  1. Vue 会自动检测目标元素是否应用了 CSS 过渡或动画。如果是,则一些 CSS 过渡 class会在适当的时机被添加和移除。
  2. 如果有作为监听器的 JavaScript 钩子,这些钩子函数会在适当时机被调用。
  3. 如果没有探测到 CSS 过渡或动画、也没有提供 JavaScript 钩子,那么 DOM 的插入、删除操作将在浏览器的下一个动画帧后执行。

使用例子:最后的效果为点击一次按钮后,下面的文字会出现或者消失。实现的方法为定义好元素进入时和消失时的状态,在写好过渡时的效果。

<script setup>
import { ref } from 'vue'

const show = ref(true)
</script>
<template>
  <button @click="show = !show">按钮</button>
  <Transition>
    <p v-if="show">我的名字</p>
  </Transition>
</template>
<style>
.v-enter-active,
.v-leave-active {
  transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
  opacity: 0;
}
</style>

过程如下:
请添加图片描述

  1. v-enter-from:进入动画的起始状态。在元素插入之前添加,在元素插入完成后的下一帧移除。
  2. v-enter-active:进入动画的生效状态。应用于整个进入动画阶段。在元素被插入之前添加,在过渡或动画完成之后移除。这个 class 可以被用来定义进入动画的持续时间、延迟与速度曲线类型。
  3. v-enter-to:进入动画的结束状态。在元素插入完成后的下一帧被添加 (也就是 v-enter-from 被移除的同时),在过渡或动画完成之后移除。
  4. v-leave-from:离开动画的起始状态。在离开过渡效果被触发时立即添加,在一帧后被移除。
  5. v-leave-active:离开动画的生效状态。应用于整个离开动画阶段。在离开过渡效果被触发时立即添加,在过渡或动画完成之后移除。这个 class 可以被用来定义离开动画的持续时间、延迟与速度曲线类型。
  6. v-leave-to:离开动画的结束状态。在一个离开动画被触发后的下一帧被添加 (也就是 v-leave-from 被移除的同时),在过渡或动画完成之后移除。

keep-alive

是一个抽象组件,用于保持组件状态或避免多次渲染。

当组件被 包裹时,其状态将会被缓存,而不是每次切换时重新渲染。

例如:下方引用了两个组件A和B

<script setup>
import { shallowRef } from 'vue'
import CompA from './CompA.vue'
import CompB from './CompB.vue'
const current = shallowRef(CompA)
</script>
<template>
  <div class="demo">
    <label><input type="radio" v-model="current" :value="CompA" /> A</label>
    <label><input type="radio" v-model="current" :value="CompB" /> B</label>
    <KeepAlive>
      <component :is="current"></component>
    </KeepAlive>
  </div>
</template>

页面中渲染效果如下:点击上面的选项会切换下面的内容,不过由于组件使用了KeepAlive,因此每次切换时不会重复渲染,会保留之前的值。

请添加图片描述
请添加图片描述

teleport

允许你将 DOM 元素渲染到应用的任何地方,而不受当前 DOM 结构的限制。这在需要在应用中动态移动元素时非常有用,例如在模态框中渲染弹出内容。

<button @click="open = true">Open Modal</button>
<Teleport to="body">
  <div v-if="open" class="modal">
    <p>Hello from the modal!</p>
    <button @click="open = false">Close</button>
  </div>
</Teleport>

Suspense

是 Vue.js 3.x 中新增的组件,用于处理异步组件的加载和状态。它可以在异步组件加载完成之前显示占位内容,并处理加载状态和错误。例如:

<Suspense>
  <template #default>
    <AsyncComponent />
  </template>
  <template #fallback>
    <div>Loading...</div>
  </template>
</Suspense>
  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值