一、名词解析:
provide:Object | () => Object
inject:Array<string> | { [key: string]: string | Symbol | Object }
说明:
provide和inject主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中。这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。
这对选项是成对使用的。子孙组件想要获取祖先组件的资源,那么怎么办呢,总不能一直取父级往上吧,而且这样代码结构容易混乱。这个就是这对选项要干的事情。
provide:提供依赖,是一个对象或返回一个对象的函数。该对象包含可注入其子孙的 property,也就是属性和属性值。在该对象中你可以使用 ES2015 Symbols 作为 key,但是只在原生支持 Symbol 和 Reflect.ownKeys 的环境下可工作。
var Provider = {
provide: {
foo: "bar",
},
// ...
};
inject: 注入依赖,是一个字符串数组,或者是一个对象,对象的 key 是本地的绑定名,value 是:
1.在可用的注入内容中搜索用的 key (字符串或 Symbol),或
2.一个对象,包含from查找健和default默认值,该对象的:
fromproperty 是在可用的注入内容中搜索用的 key (字符串或 Symbol)defaultproperty 是降级情况下使用的 value
提示:
provide和inject绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 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无法实时响应的问题和解决方法
上面已经说过:provide 和 inject 绑定并不是可响应的,虽然provide可以进行父组件向后代组件进行传值。但是,他没办法监听传输数据的变化。或者说,就是如果我在父组件改变注入的值,它没办法更新。
提示:
provide和inject绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 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"],
以上这几种方式都无法响应和对一整个对象赋值的改变,并且watch和computed也无法监听到一整个对象赋值的改变,只能响应和监听某个对象中的属性改变,这不符合我们的使用习惯,我们希望可以对一整个对象赋值并监听它的改变。
如果我们希望整个数据都是响应式的。那么可以通过以下方法实现:
我们可以让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);
},
},
祖先组件提供一个函数,子孙组件通过computed和watch获取数据的变化。
完整代码演示如下:
祖先组件
<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')
}
这里找到 initInjections 和 initProvide 方法,这就是 provide 和 inject 的初始化方法了。这两个方法都是在 src/core/instance/inject.js 中。