Vue中的provide和inject


一、名词解析:

provide:Object | () => Object
inject:Array<string> | { [key: string]: string | Symbol | Object }

说明:

provideinject 主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中。

这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。

这对选项是成对使用的。子孙组件想要获取祖先组件的资源,那么怎么办呢,总不能一直取父级往上吧,而且这样代码结构容易混乱。这个就是这对选项要干的事情。

provide:提供依赖,是一个对象或返回一个对象的函数。该对象包含可注入其子孙的 property,也就是属性和属性值。在该对象中你可以使用 ES2015 Symbols 作为 key,但是只在原生支持 SymbolReflect.ownKeys 的环境下可工作。

var Provider = {
    provide: {
        foo: 'bar'
    },
    // ...
}

inject: 注入依赖,是一个字符串数组,或者是一个对象,对象的 key 是本地的绑定名,value 是:

1.在可用的注入内容中搜索用的 key (字符串或 Symbol),或

2.一个对象,包含from查找健和default默认值,该对象的:

  • from property 是在可用的注入内容中搜索用的 key (字符串或 Symbol)
  • default property 是降级情况下使用的 value

提示:provideinject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。

//inject为一个字符串数组时,foo为provide里foo;
//为一个对象时,foo是本地生成的变量名;同时匹配provide里foo,若有from,则匹配from的值;
const Child = {
    inject: ['foo'],
}
//或是default 默认值:
//意为:若是provide里有foo属性,则使用foo的值,没有的话,生成一个foo,其value值为foo。
const Child = {
    inject: {
        foo: { default: 'foo' }
    }
}
//有 from 查找键和 default 默认值的:
//意为:from为key值,有bar这个属性,就使用bar,没有的话,其foo的默认value值为foo。
const Child = {
    inject: {
        foo: {
            from: 'bar',
            default: 'foo'
        }
    }
}
//或者为 default 默认值设定一个工厂方法:
const Child = {
    inject: {
        foo: {
        from: 'bar',
            default: () => [1, 2, 3]
        }
    }
}

二、官网示例

// 父级组件提供 'foo'
var Provider = {
    provide: {
        foo: 'bar'
    },
    // ...
}

// 子组件注入 'foo'
var Child = {
    inject: ['foo'],
    created () {
        console.log(this.foo) // => "bar"
    }
    // ...
}

三、项目案例

父组件

项目最外层的布局组件about.vue

<template>
    <div class="about">
        <h1>This is an about page</h1>
        <hr>
        <aboutCom ref="aboutCom"></aboutCom>
    </div>
</template>
<script>
import aboutCom from "../components/aboutCom.vue";
export default {
    components:{
        aboutCom
    },
    // 父组件中返回要传给下级的数据
    provide () {
        return {
            reload: this.reload,
            item: this.item,
            foo:'bar',
            bar:'zanwu'
        }
    },
    data() {
        return {
            item:{
                name:'about',
                age:28
            }
        }
    },
}
</script>

这里就是我们说的provide,向下提供信息,这里提供的是当前的vue实例,相当于给了后代一个接口。这样在任何的后代组件中,都可以使用inject选项来接收指定的我们想要添加在这个实例上的属性。

子组件及后代

<template>
    <div>
       <h2>这是about的子组件</h2>
        <hr>
       <aboutComChild ref="aboutComChild"></aboutComChild>
    </div>
</template>
<script>
import aboutComChild from "./aboutComChild.vue";
export default {
    components:{
        aboutComChild
    },
    // //引用vue reload方法
    inject: ['item',"foo"],
    data() {
        return {
            aboutComData:{
                name:"aboutCom",
                age:18
            }
        }
    },
    created(){
        this.init();
    },
    methods: {
        init(){
            console.log('这是about的子组件')
            console.log(this.foo)
        },
    },
}
</script>
<template>
    <div>
        <h3>这是aboutCom的子组件</h3>
        <hr>
        <aboutComChildSon ref="aboutComChildSon"></aboutComChildSon>
    </div>
</template>
<script>
import aboutComChildSon from "./aboutComChildSon.vue";
export default {
    components:{
        aboutComChildSon
    },
    //引用vue reload方法
    inject:{
        foo: { 
            default: 'foo',
            from: 'bar', 
        }
    },
    data() {
        return {
            aboutComChildData:{
                name:"aboutComChild",
                age:12
            }
        }
    },
    created(){
        this.init();
    },
    methods: {
        init(){
            console.log('这是aboutCom的子组件')
            console.log(this.foo)
        },
    },
}
</script>
<template>
    <div>
        <h4>这是aboutComChild的子组件</h4>
    </div>
</template>
<script>
export default {
    //引用vue reload方法
    inject: ['reload','item'],
    created(){
        this.init();
    },
    methods: {
        init(){
            console.log('这是aboutCom的子组件')
            console.log(this.reload)
            console.log(this.item)
        }
    },
}
</script>

这样也就可以访问了,当做当前vue实例的属性。这样做的好处,相当于给了一个捷径,不用使用$parent一级一级的访问。

我们可以把依赖注入看做一部分“大范围有效的prop”,

  • 祖先组件不需要知道哪些后代组件需要使用它提供的属性
  • 后代组件不需要知道被注入的属性来自哪里

四、关于provide和inject无法实时响应的问题和解决方法

上面已经说过:provideinject 绑定并不是可响应的,虽然provide可以进行父组件向后代组件进行传值。但是,他没办法监听传输数据的变化。或者说,就是如果我在父组件改变注入的值,它没办法更新。

提示:provideinject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。

经过测试发现:

// 父组件
provide: function() {
    this.myData3 = Vue.observable({
        val: "我没改变",
    });
    return {
        myData1: this.myData1, //非响应
        myData2: this.myData2, //响应
        myData3: this.myData3, //响应
    };
},
data() {
    return {
        myData1: "我没改变",
        myData2: {
            val: "我没改变",
        },
    };
},
methods: {
    change(){
        this.myData1 = "我改变了" // 触发后不能改变
        this.myData2.val = "我改变了" // 触发后能改变
        this.myData3.val = "我改变了" // 触发后能改变
        // 触发后不能改变
        this.myData2 = {
            val: "我改变了",
        } 
        // 触发后不能改变
        this.myData3 = {
            val: "我改变了",
        } 
    },
}

// 孙组件
inject: ["myData1", "myData2", "myData3"],

以上这几种方式都无法响应和对一整个对象赋值的改变,并且watchcomputed也无法监听到一整个对象赋值的改变,只能响应和监听某个对象中的属性改变,这不符合我们的使用习惯,我们希望可以对一整个对象赋值并监听它的改变。

如果我们希望整个数据都是响应式的。那么可以通过以下方法实现:

我们可以让provide提供一个函数。函数内部返回一个响应式的数据。此时整条数据的响应式的状态并不会丢失。

而且这样做有一个好处,即无法直接修改myData4 的值,因为他是一个计算属性。这样就可以避免数据的混乱。

关键代码如下:

// 父组件
provide: function () {
    return {
        myData4: () => this.myData4, //响应
    };
},
data() {
    return {
        myData4: {
            val: "我没改变",
        },
    };
 },
methods: {
    change() {
        // 触发后能改变
        this.myData4 = {
            val: "我改变了",
        };
    },
},


// 孙组件
inject: ["myData4"],
computed: {
    computedMyData4() {
      return this.myData4()
    }
},
watch: {
    computedMyData4(val) {
      console.log("我是孙组件,我监听到了myData4改变", val);
    },
},

祖先组件提供一个函数,子孙组件通过computedwatch获取数据的变化。

完整代码演示如下:

祖先组件

<template>
    <div>
        <h3>这是祖先组件</h3>
        <hr>
        <button @click="change">点我查看数据变化</button>
    </div>
</template>
<script>
import Vue from 'vue';
export default {
    // 父组件中返回要传给下级的数据
    provide () {
        this.myData3 = Vue.observable({
            val: "我没改变",
        });
        return {
            myData4: () => this.myData4, //响应
            myData1: this.myData1, //非响应
            myData2: this.myData2, //响应
            myData3: this.myData3, //响应
        };
    },
    data() {
        return {
            myData1: "我没改变",
            myData2: {
                val: "我没改变",
            },
            myData4: {
                val: "我没改变",
            },
        }
    },
    methods:{
        change(){
            console.log('change')
            this.myData1 = "我改变了" // 触发后不能改变
            this.myData2.val = "我改变了" // 触发后能改变
            this.myData3.val = "我改变了" // 触发后能改变
            // 触发后不能改变
            this.myData2 = {
                val: "我改变了",
            } 
            // 触发后不能改变
            this.myData3 = {
                val: "我改变了",
            }
            // 触发后能改变
            this.myData4 = {
                val: "我改变了myData4",
            };
        },
    }
}
</script>

子孙组件(可以为子组件,但子组件可以用prop传参数,更方便)

<template>
    <div>
        <h3>这是子孙组件</h3>
        <hr>
        <button @click="change">点我查看数据变化</button>
    </div>
</template>
<script>
//import 自己引用
export default {
    inject: ["myData1", "myData2", "myData3","myData4"],
    data() {
        return {

        }
    },
    computed: {
        computedMyData4() {
            return this.myData4()
        }
    },
    watch:{
        myData1:{
            handler(val,val1){
                console.log(val,val1,'myData1===')
            },
            deep: true,
        },
        myData2:{
            handler(val,val1){
                console.log(val,val1,'myData2====')
            },
            deep: true,
        },
        myData3:{
            handler(val,val1){
                console.log(val,val1,'myData3====')
            },
            deep: true,
        },
        computedMyData4:{
            handler(val,val1){
                console.log(val,val1,'myData4====')
            },
            deep: true,
        }
    },
    mounted() {
        console.log(this.myData1,"myData1")
        console.log(this.myData2,"myData2")
        console.log(this.myData3, "myData3")
        console.log(this.myData4, "myData4")
    },
}
</script>

补充:

通过上面的代码,我们可以发现父子组件加载时候,执行的先后顺序为:

父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子 beforeMount -> 子mounted -> 父mounted

下面附上源码位置:

初始化的方法都是在 Vue 的 _init 方法中的。

  // src/core/instance/init.js
  Vue.prototype._init = function (options?: Object) {
    ……
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
  }

这里找到 initInjectionsinitProvide 方法,这就是 provideinject 的初始化方法了。这两个方法都是在 src/core/instance/inject.js 中。


文章作者: 弈心
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 弈心 !
评论
  目录