vue3 组件

185 阅读10分钟

全局组件

注册单个组件

// 方式一
app.component(
  // 注册的名字
  'MyComponent',
  // 组件的实现
  {
    /* ... */
  }
)
// 示例:
app.component('GlobalComp4', {
  props: ['name', 'msg'],
  setup(props) {
    const refCount = ref('a-ref')

    const getMessage = () => {
      return 'a message'
    }
    onMounted(() => {
      console.log('getMessage:', getMessage())
      console.log('component initial')
    })
    return () => h('div', {
      style: {
        color: 'red'
      },
      class: 'global-comp',
      'custom-name': props.name
    }, [
          refCount.value,
          h('div', slots?.default?.()),
          h('div', slots?.footer?.({
            text: props.msg
          }))
        ])
  },
})
// 如果一个渲染函数组件不需要任何实例状态,为了简洁起见,它们也可以直接被声明为一个函数:
function SayHi () {
  return 'Beautiful world!'
}
app.component('GlobalComp5', SayHi)
// 方式二:如果使用单文件组件,可以注册被导入的 `.vue` 文件:
import MyComponent from './App.vue'

app.component('MyComponent', MyComponent)

GlobalComp4在模板中的使用方式如下:

 <GlobalComp4 name="test" msg="组件内部值,slot访问测试">
    <!-- 方式一:default不可省 -->
    <template #default>
      这是默认插槽内容
    </template>
    <!-- 方式二:直接写内容,不需要template标签 -->
    <!-- 当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 <template> 节点都被隐式地视为默认插槽的内容 -->
    <!-- 这是默认插槽内容2 -->
    <template #footer="slotProps">
      <!-- 可看定义的插槽,插槽的输入key为text -->  
      这是具名footer插槽内容:{{ slotProps.text }}
    </template>
  </GlobalComp4>

批量注册组件

// 第一步:新建register.js文件,文件内容为:
import GlobalComp1 from './GlobalComp1.vue'
import GlobalComp2 from './GlobalComp2.vue'

const components = {
  GlobalComp1,
  GlobalComp2
}

export default {
  install(app) {
    // 注册全局组件
    for (const [key, component] of Object.entries(components)) {
      app.component(key, component)
    }
  }
}
// 第二步:在main.ts中引入上述文件,并注册全局组件
import registerGlobalComps from '../src/components/register.js'

app.use(registerGlobalComps).mount('#app')

注意:\color{red}{注意:} 使用app.component需在mount之前,否则无法注册成功

递归组件

一个单文件组件可以通过它的文件名被其自己所引用。例如:名为 FooBar.vue 的组件可以在其模板中用 <FooBar/> 引用它自己。

请注意这种方式相比于导入的组件优先级更低。如果有具名的导入和组件自身推导(指的就是文件名)的名字冲突了,可以为导入的组件添加别名

import { FooBar as FooBarChild } from './components'

递归组件示例(以下为简单的菜单示例):

父组件内容:\color{#A52A2A}{父组件内容:}

<script setup lang="ts">
import Menu from './components/Menu.vue'
// 父组件配置数据,传递给子组件
const menuList = reactive([{
    id: '1',
    name: 'chapter1',
    checked: false,
    children: [{
      id: '1-1',
      name: 'chapter1-1',
      checked: true,
    }, {
      id: '1-2',
      name: 'chapter1-2',
      checked: true,
      children: [{
        id: '1-2-1',
        name: 'chapter1-2-1',
        checked: false,
      }]
    }]
  }, {
    id: '2',
    name: 'chapter2',
    checked: false,
  }, {
    id: '3',
    name: 'chapter3',
    checked: false,
  }])

<!-- 父组件引入子组件模板 -->
<Menu :list="menuList"/>

Menu.vue文件:\color{#A52A2A}{Menu.vue文件:}

<template>
  <div>
    <div v-for="item in props.list" :key="item.id">
      <label @click="onClickItem(item)"><input v-model="item.checked" type="checkbox">{{ item.name }}</label>
      <MenuTree
        v-if="item?.children?.length"
        class="child-menu"
        :list="item.children"
      />
    </div>
  </div>
</template>
<script setup lang="ts">
  const props = defineProps(['list'])
  const onClickItem = item => {
    console.log('item:', item)
  }

  // 定义该组件的组件名,如果没有此代码,在递归组件中需使用该组件的文件名
  defineOptions({
    name: 'MenuTree'
  })
</script>


<style scoped>
.child-menu {
  margin-left: 20px;
}
</style>

image.png

动态组件

由于组件是通过变量引用而不是基于字符串组件名注册的,在 <script setup> 中要使用动态组件的时候,应该使用动态的 :is 来绑定:

<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>

<template>
  <component :is="Foo" />
  <component :is="someCondition ? Foo : Bar" />
</template>

注意事项\color{red}{注意事项}

1.在Vue2 的时候is 是通过组件名称切换的 在Vue3 setup 是通过组件实例切换的

2.如果你把组件实例放到Reactive Vue会给你一个警告runtime-core.esm-bundler.js:38 [Vue warn]: Vue received a Component which was made a reactive object. This can lead to unnecessary performance overhead, and should be avoided by marking the component with markRaw or using shallowRef instead of ref. Component that was made reactive:

这是因为reactive 会进行proxy 代理 而我们组件代理之后毫无用处 节省性能开销 推荐我们使用shallowRef 或者 markRaw 跳过proxy 代理}

动态组件示例:实现一个切换tab的功能

<div
    v-for="(item, index) in tabs"
    style="display: inline-block; padding: 5px; cursor: pointer; border: 1px solid #ccc;"
    :style="{background: index === activeIndex ? '#E9967A' : '#fff'}"
    @click="changeTab(item, index)"
  >
    <div>{{ item.name }}</div>
  </div>
  <component :is="activeTab"></component>

使用markRaw的方法跳过代理:

// 方法一:
const tabs = [{
  name: '组件A',
  comp: markRaw(CompA)
}, {
  name: '组件B',
  comp: markRaw(CompB)
}]

let activeTab = shallowRef(CompA)

let activeIndex = ref(0)

const changeTab = (item, index) => {
  console.log('点击tab:', item, '--index:', index)
  activeTab = item.comp

  activeIndex.value = index
}

image.png

// 方法二:
<script lang="ts">
export default {
  components: {
    CompA,
    CompB
  }
}
</script>

<script setup lang="ts">
import CompA from './components/CompA.vue';
import CompB from './components/CompB.vue'
import { ref } from 'vue'

const tabs = [{
  name: '组件A',
  comp: 'CompA'
}, {
  name: '组件B',
  comp: 'CompB'
}]

let activeTab = 'CompA'
let activeIndex = ref(0)

const changeTab = (item, index) => {
  console.log('点击tab:', item, '--index:', index)
  activeTab = item.comp

  activeIndex.value = index
}
</script>

image.png

异步组件

defineAsyncComponent

基本使用

ES 模块动态导入也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent 搭配使用。类似 Vite 和 Webpack 这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),因此我们也可以用它来导入 Vue 单文件组件。

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
)
  • 全局注册:
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>
  • 加载与错误态
const AsyncComp = defineAsyncComponent({
  // 加载函数
  loader: () => import('./Foo.vue'),

  // 加载异步组件时使用的组件
  loadingComponent: LoadingComponent,
  // 展示加载组件前的延迟时间,默认为 200ms
  delay: 200,

  // 加载失败后展示的组件
  errorComponent: ErrorComponent,
  // 如果提供了一个 timeout 时间限制,并超时了
  // 也会显示这里配置的报错组件,默认值是:Infinity
  timeout: 3000
})

使用场景

懒加载组件,也就是说只有在页面需要渲染该组件的时候才从服务端获取。这种方式可以减少首屏的chunk体积,帮助我们提升首页的加载速度。

Suspense

<Suspense> 是一项实验性功能。它不一定会最终成为稳定功能,并且在稳定之前相关 API 也可能会发生变化。

<Suspense> 是一个内置组件,用来在组件树中协调对异步依赖的处理。它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。

示例

<Suspense>
└─ <Dashboard>
   ├─ <Profile>
   │  └─ <FriendStatus>(组件有异步的 setup())
   └─ <Content>
      ├─ <ActivityFeed> (异步组件)
      └─ <Stats>(异步组件)

有了 <Suspense> 组件后,我们就可以在等待整个多层级组件树中的各个异步依赖获取结果时,在顶层展示出加载中或加载失败的状态。

异步依赖的种类

<Suspense> 可以等待的异步依赖有两种:

  1. 带有异步 setup() 钩子的组件。这也包含了使用 <script setup> 时有顶层 await 表达式的组件。
// 带有异步setup钩子的组件
<script lang="ts">
const getArticleInfo = async () => {
  // wait 3 seconds to mimic API call
  await new Promise((resolve) => setTimeout(resolve, 1000))
  const article = {
    title: 'My Vue 3 Article',
    author: 'Matt Maribojoc',
  }
  return article
}

export default {
  async setup() {

    const article = await getArticleInfo()
    console.log(article)
    return {
      article,
    }
  }
}
</script>
// 带有顶层await表达式的组件
<script setup lang="ts">
const getArticleInfo = async () => {
  // wait 3 seconds to mimic API call
  await new Promise((resolve) => setTimeout(resolve, 1000))
  const article = {
    title: 'My Vue 3 Article1',
    author: 'Matt Maribojoc',
  }
  return article
}


const article = await getArticleInfo()

</script>

带有异步 setup() 钩子的组件必须被包裹在Suspense中,否则会报错,即使使用defineAsyncComponent来引入,也不行

  1. 异步组件

异步组件默认就是 “suspensible” 的。这意味着如果组件关系链上有一个 <Suspense>,那么这个异步组件就会被当作这个 <Suspense> 的一个异步依赖。在这种情况下,加载状态是由 <Suspense> 控制,而该组件自己的加载、报错、延时和超时等选项都将被忽略。

异步组件也可以通过在选项中指定 suspensible: false 表明不用 Suspense 控制,并让组件始终自己控制其加载状态。(异步组件可以自己内部控制错误和加载态)

加载中状态

<Suspense> 组件有两个插槽:#default 和 #fallback。两个插槽都只允许一个直接子节点 。在可能的时候都将显示默认槽中的节点。否则将显示后备槽中的节点。

<Suspense>
  <!-- 具有深层异步依赖的组件 -->
  <Dashboard />

  <!-- 在 #fallback 插槽中显示 “正在加载中” -->
  <template #fallback>
    Loading...
  </template>
</Suspense>

事件

<Suspense> 组件会触发三个事件:pendingresolve 和 fallbackpending 事件是在进入挂起状态时触发。resolve 事件是在 default 插槽完成获取新内容时触发。fallback 事件则是在 fallback 插槽的内容显示时触发。

例如,可以使用这些事件在加载新组件时在之前的 DOM 最上层显示一个加载指示器。

函数式组件

内置组件

Teleport

Teleport 是一种能够将我们的模板渲染至指定DOM节点,不受父级stylev-show等属性影响,但dataprop数据依旧能够共用的技术.

主要解决的问题 因为Teleport节点挂载在其他指定的DOM节点下,完全不受父级style样式影响。

<Teleport> 只改变了渲染的 DOM 结构,它不会影响组件间的逻辑关系。也就是说,如果 <Teleport> 包含了一个组件,那么该组件始终和这个使用了 <teleport> 的组件保持逻辑上的父子关系。传入的 props 和触发的事件也会照常工作。

这也意味着来自父组件的注入也会按预期工作,子组件将在 Vue Devtools 中嵌套在父级组件下面,而不是放在实际内容移动到的地方。

基本用法

props

interface TeleportProps {
  /**
   * 必填项。指定目标容器。
   * 可以是选择器或实际元素。
   */
  to: string | HTMLElement
  /**
   * 当值为 `true` 时,内容将保留在其原始位置
   * 而不是移动到目标容器中。
   * 可以动态更改。
   */
  disabled?: boolean
}

指定目标容器:

<Teleport to="#some-id" />
<Teleport to=".some-class" />
<Teleport to="[data-teleport]" />

有条件地禁用:

<Teleport to="#popup" :disabled="displayVideoInline">
  <video src="./my-movie.mp4">
</Teleport>

KeepAlive

它的功能是在多个组件间动态切换时缓存被移除的组件实例,也就是保留组件切换前的状态。

props

1. include/exclude

<KeepAlive> 默认会缓存内部的所有组件实例,但我们可以通过 include 和 exclude prop 来定制该行为。这两个 prop 的值都可以是一个以英文逗号分隔的字符串、一个正则表达式,或是包含这两种类型的一个数组:

<!-- 以英文逗号分隔的字符串 -->
<KeepAlive include="a,b">
  <component :is="view" />
</KeepAlive>

<!-- 正则表达式 (需使用 `v-bind`) -->
<KeepAlive :include="/a|b/">
  <component :is="view" />
</KeepAlive>

<!-- 数组 (需使用 `v-bind`) -->
<KeepAlive :include="['a', 'b']">
  <component :is="view" />
</KeepAlive>

2. max:最大缓存实例数

我们可以通过传入 max prop 来限制可被缓存的最大组件实例数。<KeepAlive> 的行为在指定了 max 后类似一个 LRU 缓存:如果缓存的实例数量即将超过指定的那个最大数量,则最久没有被访问的缓存实例将被销毁,以便为新的实例腾出空间。

<KeepAlive :max="10">
  <component :is="activeComponent" />
</KeepAlive>

被缓存实例 的生命周期

当一个组件实例从 DOM 上移除但因为被 <KeepAlive> 缓存而仍作为组件树的一部分时,它将变为不活跃状态而不是被卸载。当一个组件实例作为缓存树的一部分插入到 DOM 中时,它将重新被激活

一个持续存在的组件可以通过 onActivated() 和 onDeactivated() 注册相应的两个状态的生命周期钩子:

<script setup>
import { onActivated, onDeactivated } from 'vue'

onActivated(() => {
  // 调用时机为首次挂载
  // 以及每次从缓存中被重新插入时
})

onDeactivated(() => {
  // 在从 DOM 上移除、进入缓存
  // 以及组件卸载时调用
})
</script>
  • onActivated 在组件挂载时也会调用,并且 onDeactivated 在组件卸载时也会调用。
  • 这两个钩子不仅适用于 <KeepAlive> 缓存的根组件,也适用于缓存树中的后代组件。

被keep-alive包裹的子组件的生命周期变化:

  • 初次进入时: onMounted> onActivated
  • 退出后触发 deactivated
  • 再次进入:
  • 只会触发 onActivated
  • 事件挂载的方法等,只执行一次的放在 onMounted中;组件每次进去执行的方法放在 onActivated中

示例:

<KeepAlive v-if="showComp">
    <component :is="activeComponent"></component>
  </KeepAlive>
  <button @click="changeComp">切换组件</button>
  <button @click="unMountComp">卸载组件</button>
<script setup lang="ts">
import CompB from './components/CompB.vue'
import CompD from './components/CompD.vue'

let activeComponent = shallowRef(CompB)
let count = 0
const changeComp = () => {
  count++
  activeComponent.value = count % 2 === 0 ? CompB : CompD
}

let showComp = ref(true)
const unMountComp = () => {
  showComp.value = false
}
<!-- 组件B模板 -->
<template>
  <div>
    组件B
  </div>
</template>
// 组件B js
<script setup lang="ts">
import { onActivated, onBeforeMount, onMounted, onDeactivated, onUnmounted } from 'vue'

// 首次渲染触发
  onBeforeMount(() => {
    console.log('onBeforeMount');
  })

// 首次渲染触发
  onMounted(() => {
    console.log('onMounted');
  })

// 首次渲染以及进入活跃状态触发
  onActivated(() => {
    console.log('keep-alive onActivated')
  })

// 进入缓存状态触发  
// 组件卸载触发
  onDeactivated(() => {
    console.log('keep-alive 组件进入deactive状态');
  })

// 组件卸载触发
  onUnmounted(() => {
    console.log('组件卸载')
  })
</script>
  • 首次渲染时,生命周期钩子触发顺序:onMounted > onActivated
  • 组件卸载时,生命周期钩子触发顺序:onDeactivated > onUnmounted

在有子组件的情况下:

  • 首次渲染,子组件与父组件生命周期钩子触发顺序:父组件onBeforeMount > 子组件onBeforeMount > 子组件onMounted > 父组件onMounted > 子组件onActivated > 父组件onActivated
  • 组件卸载,子组件与父组件生命周期钩子触发顺序:子组件onDeactivated > 父组件onDeactivated > 子组件onUnmounted > 父组件onUnmounted