一、依赖注入
1、简介
在前面的笔记中,我们学习过父组件向子组件传递数据时,需要借助props
来实现。但如果父组件想要向孙子组件传递数据,那就要连续使用两层props
逐级向下传递,如果要接收数据的是更深层的后代组件,则需要连续使用多层props
进行逐级传递,代码十分繁琐。
依赖注入(provide/inject
)就是来解决这一问题的,该特性可以实现父组件向其后代组件跨层级传递数据,只要父组件向其后代组件提供了依赖(provide
),无论两者中间相隔多少组件层级,后代组件都可以注入(inject
)父组件提供的依赖,从而实现跨组件层级的数据交互。但过多的使用provide/inject
特性,会增加组件之间的耦合性,降低组件的可复用性,因此不可滥用。
2、provide(提供依赖)
局部provide:
父组件要给后代组件传递数据(提供依赖),需要使用procide(name,value)
函数,该函数有两个参数:参数name
表示的是传递数据的名称(依赖名称),可以是一个字符串或者一个Symbol
,后代组件需要通过依赖名称来查找注入数据的值;参数value
表示传递数据的值(依赖值),可以是任意类型的数据,可以是一个数据常量,也可以是一个响应式变量ref
。
如果注入的数据值是一个响应式变量,则后代组件在使用该数据时,依旧具有响应式状态。
一个组件可以多次调用 provide()
,使用不同的依赖名称,注入不同的依赖值。
<script setup>
// 导入需要使用的API
import { ref, provide } from 'vue'
// 声明一个响应式变量
let count = ref(0);
// 声明一个普通变量
const text = '一个常量'
// 使用provide()注入一个响应式变量
provide('count',count);
// 使用provide()注入一个普通js变量
provide('text',text);
</script>
全局provide:
除了在组件中进行依赖注入,还可以在应用层面进行依赖注入,相当于注入了一个全局的依赖,该依赖可以在应用中的所有组件中注入使用。全局依赖注入需要借助main.js
中createApp()
创建的应用实例:
// main.js中导入API
import { createApp, ref } from 'vue'
// 其他。。。
// 创建应用实例
const app = createApp(App)
// 声明响应式变量
let test = ref('哈哈哈')
// provide() 传递常量
app.provide('all', 123)
// provide() 传递响应式变量
app.provide('test', test)
// 其他。。。
想要声明全局变量除了使用依赖注入之外,还可以利用app.config.globalProperties
声明全局变量。
3、inject(注入依赖)
上层组件通过provide()
提供依赖之后,后代组件需要通过inject(name[,default,boolean])
函数来注入依赖,将上层组件传递的数据接收到当前组件中来。该函数只有一个必选参数name
,表示要注入依赖的名称,也就是上层组件中provide()
的name
参数:
<script setup>
// 导入需要使用的API
import { inject } from 'vue';
// 注入响应式变量依赖
let count = inject('count')
console.log(count);
// 注入普通变量依赖
let text = inject('text')
console.log(text);
// 注入全局普通变量依赖
let all = inject('all')
console.log(all);
// 注入全局响应式依赖
let test = inject('test')
console.log(test);
</script>
控制台输出:
根据上面控制台的输出结果可以看出,如果上层组件提供的依赖是一个响应式变量ref
,则通过inject()
注入的依赖将是该ref
对象,不会被解包为变量值,这种方式保持了ref
在上层组件和后代组件之间的响应式变化。
默认值:
默认情况下,后代组件中inject()
注入的依赖必然是上层组件中已经提供的依赖,否则会抛出一个运行时警告。但如果并不要求注入的依赖被提供过,则可以给依赖设置一个默认值。当inject()
未能从上层组件获取到依赖值时,就会将默认值作为依赖的值。这也就是inject()
函数的第一个可选参数:
// 注入一个未提供的依赖 并设置默认值
let test2 = inject('test2','默认值')
console.log(test2); // 默认值
如果默认值需要调用一个函数或者初始化一个类来设置,为了避免在用不到默认值的情况下造成的性能消耗,可以使用工厂函数来创建默认值,并通过inject()
函数的第二个可选参数,表明第二个参数为一个工厂函数:
// 利用工厂函数设置默认值
let test3 = inject('test3',() => new Object(),true)
console.log(test3);
4、响应式变量的依赖注入
我们在上层组件中可以将响应式变量ref
作为提供的依赖,后代组件将该依赖注入之后,也就获取到该ref
,自然也可以在后代组件中修改该ref
的值,但这样会影响到所有注入该依赖的组件,而且逻辑比较混乱。所以针对响应式数据,建议将所有对响应式数据的更新修改操作都集中保持在提供依赖的组件中。
即使需要在后代组件中更新修改响应式数据的值,也可以可以通过在提供依赖的组件中声明并提供一个更新数据的方法。
提供依赖的上层组件:
<script setup>
// 导入需要使用的API
import { ref, provide } from 'vue'
// 声明一个响应式变量
let count = ref(0);
// 声明响应式依赖的更新方法
const changeCount = (n) => {
count.value=n
}
// 使用provide()注入响应式变量
provide('count',count);
// 使用provide()注入响应式变量的更新方法
provide('changeCount',changeCount)
</script>
注入依赖的后代组件:
<script setup>
// 导入需要使用的API
import { inject } from 'vue';
// 注入响应式变量依赖
let count = inject('count')
// 注入响应式变量依赖的更新方法
let changeCount = inject('changeCount')
// 其他。。。
// 调用更新方法修改响应式变量的值
changeCount(1);
// 其他。。。
</script>
5、readonly()
如果想要确保上层组件中提供的依赖数据不被注入依赖的后代组件所修改,则需要借助readonly()
函数来设置依赖只读。如果设置只读的是响应式变量ref
或着复杂数据类型的JS变量,则在后代组件中尝试修改时,上层组件和后代组件中的变量值都不会被修改;如果设置只读的是简单数据类型的JS变量,则在后代组件中尝试修改时,上层组件的变量值不变,下层组件的变量值会变。
因为传递响应式变量和复杂数据类型的JS变量是将本身的引用传递过去了,上层组件和后代组件中引用的同一个ref
;而简单数据类型的JS变量,则将在后代组件中新建了一份变量,与上层组件中的变量并非同一个。
提供依赖的上层组件:
<script setup>
// 导入需要使用的API
import { ref, provide, readonly } from 'vue'
// 声明一个响应式变量
let count = ref(0);
// 声明一个普通变量
const text = '一个常量'
// 使用provide()注入一个响应式变量
provide('count',readonly(count));
// 使用provide()注入一个普通js变量
provide('text',readonly(text));
</script>
注入依赖的后代组件:
<script setup>
// 导入需要使用的API
import { inject } from 'vue';
// 注入响应式变量依赖
let count = inject('count');
console.log(count.value); // 0
count.value++;
console.log(count.value); // 0
// 注入普通变量依赖
let text = inject('text')
console.log(text); // 一个常量
text = '2222'
console.log(text); // 222 但上层组件中依旧是text
</script>
6、依赖名称冲突
如果上层组件中在提供依赖时,多个依赖使用的同一个依赖名称,则最终后代组件在注入依赖时获取的数据,将是最后一个使用该依赖名称的数据,因为后面的数据会将前面的数据覆盖。。
在构建大型项目时,为了避免依赖名称的冲突,可以使用Symbol
作为依赖的名称,因为Symbol
数据永远不会重复。将所有Symbol
存储到一个单独的js文件中,并将Symbol
导出,这样方便Symbol
在上层组件和后代组件的中使用。
用来存储Symbol
的js文件:
// 将Symbol数据导出
export const oneKey = Symbol()
提供依赖的上层组件:
<script setup>
import { provide } from 'vue'
import { oneKey } from './keys.js'
// 向后代组件提供依赖
provide(oneKey, '要提供的数据');
</script>
注入依赖的后代组件:
<script setup>
import { inject } from 'vue'
import { oneKey } from './keys.js'
// 向当前组件注入依赖
const test = inject(oneKey);
</script>
更多Symbol
相关内容请查看:JavaScript 之 Symbol 数据类型。
二、异步组件
1、简介
异步组件是指以异步的方式去加载组件,这些组件不会在页面初始加载时立即加载,而是在需要渲染到DOM上的时候才加载,从而可以提高应用的加载速度和性能。
Vue提供了defineAsyncComponent()
方法用来实现异步组件的功能。
2、基本用法
defineAsyncComponent()
方法的参数是一个返回Promise
的加载函数,当从服务器获取组件成功时调用resolve()
,加载失败时调用reject()
。加载成功之后,该方法的返回值是一个包装过的组件,使用变量接收返回值后,就可以通过变量在组件模板中使用异步加载的组件了。
<script setup>
// 导入要使用的方法
import { defineAsyncComponent } from 'vue'
// 调用方法 异步加载组件 并将加载的组件赋值给AsyncComp
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...从服务器获取组件
// 加载成功
resolve(/* 获取到的组件 */)
// 加载失败
reject(/* 加载失败 */)
})
})
</script>
<template>
<div>
<!-- 正常使用AsyncComp组件 -->
<AsyncComp />
</div>
</template>
3、ES模块动态导入
ES模块动态导入import
,其实也会返回一个Promise
,所以可以动态导入Vue单文件组件并与defineAsyncComponent()
方法搭配使用,实现组件的异步加载。异步加载获取的是一个包装过的组件,它会将接收到的props
和插槽、事件监听器等内容传递给内部加载的异步组件。
Vite 和 Webpack等构建工具也都支持该语法。
<template>
<div>
<div v-if="show">
<AsyncChild />
</div>
</div>
</template>
<script setup>
// 导入需要使用的API
import { ref, defineAsyncComponent } from 'vue'
// 声明响应式变量
let show = ref(false)
// 设置定时器 修改响应式变量的值
setTimeout(() => {
show.value = true;
},3000)
// 异步加载组件 会在页面渲染 <AsyncChild> 时 调用执行
const AsyncChild = defineAsyncComponent(() => {
console.log('加载异步组件');
return import('../components/Child3.vue')
})
</script>
与普通组件一样,异步组件也支持通过app.component()
注册为全局组件:
// main.js
app.component('AsyncChild', defineAsyncComponent(() =>
import('../components/Child3.vue')
))
4、加载与错误状态
既然是异步加载,那就必然会涉及到加载中和加载失败的状态,defineAsyncComponent()
方法在高级选项中提供了处理这些状态方法,包括加载状态显示、加载错误兜底展示等内容:
const AsyncComp = defineAsyncComponent({
// 异步加载组件函数
loader: () => import('../components/Child3.vue'),
// 加载中状态 展示的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms 避免加载太快导致的闪烁问题
delay: 200,
// 加载失败状态 展示的组件
errorComponent: ErrorComponent,
// 可以指定 timeout 时间限制,超时后也会显示这里配置的报错组件,默认值是:Infinity 无限
timeout: 3000
})
5、Suspense
异步组件可以搭配内置的 <Suspense>
组件一起使用,但<Suspense>
目前还是一项实验性功能,因此这里就不展开介绍了,感兴趣的小伙伴可以查阅相应文档:Suspense
。