胖蔡说技术
随便扯扯

Vue3中如何使用Provide和Inject实现通信

介绍

在vue项目中我们会经常用到组件之间的相互传参,我们最常用的就是:

  • prop(父组件像子组件传参)
  • emits (子组件向父组件传参)
  • vuex/pinia (组件间传参)
  • provide/inject
  • 以及路由传参等

在对等级之间传参中,像是是父子,我们使用props是ok的,但是有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 prop 沿着组件链逐级传递下去,可能会很麻烦。这里就要用到上面的第四种方法(provide/inject )了,其实vue2中就已经有这个概念了,但是这里讲的是以vue3为主的?~

Vue3.0 中,对于组件间的传参,我们可以使用一对 provide 和 inject。无论组件层次结构有多深,父组件都可以作为其所有子组件的数据提供者。父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来接受并使用它。几点:

  • provide和inject是成对出现的
  • 用于父组件向子孙组件传递数据
  • provide在父组件中返回要传给下级的数据,inject在需要使用这个数据的子辈组件或者孙辈等下级组件中注入数据

基础使用

父组件中,引入provideprovide 函数有两个参数 provide(key, value)

  • key:是一个字符串,用来标识这个数据
  • value:是一个数据,可以是任意类型的数据,这个数据可以在任意子组件中通过inject函数获取到
// 父组件
// 引入provide
import { provide,ref } from 'vue'

const msg  = '我的父组件中的provide'

provide('msg',msg)

子组件中,引入injectinject函数也有两个参数

  • inject(key, defaultValue)
  • key: 传入provide的keydefaultValue: 默认值,如果没有找到key,那就只用默认值的数据
// 子组件
// 引入 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>

但是上面的代码有个问题:如果父组件里面的按钮改变msg 的文字,却发现,子组件和孙组件并没有跟着一起改变??

响应式

在vue3中,大家都知道下响应式的话是用ref或者reactive ,在这里也是一样的,给数据添加ref即可:

// 引入provide
import { provide,ref } from 'vue'

// ref 创建响应式数据
const msg  = ref('我是父组件的数据')

provide('msg',msg)

const changeFun = () => {
  msg.value = '我是父组件的数据,我被改变了'
}

这上面我们实现了父像子孙组件传递数据,那如过子孙组件想改变数据呢?

子孙组件改变数据

如果想子孙组件改变数据,很简单既然上面我们已经是响应式数据了,我们可以直接在子组件更改,然后赋值即可,就像父组件里面的流程一样。

import { ref,inject,provide } from 'vue'

const msg = inject('msg','wahaha')

const changeFun = () => {
  msg.value = '我是子组件的数据,我被改变了'
}

provide('msg',msg)

但是这样操作好像有点混乱,更好的做法应该是在父组件创建一个专门改变数据的方法,然后传递下去,让子孙组件去调用父组件:

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 的数据,改如何禁止呢?

子孙组件只使用

如何让子孙组件只用数据呢?而不会一不下更改数据。这里我们借助 readonly 只读属性

父组件:

// 引入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)  // 并不会被改变

我们发现点击的时候数据并不会改变,并且会出现一个警告:

源码

通过上面的学习,我们已经能在项目中使用provide/inject了,现在让我们来看一下他们俩的源码是什么样的。下载源码的地址:https://github.com/vuejsvue的源码是在core中。provide/inject的源码地址:/core/packages/runtime-core/src>/apiInject.ts 中,因为ts 不是很熟悉,所以我这边转换为js 大家来看一下.

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;
    }
}

源码里面我们看到:

  • currentInstance无值即没有当前活动的实例,那开发环境会报警 且不能在setup之外使用
  • 取当前实例上的provides对象,再取父实例对象上的provides对象
  • 两者相比较,如果相等,则继承父实例对象上的provides
  • 赋值

provide API就是通过获取当前组件的实例对象,将传进来的数据存储在当前的组件实例对象上的provides上,并且通过ES6的新API Object.create把父组件的provides属性设置到当前的组件实例对象的provides属性的原型对象上。

我们再来看一下组件实例的 provides:

位置在:runtime-core/src/component.ts

从源码里面我们能看到 instance 中有两个属性,一个是parent,一个provides。在初始化的时候如果有父组件那就把父组件的provides赋值给当前实例,如果没有父组件,那就创建一个新的appContext(从父组件获取 appContext,如果没有,从 vnode.appContext 获取,如果没有,使用空的 appContext),并且把provides属性设置为新对象原型对象上的属性。

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.`); // 如果当前实例不存在 报错
    }
}

从上面可以看出:

  • 获取当前组件实例,判断是否有实例
  • 获取当前实例或父的provides对象,进行遍历
  • 如果provides存在并且key在provides中,返回provides中key对应的值
  • 如果没有则判断是否存在默认内容,默认内容如果是个函数,就执行并且通过call方法把组件实例的代理对象绑定到该函数,否则就直接返回默认内容。

以上总结:整体来说就是利用原型和原型链进行数据的继承和获取。在provide 函数中,设置父级的provides为当前provides对象原型对象上的属性,在inject函数中,先找provides对象自身的属性,如果自身查找不到,则沿着原型链向上一个对象中去查找。

赞(0) 打赏
转载请附上原文出处链接:胖蔡说技术 » Vue3中如何使用Provide和Inject实现通信
分享到: 更多 (0)

请小编喝杯咖啡~

支付宝扫一扫打赏

微信扫一扫打赏