Vue3 学习笔记--深入组件

1、组件注册

1.1 全局注册

Vue.createApp({...}).component('my-component-name', {
  // ... 选项 ...
})

或者像下面这样写

const app = Vue.createApp({})

app.component('component-a', {
  /* ... */
})
app.component('component-b', {
  /* ... */
})

1.2 局部注册

通过一个普通的 JavaScript 对象来定义组件

const ComponentA = {
  /* ... */
}
const ComponentB = {
  /* ... */
}

然后在 components 选项中定义你想要使用的组件:

const app = Vue.createApp({
  components: {
    'component-a': ComponentA, // component-a 为对应的组件名
    'component-b': ComponentB
  }
})

局部注册的组件在其子组件中不可用

例如,上面的组件 ComponentAComponentB 都是在 app 中局部注册的

但在 ComponentB 中是无法使用 ComponentA 的

除非你这样写:

const ComponentA = {
  /* ... */
}

const ComponentB = {
  components: {
    'component-a': ComponentA
  }
  // ...
}

ComponentB 中注册 ComponentA 之后,才能使用 ComponentA


2、父子组件通信

父子组件通过两种不同的方式来相互通信

  • 父传子:props
  • 子传父:$emit

先来讲一下父传子,也就是 props

2.1 Props

Prop 是你可以在组件上注册的一些自定义 attribute

Props 有两种常见的用法:

  • 数组
  • 对象

直接用代码来解释用法

Vue.component('blog-post', {
  props: ['title'],
  template: '<h3>{{ title }}</h3>'
})

在上述代码中,我们局部注册了一个组件 blog-post

并且在 props 中自定义了一个 attribute,并且在 template 中使用了 title

<blog-post title="My journey with Vue"></blog-post>
<blog-post title="Blogging with Vue"></blog-post>
<blog-post title="Why Vue is so fun"></blog-post>

在父组件中使用 blog-post 组件,并且给 title 赋值

这样就实现了父组件给子组件传值

页面效果如下:

2.1.1 Props 的数组用法 

// 简单语法
app.component('props-demo-simple', {
  props: ['size', 'myMessage']
})

2.1.2 Props 的对象用法

type 的类型可以是下面这些:

补充:对象类型的其它写法

app.component('my-component', {
  props: {
    // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
    propA: Number,
    // 多个可能的类型
    propB: [String, Number],
    // 必填的字符串
    propC: {
      type: String,
      required: true
    },
    // 带有默认值的数字
    propD: {
      type: Number,
      default: 100
    },
    // 带有默认值的对象
    propE: {
      type: Object,
      // 对象或数组默认值必须从一个工厂函数获取
      default() {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator(value) {
        // 这个值必须匹配下列字符串中的一个
        return ['success', 'warning', 'danger'].includes(value)
      }
    },
    // 具有默认值的函数
    propG: {
      type: Function,
      // 与对象或数组默认值不同,这不是一个工厂函数 —— 这是一个用作默认值的函数
      default() {
        return 'Default function'
      }
    }
  }
})

注意:使用 DOM 中的模板时,驼峰命名的 prop 名需要使用其等价的短横线分隔符命名来替换

2.2 非 Prop 的 Attribute

一个非 propattribute 是指传向一个组件

但是该组件并没有相应 propsemits 定义的 attribute

2.2.1 Attribute 继承

2.2.2 禁用 Attribute 继承

如果我们不希望根元素继承 attribute

可以在组件的选项中设置

inheritAttrs: false

如下所示:

2.2.3 多个根节点上的 Attribute 继承

2.3 监听子组件事件

首先,我们需要 在子组件中定义好某些情况下触发的事件名称

其次,在父组件中以 v-on 的方式传入要监听的事件名称,并绑定对应的方法

最后,在子组件中发生某个事件的时候,根据事件名触发对应的事件

先看一个计数器的例子,如下图所示:

大致过程就是:

2.3.1 验证抛出的事件

const app = createApp({})

// 数组语法
app.component('todo-item', {
  emits: ['check'],
  created() {
    this.$emit('check')
  }
})

// 对象语法
app.component('reply-form', {
  emits: {
    // 没有验证函数
    click: null,

    // 带有验证函数
    submit: payload => {
      if (payload.email && payload.password) {
        return true
      } else {
        console.warn(`Invalid submit event payload!`)
        return false
      }
    }
  }
})

3、非父子组件通信

使用 provide 和 inject

无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者

这个特性有两个部分:

  1. 父组件有一个 provide 选项来提供数据
  2. 子组件有一个 inject 选项来开始使用这些数据

还是继续用前面那个例子进行讲解

provide 组件实例的 property

但如果 todos 中的内容发生了变化,也就是 todos.length 改变时,不会反映在 inject 中

3.1 处理响应性

全局事件总库 mitt 库 ?


4、插槽

4.1 基本使用

4.2 默认内容

使用插槽时,如果没有提供内容,则会显示插槽默认的内容

如果我们提供内容,则这个提供的内容将会被渲染从而取代默认内容

4.3 具名插槽

4.3.1 具名插槽的缩写

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>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

4.4 动态插槽名

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

4.5 渲染作用域

规则只有一条:

父级模板里的所有内容都是在父级作用域中编译的

子模板里的所有内容都是在子作用域中编译的 

4.6 作用域插槽

有时让插槽内容能够访问子组件中才有的数据是很有用的

下面我们来看一个例子,组件 todo-list 中包含一个 items

我们将其动态的绑定到了 slot 上面

app.component('todo-list', {
  data() {
    return {
      items: ['Feed a cat', 'Buy milk']
    }
  },
  template: `
    <ul>
      <li v-for="( item, index ) in items">
      <slot :item="item"></slot>
  </li>
</ul>
  `
})

我们在父组件中使用了 todo-list 组件,要想在父组件中使用 slot 中绑定的 attribute

我们需要先包裹一层 template,然后把之前绑定的 attribute 传过来

也就是给 v-slot 赋一个 slotProps (绑定在 slot 元素上的 attribute 被称为 插槽 prop

最后通过 slotProps 获取传过来的值

<todo-list>
  <template v-slot:default="slotProps">
    <span>{{ slotProps.item }}</span>
  </template>
</todo-list>

4.6.1 独占默认插槽的缩写语法

 在上述情况下,当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用

<todo-list v-slot:default="slotProps">
  <span>{{ slotProps.item }}</span>
</todo-list>

这种写法还可以更简单

当我们的插槽是默认插槽时,也就是说它没有名字,那就可以像这样写:

<todo-list v-slot="slotProps">
  <span>{{ slotProps.item }}</span>
</todo-list>

默认插槽的缩写语法不能和具名插槽混用

只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template> 的语法:

<todo-list>
  <template v-slot:default="slotProps">
    <span>{{ slotProps.item }}</span>
  </template>

  <template v-slot:other="otherSlotProps">
    ...
  </template>
</todo-list>

5、动态组件

上述内容可以通过 Vue 的 <component> 元素加一个特殊的 is Attribute 来实现:

<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component :is="currentTabComponent"></component>

注:也可以对动态组件传值和监听事件

5.1 keep-live

当在组件之间切换的时候,你有时会想保持这些组件的状态,以避免反复渲染导致的性能问题

用法如下:

<!-- 失活的组件将会被缓存!-->
<keep-alive>
  <component :is="currentTabComponent"></component>
</keep-alive>

keep-live 的属性:

  • include  只有名称匹配的组件会被缓存
  • exclude  任何名称匹配的组件都不会被缓存
  • max  最多可以缓存多少组件实例

注:对于已经被缓存的组件来说,再次切换回该组件时

       我们是不会执行 created 或者 mounted 等生命周期函数

       但是可以使用 activateddeactivated 这两个生命周期钩子函数来监听 

5.2 Webpack 的代码分包

默认情况下,在构建整个组件树的过程中,因为组件和组件之间是通过模块化直接依赖的

那么 webpack 在打包时就会将组件模块打包到一起(比如全部打包到 app.js 文件中)

随着项目的不断庞大,app.js 文件的内容过大,会造成 首屏的渲染速度变慢

所以,对于一些不需要立即使用的组件,我们可以对其进行拆分,单独打包

拆分成一个个 chunk.js

这些 chunk.js 会在需要的时候,从服务器上加载下来,并运行对应的代码

Vue 中也有类似的方法


6、异步组件

在大型应用中,我们可能需要将应用分割成小一些的代码块

并且只在需要的时候才从服务器加载一个模块

Vue 中有一个 defineAsyncComponent 方法可以实现上述需求

用法如下:

import { defineAsyncComponent } from 'vue' // 导入

const AsyncComp = defineAsyncComponent(() =>
  import('./components/AsyncComponent.vue')
)  // 实现异步加载

在局部注册组件时,也可以使用 defineAsyncComponent

import { createApp, defineAsyncComponent } from 'vue'

createApp({
  // ...
  components: {
    AsyncComponent: defineAsyncComponent(() =>
      import('./components/AsyncComponent.vue')
    )
  }
})

对于高阶用法,defineAsyncComponent 可以接受一个对象

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent({
  // 工厂函数
  loader: () => import('./Foo.vue')
  // 加载异步组件时要使用的组件
  loadingComponent: LoadingComponent,
  // 加载失败时要使用的组件
  errorComponent: ErrorComponent,
  // 在显示 loadingComponent 之前的延迟 | 默认值:200(单位 ms)
  delay: 200,
  // 如果提供了 timeout ,并且加载组件的时间超过了设定值,将显示错误组件
  // 默认值:Infinity(即永不超时,单位 ms)
  timeout: 3000,
  // 定义组件是否可挂起 | 默认值:true
  suspensible: false,
  /**
   *
   * @param {*} error 错误信息对象
   * @param {*} retry 一个函数,用于指示当 promise 加载器 reject 时,加载器是否应该重试
   * @param {*} fail  一个函数,指示加载程序结束退出
   * @param {*} attempts 允许的最大重试次数
   */
  onError(error, retry, fail, attempts) {
    if (error.message.match(/fetch/) && attempts <= 3) {
      // 请求发生错误时重试,最多可尝试 3 次
      retry()
    } else {
      // 注意,retry/fail 就像 promise 的 resolve/reject 一样:
      // 必须调用其中一个才能继续错误处理。
      fail()
    }
  }
})

6.1 Suspense

是一个实验性的新特性

提供两个插槽,fallback 为备用的


7、模板引用

尽管存在 prop 和事件,但有时我们可能仍然需要直接访问 JavaScript 中的子组件

这个时候,我们可以使用 $refs

同样,作为子组件,我们可以通过 $parent$root 访问父组件中的元素


8、生命周期

所有的生命周期钩子自动绑定 this 上下文到实例中

这也意味着我们 不能使用箭头函数来定义一个生命周期方法

下面来介绍一下什么是生命周期

每个组件都可能会经历从 创建、挂载、更新以及卸载 等一系列的过程      

在这个过程中的 某一个阶段,我们可能会想要 添加一些属于自己的代码逻辑

但是我们如果想要知道目前组件正处于哪一个阶段的话

需要借助 Vue 给我们提供的关于组件的 生命周期函数

生命周期函数

生命周期函数是一些钩子,在某个时间会被 Vue 源码内部进行回调

通过对生命周期函数的回调,我们可以知道目前组件正在经历什么阶段

那么我们就可以在该生命周期中编写属于自己的代码

生命周期图示:


9、组件的 v-model

默认情况下,组件上的 v-model 使用 modelValue 作为 prop

update:modelValue 作为事件

不太懂?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值