Vue
中父组件到子组件的通信主要由子组件的props
属性实现。但是在一些情况下,父组件无法直接向子组件的props
传值。比如子组件通过父组件的slot
进入父组件,父组件根本不知道子组件是谁,更不用说用子组件的props
了。这时应该怎么办呢?Vue
在2.2.0
版本引入了provide
与inject
,正好适合处理这一情况。
什么是provide与inject
用文档 的话说:
provide
/inject
需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。
这就是说从父组件的provide
属性传入一个对象,子组件(或者是孙组件,只要是子级组件)可以用inject
属性接收父组件的provide
属性。比如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 // main.vue <template > <c1 message ="hello world" > <c2 > </c2 > </c1 > </template > // c1.vue <template > <div id ="c1" > <slot > </slot > </div > </template > <script > export default { props : ['message' ], provides () { return { message : this .message } } } </script > // c2.vue <template > <div id ="c2" > {{ message }} </div > </template > <script > export default { inject : ['message' ] } </script >
上面的main
组件会被渲染为:
1 2 3 <div id ="c1" > <div id = "c2" > hello world</div > </div >
可以看到,c1
组件在不清楚子组件是什么的情况下,将它的props
中的message
传给了c2
组件。在这里c1
组件就像是一个数据源一样,为子组件提供数据。但是,c1
组件提供的数据仅在c1
的子孙组件中可见,因此可以算作是有作用域限定的数据源。
父到子孙组件方向的数据流
父到子孙组件方向是provide/inject
机制设计时的数据流方向。我们可能会猜想,在父组件中更改provide
的值,子组件会响应式的发生改变。但是注意到文档中话。
提示:provide
和inject
绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
这意味着,如果provide
的值不是可监听对象时,在父组件中更改provide
的值,子组件不会发生任何变化。比如模板仍然为上面那个例子的模板,message
的值是一个props
属性,不是可监听对象,如果我们在c1
的mounted
钩子函数里改变message
的值。如:
1 2 3 4 5 6 7 8 9 10 11 // c1.vue <script > export default { mounted () { setTimeout ( () => { this .message = 'Opps, it would not be rendered' }, 1000 ) } } </script >
子组件不会响应修改后的值。
但是如果provide
的值是一个可监听对象呢?请看一下例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <script > export default { data () { return { message : 'hello world' } }, provide () { messageData : this .$data }, mounted () { setTimeout (() => { this .message = 'I can show in c2.' }, 10000 ) } } </script > // c2.vue <template > <div id ="c2" > {{ messageData.message }} </div > </template > <script > export default { inject : ['messageData' ] } </script >
此时在c1
挂载10s后,子组件将会显示I can show in c2
。为什么呢?c2
中messageData
实际上就是c1
实例的this.$data
。而this.$data
上有message
的响应式getter
与setter
。所以c2
的视图会被message
的dep
收集,因此在c1
中更新message
,c2
的视图也会更新。如果对此处不熟悉,可以看一看关于Vue
数据绑定的文章。
Vue的源码实现
首先我们来看provide
与inject
的初始化,在src/core/instance/init.js
里,我们能够看到它们初始化的过程:
1 2 3 4 5 6 7 8 9 10 Vue .prototype ._init = function (options?: Object ) { callHook (vm, 'beforeCreate' ) initInjections (vm) initState (vm) initProvide (vm) callHook (vm, 'created' ) }
可以看到在一个组件内,initInjections
是先于initProvide
调用的,但是从整个组件树的初始化顺序来看,父组件的initProvide
的调用要先于子组件的initInjections
。为了理解上的方便,我们先来看initProvide
。
initProvide
initProvide
定义在src/core/instance/inject.js
中:
1 2 3 4 5 6 7 8 9 export function initProvide (vm : Component ) { const provide = vm.$options .provide if (provide) { vm._provided = typeof provide === 'function' ? provide.call (vm) : provide } }
initProvide
函数很简单,就是将组件的provide
对象放到vm._provided
中。这里兼顾了provide
为函数与为对象两种情况。与文档中所述的一致:
类型:
provide:Object | () => Object
注意到,initProvide
方法中只是进行了简单的复制。在大多数情况下,如果要把父级的响应式属性作为provide
,此时只有值被复制进去。Vue并没有对_provided
属性做响应式处理(熟悉源码的同学应该知道defineReactive
方法)。因此,provide
是非响应式的。
initInjections
initInjections
定义同一个文件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 export function initInjections (vm : Component ) { const result = resolveInject (vm.$options .inject , vm) if (result) { toggleObserving (false ) Object .keys (result).forEach (key => { if (process.env .NODE_ENV !== 'production' ) { defineReactive (vm, key, result[key], () => { warn ( `Avoid mutating an injected value directly since the changes will be ` + `overwritten whenever the provided component re-renders. ` + `injection being mutated: "${key} "` , vm ) }) } else { defineReactive (vm, key, result[key]) } }) toggleObserving (true ) } }
这里出现了一个resolveInject
函数,还是在这个文件里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 export function resolveInject (inject : any, vm : Component ): ?Object { if (inject) { const result = Object .create (null ) const keys = hasSymbol ? Reflect .ownKeys (inject).filter (key => { return Object .getOwnPropertyDescriptor (inject, key).enumerable }) : Object .keys (inject) for (let i = 0 ; i < keys.length ; i++) { const key = keys[i] const provideKey = inject[key].from let source = vm while (source) { if (source._provided && hasOwn (source._provided , provideKey)) { result[key] = source._provided [provideKey] break } source = source.$parent } if (!source) { if ('default' in inject[key]) { const provideDefault = inject[key].default result[key] = typeof provideDefault === 'function' ? provideDefault.call (vm) : provideDefault } else if (process.env .NODE_ENV !== 'production' ) { warn (`Injection "${key} " not found` , vm) } } } return result } }
实际上,resolveInject
是实现provide/inject
的核心函数。它从父/祖父组件中把provide
的值捕捉下来,之后initInjections
利用这一结果,在自身组件上定义响应式属性。注意到在定义响应式属性之前,toggleObserving(false)
。这意味着inject
的值里面是没有__ob__
的。也就是说,当更改inject
的值会触发视图的改变 ,而更改inject
对象的属性不会触发视图改变 。当然,最佳实践是不要在子组件里更改inject
。
纵观整个过程,provide/inject
机制是非响应式的,即provide
与inject
之间没有绑定。具体的值是在子组件初始化 过程中决定的。
总结
provide/inject
提供了一种新的组件间通信的方法。它允许父组件向子孙组件间进行跨层级的数据分发。但是provide/inject
是非响应式的,如果要子孙组件根据父组件的值进行改变,provide/inject
机制不是一个好的选择。此时可以使用Vuex
来管理状态。
参考资料
API - Vue.js
Vue源码分析之Observer