【Vue】provide/inject实现组件通信及响应式数据更新
一、provide/inject实现组件通信
provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性。
inject 选项应该是:一个字符串数组,或一个对象
provide/inject是Vue.js2.2.0版本后新增的API:
provide:Object | () => Object//一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性。
inject:Array<string> | { [key: string]: string | Symbol | Object }//一个字符串数组,或一个对象
虽然官方文档说,provide和inject主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中,但是在插件 / 组件库(比如 iView,事实上 iView 的很多组件都在用)。不过建议归建议,如果你用好了,这个 API 会非常有用。
这对选项需要一起使用,以允许一个祖先组件向其所有的子孙后代注入一个依赖,不论组件的层次有多深,并在起上下游关系成立的时间里始终生效。
注意:provide和inject绑定并不是可响应的。这显然不是设计的失误,而是刻意的。
下面我们来看一看它最简单的用法:
//祖先级组件(上级组件)
<template>
<div>
<Pro></Pro>
</div>
</template>
<script>
import Pro from '../components/provide.vue';
export default {
data(){
return{
}
},
provide:{
foo:'test'
},
components:{
Pro,
}
}
</script>
<style scoped>
</style>
//子孙级组件(下级组件)
<template>
<div>
<p>{{foo}}</p>
</div>
</template>
<script>
export default {
data(){
return {
}
},
inject:['foo'],
}
</script>
<style scoped>
</style>
我们在上级组件中设置了一个provide:foo,值为test,它的作用就是将foo这个变量提供给它的所有下级组件。而在下级组件中通过inject注入了从上级组件中提供的foo变量,那么在下级组件中,就可以直接通过this.foo来访问了。
再次强调一遍,provide和inject绑定并不是可响应的,所以上述例子中上级组件的foo改变了,下级组件的this.foo的值还是不会改变的。
我们一般会在main.js中导入app.vue作为根组件,我们需要在app.vue上做文章,这就是我们实现功能的关键。我们可以这样理解:app.vue作为一个最外层的根组件,用来存储所有需要的全局数据和状态。因为项目中的所有组件(包含路由),它的父组件(或根组件)都是app.vue,所有我们可以把整个app.vue实例通过provide对外提供。那么,所有的组件都能共享其数据,方法等。
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
provide () {
return {
app: this
}
}
}
</script>
上面,我们把整个app.vue的实例`this`对外提供,接下来,任何组件(或路由)只要通过`inject`注入app.vue的话,都可以通过this.app.xxx的形式来访问app.vue的data,computed,method等内容。
app.vue是整个项目第一个被渲染的组件,而且只会渲染一次(即使切换路由,app.vue也不会被再次渲染),利用这个特性,很适合做一次性全局的状态数据管理,例如我们将用户的登录信息保存起来:
//app.vue,部分代码省略:
<script>
export default {
provide () {
return {
app: this
}
},
data () {
return {
userInfo: null
}
},
methods: {
getUserInfo () {
// 这里通过 ajax 获取用户信息后,赋值给 this.userInfo,以下为伪代码
$.ajax('/user/info', (data) => {
this.userInfo = data;
});
}
},
mounted () {
this.getUserInfo();
}
}
</script>
这样,任何页面或组件只要通过inject注入app后,就可以直接访问userInfo的数据了,比如:
<template>
<div>
{{ app.userInfo }}
</div>
</template>
<script>
export default {
inject: ['app']
}
</script>
是不是很简单呢。除了直接使用数据,还可以调用方法。比如在某个页面里,修改了个人资料,这时一开始在app.vue里获取的userInfo已经不是最新的了,需要重新获取。可以这样使用:
//某个页面:
<template>
<div>
{{ app.userInfo }}
</div>
</template>
<script>
export default {
inject: ['app'],
methods: {
changeUserInfo () {
// 这里修改完用户数据后,通知 app.vue 更新,以下为伪代码
$.ajax('/user/update', () => {
// 直接通过 this.app 就可以调用 app.vue 里的方法this.app.getUserInfo();
})
}
}
}
</script>
同样非常简单。只要理解了 `this.app` 是直接获取整个 `app.vue` 的实例后,使用起来就得心应手了。想一想,配置复杂的 Vuex 的全部功能,现在是不是都可以通过 `provide / inject` 来实现了呢?
如果你顾忌 Vue.js 文档中所说,provide / inject 不推荐直接在应用程序中使用,那没有关系,仍然使用你熟悉的 Vuex 或 Bus 来管理你的项目就好。我们介绍的这对 API,主要还是在独立组件中发挥作用的。
只要一个组件使用了 `provide` 向下提供数据,那其下所有的子组件都可以通过 `inject` 来注入,不管中间隔了多少代,而且可以注入多个来自不同父级提供的数据。需要注意的是,一旦注入了某个数据,比如上面示例中的 `app`,那这个组件中就不能再声明 `app` 这个数据了,因为它已经被父级占有。
二、mixins
如果你的项目足够复杂,或需要多人协同开发时,在app.vue里会写非常多的代码,多到结构复杂难以维护。这时可以使用 Vue.js 的混合mixins,将不同的逻辑分开到不同的 js 文件里。
我先简单介绍一下什么是mixins:
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。(个人理解mixins就是定义一部分公共的方法或者计算属性,然后混入到各个组件中使用,方便管理与统一修改)
比如上面的用户信息,就可以放到混合里:
//新建文件(user.js)
export default {
data () {
return {
userInfo: null
}
},
methods: {
getUserInfo () {
// 这里通过 ajax 获取用户信息后,赋值给 this.userInfo,以下为伪代码
$.ajax('/user/info', (data) => {
this.userInfo = data;
});
}
},
mounted () {
this.getUserInfo();
}
}
然后在app.vue中混合:
<script>
import mixins_user from'../mixins/user.js';
export default {
mixins: [mixins_user],
data () {
return {
}
}
}
</script>
这样,跟用户信息相关的逻辑,都可以在user.js里维护,或者由某个人来维护,app.vue也就很容易维护了。
要深入了解混入请参照官方文档:https://cn.vuejs.org/v2/guide/mixins.html
三、provide/inject实现响应式数据更新
3.1问题
前面的例子中,provide和inject绑定并不是可响应的,所以上述例子中上级组件的foo改变了,下级组件的this.foo的值还是不会改变的,但实际使用的时候希望父组件的属性修改后,子组件能监听到属性的修改并执行一些逻辑。
首先假设我们在祖辈时候传入进来是个动态的数据,官方不是说如果你传入了一个可监听的对象,那么其对象还是可响应的么?
parent父页面:
export default { provide() { return { foo: this.fonnB } }, data(){ return { fonnB: 'old word' } } created() { setTimeout(()=>{ this.fonnB = "new words"; // 这里foo变化了,但子组件获得的foo 依旧是old words },1000) }, }
child子页面:
export default { inject:['foo'], data(){ return { childfooOld: this.foo } }, computed:{ chilrdfoo() { return this.foo } }, created () { console.log(this.foo) // -> 'old word' setTimeout(() => { console.log(this.chilrdfoo); // 这里计算属性依旧是old words }, 2000); } }
结果:
通过上面方式,经过验证,子组件页面都没办法实现响应更新this.foo的值。
3.2实现响应式数据更新:
方法1:
将一个函数赋值给provide的一个值,这个函数返回父组件的动态数据,然后在子孙组件里面调用这个函数。实际上这个函数存储了父组件实例的引用,所以每次子组件都能获取到最新的数据。代码长下面的样子:
Parent组件:
<template> <div class="parent-container"> Parent组件 <br/> <button type="button" @click="changeName">改变name</button> <br/> Parent组件中 name的值: {{name}} <Child v-bind="{name: 'k3vvvv'}" /> </div> </template> <style scoped> .parent-container { padding: 30px; border: 1px solid burlywood; } </style> <script> import Child from './Child' export default { name: 'Parent', data () { return { name: 'Kevin' } }, methods: { changeName (val) { this.name = 'Kev' } }, provide: function () { return { nameFromParent: this.name, getReaciveNameFromParent: () => this.name } }, // provide: { // nameFromParent: this.name, // getReaciveNameFromParent: () => this.name // }, components: { Child } } </script>
Child组件:
<template> <div class="child-container"> Child组件 <br/> <GrandSon /> </div> </template> <style scoped> .child-container { padding: 30px; border: 1px solid burlywood; } </style> <script> import GrandSon from './GrandSon' export default { components: { GrandSon } } </script>
GrandSon组件:
<template> <div class="grandson-container"> Grandson组件 <br/> {{nameFromParent}} <br/> {{reactiveNameFromParent}} </div> </template> <style scoped> .grandson-container { padding: 30px; border: 1px solid burlywood; } </style> <script> export default { inject: ['nameFromParent', 'getReaciveNameFromParent'], computed: { reactiveNameFromParent () { return this.getReaciveNameFromParent() } }, watch: { 'reactiveNameFromParent': function (val) { console.log('来自Parent组件的name值发生了变化', val) } }, mounted () { console.log(this.nameFromParent, 'nameFromParent') } } </script>
结果:
来自于reactiveNameFromParent ,随着祖先组件变化而变化了
方法2:
因此我们给provide的属性传入一个可监听的"对象"fonnB,那么fonnB的属性是可响应的:
parent页面:
export default { provide(){ return {foo:this.fonnB} }, data(){ return { fonnB:{a:'old word'} } } created() { setTimeout(()=>{ this.fonnB.a="new words"; },1000) }, }
child页面:
export default { inject:['foo'], data(){ return { childfooOld:this.foo.a } }, computed:{ chilrfoo(){ return this.foo.a } } }
转自:https://blog.csdn.net/liuhua_2323/article/details/94780849
响应式参考: