一、名词解析:
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默认值,该对象的:
from
property 是在可用的注入内容中搜索用的 key (字符串或 Symbol)default
property 是降级情况下使用的 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
中。