vue3 - 组件化补充

124 阅读8分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第21天,点击查看活动详情

生命周期函数

每个组件都可能会经历从创建、挂载、更新、卸载等一系列的过程, 而这一系列的过程就被称之为组件的生命周期

在这个过程中的某一个阶段,我们可能会想要添加一些属于自己的代码逻辑,为此vue给我们提供了对应的生命周期钩子

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

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

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

vue的生命周期钩子示意图

refs

某些情况下,我们在组件中想要直接获取到元素对象或者子组件实例

这个时候,我们可以给元素或者组件绑定一个ref的attribute属性

在组件对象中有一个名为$refs的属性,其值是一个对象

在该对象中持有注册过 ref attribute 的所有 DOM 元素和组件实例

<template>
	<div>
		<h2 ref="h2">this is h2 element</h2>
		<Child ref="child" />
	</div>
</template>

<script>
import Child from './components/Child'

export default {
	components: {
		Child
	},

	// ref获取到的一般是组件的原生dom对象
	// 所以只有组件或对应元素被挂载后,才会存在对应的dom对象
	mounted() {
		// 如果是元素,那么获取到的就是对应的原生DOM
		console.log(this.$refs.h2)
		// 如果是一个组件,那么获取到的就是对应的组件实例对象
		console.log(this.$refs.child)

		// 在该组件实例对象上有一个属性$el
		// 该属性的值
		// 1. 在组件有且仅有一个根元素的情况下,值是组件根元素所对应的原生dom对象
		// 2. 如果组件有多个根元素,vue会将子组件作为一个个node节点来处理
		// 		所以获取到的值为第一个node节点,一般为#text
		// 		所以如果要获取到第一个真正的元素节点需要调用其nextElementSibling属性

		// 因此,虽然vue3支持在一个组件中拥有多个根元素
		// 但是为了维护和使用方便,依旧推荐一个组件有且只能拥有一个根组件
		console.log(this.$refs.child.$el)
	}
}
</script>
<template>
	<Infos  :ref="getSectionRef" />
  <Facility  :ref="getSectionRef" />
  <Landlord :ref="getSectionRef" />
</template>

<script setup>
  // 当ref绑定的不是字符串,而是函数的时候
  // vue会自动调用函数,并把对应元素或组件实例作为函数的参数传入
  const getSectionRef = value => {
    	// 挂载完成后会执行该回调
      // 卸载后也会执行该回调
    	if (!value) return

    	// 使用这种方式可以方便我们批量获取对应元素
      // 对于组件实例 获取组件的根元素的原生dom元素可以通过$el属性
      sectionEls.value[name] = value.$el
    }
  }
</script>

$parent$root

属性说明
$parent获取当前组件的父组件
$root获取当前组件树的根组件,也就是App实例对象

动态组件

<!--
	动态组件是使用 component 组件
	通过一个特殊的属性 -> is 来设置该内置组件需要展示的那个组件
-->

<!--
	is的值为属性名称,只要是任何的合法的组件名都是可以被正常识别的
	即cpnName的值 可以是 myCpn, my-cpn 和 MyCpn 中的任意一个
	动态组件一般需需要在多个组件之间进行切换展示的时候使用
	所以其is一般应该绑定的是一个变量
-->
<component :is="cpnName" />
<!-- 在动态组件上传递props 和 进行事件绑定 的方式 和普通组件是一致的 -->
<component name="Klaus" @getName="getName" :is="cpnName" />

keep-alive

对于动态组件,在切换组件后,原本的组件会被销毁,新的组件会被创建

如果频繁切换的时候,会比较损耗性能,且因为原本组件已经被销毁,所以原本组件的状态也会被重置

在开发中某些情况我们希望继续保持组件的状态,而不是销毁掉,这个时候我们就可以使用一个内置组件:keep-alive

属性可选值说明
include`stringRegExpArray`只有名称匹配的组件会被缓存
exclude`stringRegExpArray`任何名称匹配的组件都不会被缓存
max`numberstring`最多可以缓存多少组件实例,一旦达到这个数 字,那么缓存组件中最近没有被访问的实例会被销毁

include和exclude二者都可以用逗号分隔字符串、正则表达式或一个数组来表示

  1. 如果使用逗号分隔的字符串来缓存多个组件的时候,不能添加对应的空格

  2. keep-alive在缓存组件的时候,查找的不是引入的组件名,而是对应组件实例的name属性

    也就是说组件Options Api中的name选项,该选项除了可以被keep-alive使用外

    也可以在vue devTool中标识对应组件的名称,以方便进行调试

对于缓存的组件来说,再次进入时,我们是不会执行created或者mounted等生命周期函数的(除第一次进入和被销毁的时候外)

但是有时候我们确实希望监听到何时重新进入到了组件,何时离开了组件

这个时候我们可以使用activated 和 deactivated 这两个生命周期钩子函数来监听

这两个生命周期主要缓存组件显示和隐藏切换的时候就会被触发,即使该组件是刚刚被创建或者正在被销毁

也就是说这两个生命周期函数可能会和created和unmounted这两个生命周期钩子一起被回调

异步组件

默认情况下,在构建整个组件树的过程中,因为组件和组件之间是通过模块化直接依赖的,那么打包工具在打包时就会将组件模块打包到一起

即所有的自己的业务逻辑会被打包到一个名为app.js的文件中,所有的外部依赖,也就是那些第三方库会被打包会被单独打包到vendor.js中

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

所以,对于一些不需要立即使用的组件,我们可以单独对它们进行拆分,拆分成一些小的代码块chunk.js,例如那些不需要在首屏进行渲染的内容, 这些chunk.js会在需要时从服务器加载下来,并且运行代码,显示对应的内容, 也就是说为的按需引入

默认情况下,当我们使用import函数异步引入的模块,webpack等打包工具在对这类模块进行打包的过程中,就会单独形成一个独立的js文件,这个过程就被称之为分包

所以如果我们的项目过大了,对于某些组件我们希望通过异步的方式来进行加载(目的是可以对其进行分包处理),那么Vue中给我们提供了一个函数: defineAsyncComponent

defineAsyncComponent

<template>
	<div>
		 <Cpn />
	</div>
</template>

<script>
import { defineAsyncComponent } from 'vue'

// 写法一 -> 参数是一个返回对应Promise的工厂函数
// 所谓工厂函数 就是那些专门用于返回特定实例的函数

// 在import函数中引入组件也是可以省略后缀的
// 因为import函数和import关键字的解析规则本质上是一致的
const Cpn = defineAsyncComponent(() => import('./components/Cpn'))

export default {
	components: {
		Cpn
	}
}
</script>
// defineAsyncComponent 参数二 - 对应的配置对象
const Home = defineAsyncComponent({
  // 实际需要加载的组件
  loader: () => import('./components/Home.vue'),
  // 异步组件加载过程中显示的组件
  loadingComponent: Loader,
  // 组件加载错误的时候显示的组件
  errorComponent: Error,
  // 延迟2s后,再显示loadingComponent
  delay: 2000,
  // 单位ms,如果在timeout中所设置的时间之内组件还没有被下载下来
  // 无论是否报错,直接认定为失败
  // 默认值是 Infinity
  timeout: 0
})

但是,实际开发中,我们可能并不需要配置那么多的属性,为此vue提供了个内置组件suspense

Suspense是一个内置的全局组件,该组件有两个插槽

  • default:如果default可以显示,那么显示default的内容
  • fallback:如果default无法显示,那么会显示fallback插槽的内容
<suspense>
  <template #default>
     <About />
  </template>
  <!--
    如果默认插槽中的内容无法显示(error的时候)或者About组件还在加载的时候(loading的时候)
    会显示fallback插槽中的内容
  -->
  <template #fallback>
     <!--
        这里只是演示,所以也放置了异步组件,实际开发中,推荐在fallback插槽中放置同步组件
     -->
     <Home />
  </template>
</suspense>

组件v-model

当我们在组件上使用v-model的时候

默认情况下的v-model其实是绑定了 modelValue 属性和 @update:modelValue的事件

我们可以给v-model传入一个参数,那么这个参数的名称就是我们绑定属性的名称

例如: v-model:title 等价于 1. 绑定了title属性 2. 监听了 @update:title的事件

父组件

<template>
	<div>
		 <!--
				// 默认情况下,组件上的v-model传递给子组件的时候,对应的prop名为modelValue
				// 自定义事件的$event本质上就是子组件在调用@update:modelValue的时候传入的参数值
				<Cpn v-model="count" /> === <Cpn :modelValue="count" @update:modelValue="count = $event" />

				// 也可以自己指定所传递的prop的名称 -> 通过v-model:[propName]的形式
				<Cpn v-model:count="count" /> === <Cpn :count="count" @update:count="count = $event" />
		  -->
		 <Cpn v-model:count="count" />
		 <div>{{ count }}</div>
	</div>
</template>

<script>
import Cpn from './components/Cpn'

export default {
	components: {
		Cpn
	},
	data() {
		return {
			count: 0
		}
	}
}
</script>

子组件

<template>
	<div>
		<input type="text" v-model="count">
	</div>
</template>

<script>
export default {
	emits: ['update:count'],

	props: {
		count: {
			type: [String, Number]
		}
	},

	watch: {
		count(v) {
			this.$emit('update:count', v)
		}
	}
}
</script>

Mixin

组件和组件之间有时候会存在相同的代码逻辑,我们希望对相同的代码逻辑进行抽取的时候,就可以使用mixin选项

mixin选项只要是用于抽取options api中的相关逻辑代码,在compositions api中使用较少

  1. Mixin提供了一种非常灵活的方式,来分发Vue组件中的可复用功能
  2. 一个Mixin对象可以包含任何组件选项
  3. 当组件使用Mixin对象时,所有Mixin对象的选项将被 混合 进入该组件本身的选项中

mixin

// 导出一个对象 该对象中的属性和options api中的可配置选项都是一致的
// 在mixin的时候,会执行类似于 Object.assign(mixin中选项,组件options选项)
// 所以在冲突的时候,会优先保留组件内部选项,而覆盖mixin中的选项

// 而对于生命周期函数而言,组件内部的对应生命周期函数和mixin中的对应的生命周期函数 会合并成一个数组
// 在执行对应生命周期的函数的时候,会迭代对应数组并依次执行其中方法
// 一般先执行mixin中的对应生命周期函数,在执行组件中的对应生命周期函数
export default {
	data() {
		return {
			msg: 'message in mixin'
		}
	},

	created() {
		console.log('created in mixin')
	}
}

组件

<template>
	<div>
		{{ msg }}
	</div>
</template>

<script>
// 引入mixin
import cpnMixin from '/src/mixin/cpnMixin'

export default {
	// 使用mixins选项 进行 混入(注入)
	mixins: [cpnMixin],

	created() {
		console.log('create in cpn')
	}
}
</script>

冲突合并

  1. 如果是data函数的返回值对象
    • 返回值对象默认情况下会进行合并
    • 如果data返回值对象的属性发生了冲突,那么会保留组件自身的数据
  2. 生命周期钩子函数
    • 生命周期的钩子函数会被合并到数组中,依次被调用
  3. 值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象
    • 比如都有methods选项,并且都定义了方法,那么它们都会生效
    • 但是如果对象的key相同,那么会取组件对象的键值对

全局mixin

如果组件中的某些选项,是所有的组件都需要拥有的,那么这个时候我们可以使用全局的mixin

全局的Mixin可以使用 应用app的方法 mixin 来完成注册, 一旦注册,那么全局混入的选项将会影响每一个组件

app.mixin({
  data() {
    return {
      msg: 'msg in global mixin'
    }
  },
  created() {
    console.log('created in global mixin')
  }
})