前言
provide/inject在面试中,偶尔会问到,它通常在回答数据通信的内容中提到,那么它是怎么实现的呢?一起来分析源码吧。
官方定义:provide/inject这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。
provide/inject基本使用
在分析源码之前,先看下其基本使用,其实一般业务开发代码中比较少用到provide/inject,在写一些库的时候才会用到,那么看看基本使用方法吧:
index.html
html
<section id="app">
<button @click="plus">+1</button>
<div id="count">count:{{ count }}</div>
<display-count></display-count>
</section>
<script src="../../dist/vue.js"></script>
<script src="app.js"></script>
app.js
js
const store = {}
const DisplayCount = {
name: 'DisplayCount',
template: `
<div>
DisplayCount: {{$store.state.count}}
</div>
`,
inject: ['$store'],
data() {
return {
}
},
}
var app = new Vue({
name: 'SimpleDemoAPI_Provide_Inject',
components: {
'display-count': DisplayCount
},
provide: {
$store: store
},
data() {
return {
count: 0
}
},
created() {
store.state = this.$data
},
methods: {
plus() {
this.count += 1;
}
}
})
效果:

思维导图
provide/inject链路图:

分析源码
初始化vue的代码片段
源码位置:src\core\instance\init.js
js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
// ...
initInjections(vm) // 初始化inject,读取provide的值,绑定到组件实例中
// ...
initProvide(vm) // 初始化 provide,原理:把options.provide挂载到vm._provide,inject时读取值,绑定到组件实例中
// ...
}
}
provide/inject的实现代码片段
源码位置:src\core\instance\inject.js
js
/* @flow */
import { hasOwn } from 'shared/util'
import { warn, hasSymbol } from '../util/index'
import { defineReactive, toggleObserving } from '../observer/index'
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
export function initInjections (vm: Component) {
// 根据inject的提供的key,寻找provide中对应的值
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false)
Object.keys(result).forEach(key => {
/* istanbul ignore else */
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 {
// 将inject的key对应的provide的值,绑定到vue/vue组件实例上
defineReactive(vm, key, result[key])
}
})
toggleObserving(true)
}
}
// 根据inject的提供的key,寻找provide中对应的值
export function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
// inject is :any because flow is not smart enough to figure out cached
const result = Object.create(null)
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// #6574 in case the inject object is observed...
if (key === '__ob__') continue
const provideKey = inject[key].from
let source = vm
// 遍历找父级provide
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
// 找到父级的provide的属性,放到result,再返回
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
}
}
从源码上看,可以看出provide/inject的实现很简单,代码量也很少,几十行就搞定了。
下面我们来分析代码逻辑,结合上面的链路图一起看,
- 初始化Vue,首先初始化
inject
,如果是根组件,则是没有inject
的,子组件才会有inject
,继续往下走 - 初始化
provide
,调用initProvide()
,很简单,直接把provide
的值赋值给组件的_provide
属性,保存起来,等待子组件inject读取其值 - 子组件开始初始化
inject
,调用initInjections()
,先把调用resolveInject()
把inject
解析出来,再调用解析出来的值,根据对应的key取到值,再调用defineReactive()
把inject
的key定义到组件实例中(defineReactive()
就是调用Object.defineProperty()
),至此provide/inject
就实现了。 - 那么
resolveInject()
是怎么解析inject的呢?很简单,遍历inject
的key,同时遍历当前组件实例中父级组件的provide
属性,如果就返回就ok了,查找provide
的过程其实就是数据结构中链表的查找的操作。
总结
provide/inject实现原理很简单:
组件初始化时先初始化inject注入的值,初始化的过程就是去查找父组件中的provide的值,再把对应的键和值通过Object.defineProperty把键值定义到当前组件中,所以组件可以通过this访问到inject注入的值。