Commit 1126438d authored by zhangshun's avatar zhangshun
parents ad208b8b 4573ed34
Pipeline #17071 failed with stage
---
title: vue中的Provide _ Inject
date: 2022/10/31
tags:
- Provide
- Inject
author: 张芳利
---
<a name="wqAvz"></a>
# 介绍
在vue项目中我们会经常用到组件之间的相互传参,我们最常用的就是:
1. prop(父组件像子组件传参)
2. emits (子组件向父组件传参)
3. vuex/pinia (组件间传参)
4. provide/inject
5. 以及路由传参等
在对等级之间传参中,像是是父子,我们使用props是ok的,但是有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 prop 沿着组件链逐级传递下去,可能会很麻烦。这里就要用到上面的第四种方法(provide/inject )了,其实vue2中就已经有这个概念了,但是这里讲的是以vue3为主的🌼~<br /> Vue3.0 中,对于组件间的传参,我们可以使用一对 provide 和 inject。无论组件层次结构有多深,父组件都可以作为其所有子组件的数据提供者。父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来接受并使用它。<br />几点:
1. provide和inject是成对出现的,
2. 用于父组件向子孙组件传递数据,
3. provide在父组件中返回要传给下级的数据,inject在需要使用这个数据的子辈组件或者孙辈等下级组件中注入数据。
![1.png](/yifu-study-front-share/images/Provide/Provide.png)
<a name="QgVpj"></a>
# 基本使用
父组件中,引入provide<br />provide 函数有两个参数 provide(key, value)
1. key:是一个字符串,用来标识这个数据
2. value:是一个数据,可以是任意类型的数据,这个数据可以在任意子组件中通过inject函数获取到
例如:父组件
```
// 引入provide
import { provide,ref } from 'vue'
const msg = '我的父组件中的provide'
provide('msg',msg)
```
子组件中,引入inject<br />inject函数也有两个参数 inject(key, defaultValue)<br />key: 传入provide的key<br />defaultValue: 默认值,如果没有找到key,那就只用默认值的数据<br />子组件:
```
// 引入 inject
import { ref,inject,provide } from 'vue'
// 获取父组件中的provide
const msg = inject('msg','wahaha')
....
<h2>我是子组件</h2>
<p>子:{{msg}}</p>
```
孙组件:
```
import { ref,inject,provide } from 'vue'
const msg = inject('msg')
----------------
<h3>我是孙组件</h3>
<p>孙:{{msg}}</p>
```
![image2.png](/yifu-study-front-share/images/Provide/Provide2.png)<br />但是上面的代码有个问题:如果父组件里面的按钮改变msg 的文字,却发现,子组件和孙组件并没有跟着一起改变??
<a name="gc2nC"></a>
## 响应式
在vue3中,大家都知道下响应式的话是用ref或者reactive ,在这里也是一样的,给数据添加ref即可<br />父组件
```
// 引入provide
import { provide,ref } from 'vue'
// ref 创建响应式数据
const msg = ref('我是父组件的数据')
provide('msg',msg)
const changeFun = () => {
msg.value = '我是父组件的数据,我被改变了'
}
```
![image3.png](/yifu-study-front-share/images/Provide/Provide3.png)
这上面我们实现了父像子孙组件传递数据,那如过子孙组件想改变数据呢?
<a name="UgGOz"></a>
## 子孙组件改变数据
如果想子孙组件改变数据,很简单既然上面我们已经是响应式数据了,我们可以直接在子组件更改,然后赋值即可,就像父组件里面的流程一样
子组件:
```
import { ref,inject,provide } from 'vue'
const msg = inject('msg','wahaha')
const changeFun = () => {
msg.value = '我是子组件的数据,我被改变了'
}
provide('msg',msg)
```
![image4.png](/yifu-study-front-share/images/Provide/Provide4.png)<br />但是这样操作好像有点混乱,更好的做法应该是在父组件创建一个专门改变数据的方法,然后传递下去,让子孙组件去调用<br />父组件:
```
const changeMsg = (value) => {
msg.value = '我是msg,我被改变成'+ value
}
provide('changeMsg',changeMsg)
```
子孙组件:
```
import { ref,inject } from 'vue'
const msg = inject('msg','wahaha')
const changeMsg = inject('changeMsg')
const changeFun = () => {
changeMsg('娃哈哈')
}
```
通过上面的方法我们可以进行数据修改,但是有时候我们发现可能一不小心,还是会去在子孙组件中直接修改msg 的数据,改如何禁止呢?
<a name="ZL8Wl"></a>
## 子孙组件只使用
如何让子孙组件只用数据呢?而不会一不下更改数据。这里我们借助 readonly 只读属性<br />父组件:
```
// 引入provide
import { provide,ref,readonly } from 'vue'
// ref 创建响应式数据
const msg = ref('我是父组件的数据')
provide('msg',readonly(msg))
const changeFun = () => {
changeMsg('父级改变了')
}
const changeMsg = (value) => {
msg.value = '我是msg,我被改变成'+ value
}
provide('changeMsg',changeMsg)
```
子组件
```
import { ref,inject,provide } from 'vue'
const msg = inject('msg','wahaha')
const changeMsg = inject('changeMsg')
const changeFun = () => {
msg.value= 'haha'
//changeMsg('父级改变了')
}
provide('msg',msg) // 并不会被改变
```
我们发现点击的时候数据并不会改变,并且会出现一个警告:<br />![image5.png](/yifu-study-front-share/images/Provide/Provide5.png)
<a name="VG2tK"></a>
# 源码
通过上面的学习,我们已经能在项目中使用provide/inject了,现在让我们来看一下他们俩的源码是什么样的。<br />下载源码的地址:[https://github.com/vuejs](https://github.com/vuejs) ,vue的源码是在core中。<br />provide/inject的源码地址:/core/packages/runtime-core/src>/apiInject.ts 中,因为ts 不是很熟悉,所以我这边转换为js 大家来看一下;
<a name="FBuRi"></a>
### provide
```
import { currentInstance } from './component';
export function provide(key, value) {
if (!currentInstance) {
if (__DEV__) {
warn(`provide() can only be used inside setup().`); // 不能在setup之外使用
}
}
else {
let provides = currentInstance.provides; // 获取当前实例的provides
// by default an instance inherits its parent's provides object
//默认情况下,实例继承其父对象的providers
// but when it needs to provide values of its own, it creates its
// 但当它需要提供自己的价值时,它就会创造自己的价值
// own provides object using parent provides object as prototype.
// 提供对象使用父提供对象作为原型。
// this way in `inject` we can simply look up injections from direct
// 这样,在“注入”中,我们可以简单地从直接查找注入
// parent and let the prototype chain do the work.
// 让原型链做工作。
const parentProvides = currentInstance.parent && currentInstance.parent.provides;
// 获取当前实例的父实例的provides
if (parentProvides === provides) {
// 如果当前实例的provides和父实例的provides相同
provides = currentInstance.provides = Object.create(parentProvides);
// Object.create(parentProvides) 会创建一个新对象,这个对象的原型是parentProvides
}
provides[key] = value;
}
}
```
源码里面我们看到
1. 若currentInstance无值即没有当前活动的实例,那开发环境会报警 且不能在setup之外使用
2. 取当前实例上的provides对象,再取父实例对象上的provides对象
3. 两者相比较,如果相等,则继承父实例对象上的provides
4. 赋值
provide API就是通过获取当前组件的实例对象,将传进来的数据存储在当前的组件实例对象上的provides上,并且通过ES6的新API Object.create把父组件的provides属性设置到当前的组件实例对象的provides属性的原型对象上。<br />我们再来看一下组件实例的 provides<br />
![image6.png](/yifu-study-front-share/images/Provide/Provide6.png)<br />位置在:runtime-core/src/component.ts<br />
![image7.png](/yifu-study-front-share/images/Provide/Provide7.png)<br />
![image8.png](/yifu-study-front-share/images/Provide/Provide8.png)<br />从源码里面我们能看到 instance 中有两个属性,一个是parent,一个provides。在初始化的时候如果有父组件那就把父组件的provides赋值给当前实例,如果没有父组件,那就创建一个新的appContext(从父组件获取 appContext,如果没有,从 vnode.appContext 获取,如果没有,使用空的 appContext),并且把provides属性设置为新对象原型对象上的属性。
<a name="CTtTq"></a>
### inject
```
export function inject(key, defaultValue, treatDefaultAsFactory = false) {
// fallback to `currentRenderingInstance` so that this can be called in
// a functional component
const instance = currentInstance || currentRenderingInstance; // 获取当前实例
if (instance) {
// #2400
// to support `app.use` plugins,
// fallback to appContext's `provides` if the instance is at root
const provides = instance.parent == null // 如果当前实例没有父实例
? instance.vnode.appContext && instance.vnode.appContext.provides // 获取当前实例的appContext的provides
: instance.parent.provides; // 获取当前实例的父实例的provides
if (provides && key in provides) { // 如果provides存在并且key在provides中
// TS doesn't allow symbol as index type
return provides[key]; // 返回provides中key对应的值
}
else if (arguments.length > 1) { // 如果参数大于1
return treatDefaultAsFactory && isFunction(defaultValue) // treatDefaultAsFactory为true 并且defaultValue为函数
? defaultValue.call(instance.proxy) // 调用defaultValue函数
: defaultValue; // 否则返回defaultValue
}
else if (__DEV__) {
warn(`injection "${String(key)}" not found.`); // 如果没有找到key对应的值 报错
}
}
else if (__DEV__) {
warn(`inject() can only be used inside setup() or functional components.`); // 如果当前实例不存在 报错
}
}
```
从上面可以看出
1. 获取当前组件实例,判断是否有实例
2. 获取当前实例或父的provides对象,进行遍历
3. 如果provides存在并且key在provides中,返回provides中key对应的值
4. 如果没有则判断是否存在默认内容,默认内容如果是个函数,就执行并且通过call方法把组件实例的代理对象绑定到该函数,否则就直接返回默认内容。
以上总结:<br />整体来说就是利用原型和原型链进行数据的继承和获取。在provide 函数中,设置父级的provides为当前provides对象原型对象上的属性,在inject函数中,先找provides对象自身的属性,如果自身查找不到,则沿着原型链向上一个对象中去查找
<a name="VoVPb"></a>
# 额外知识点 Object.create()
**Object.create()** 方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)。<br />Object.create(proto, [propertiesObject])
- proto:必须。新创建对象的原型对象,即通过 Object.create()生成的对象的原型 指向 proto(可以是 null、对象、函数的 prototype 属性)。(注:**创建空原型的对象时需传 null , 否则会抛出 TypeError 异常**)。
- propertiesObjec:可选。添加到新创建对象的可枚举属性(即其自身的属性,而不是原型链上的枚举属性)对象的属性描述符以及相应的属性名称。这些属性对应 Object.defineProperties()的第二个参数。
- 返回值:一个新对象,带着指定的原型对象和属性。
因此可以看出 Object.create()的两个参数其实就是,proto 设置原型对象的属性和方法,propertiesObjec 设置新创建对象的属性和方法。而 propertiesObjec 创建属性方法的时候加入了属性可配置的形式,相当于简化并增强对象对象的写法,让其拥有对象字面量和 Object.defineProperties()的特性
```
const obj = {
name:'我是obj'
}
const obj2 = Object.create(obj)
console.log(obj2)
```
![image9.png](/yifu-study-front-share/images/Provide/Provide9.png)<br />第二个参数
```
const obj3 = Object.create(obj,{
age:{
value:18
}
})
console.log(obj3)
```
![image10.png](/yifu-study-front-share/images/Provide/Provide10.png)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment