依赖注入
当深层嵌套的子孙组件想要拿到父组件的数据时,我们可以使用provide和inject。官网原理图:
使用案例: 我们假设组件嵌套结构如下:
Root
└─ TodoList
├─ TodoItem
└─ TodoListFooter
├─ ClearTodosButton
└─ TodoListStatistics
我们通过provide和inject将组件TodoList的属性直接传给组件TodoListStatistics:
const app = Vue.createApp({})
app.component('todo-list', {
data() {
return {
todos: ['张三', '李四']
}
},
provide: { // provide是一个对象
name: '王五'
},
template: `
<div>
{{ todos.length }}
</div>
`
})
app.component('todo-list-statistics', {
inject: ['name'],
created() {
console.log(`Injected 属性: ${this.name}`) // > Injected 属性: 王五
}
})
上例中的provide定义为一个对象。如果需要在provide里使用data中的属性,需要把provide定义成一个方法,否则会报错。
app.component('todo-list', {
data() {
return {
todos: ['张三', '李四']
}
},
provide() { // provide是一个function
return {
todoLength: this.todos.length
}
},
template: `
...
`
})
依赖注入的优缺点如下:
优点:
- 祖先组件不需要知道哪些后代组件使用它提供的数据;
- 后代组件不需要知道被注入的数据来自哪里; 缺点:
- 组件间的耦合较为紧密,不易重构;
- 提供的属性是非响应式的;解决方案见官方文档
源码解读
了解了inject和provide的使用后,我们来深层解读下源码实现。废话不多说,直接上源码:
组件实例初始化的时候会调用Vue.prototype._init,该文件中,可以了解到:
- inject、provide的初始化时间在生命周期钩子函数beforeCreate之后,created之前;
- initInjections(vm) 解析inject是在初始化data/props之前;
- initProvide(vm) 解析provide是在初始化data/props之后;
这也符合数据初始化的一个处理逻辑。
Vue.prototype._init源码:
//初始化inject和provide
// expose real self
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函数,功能是获取组件注册的inject属性合集,然后遍历合集进行响应式监听。 源码如下:
export function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm) // 1、根据注册的inject,通过$parent向上查找对应的provide
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 {
defineReactive(vm, key, result[key]) // 2、进行响应式监听
}
})
toggleObserving(true)
}
}
resolveInject函数,功能是通过$parent一层层向上查找祖先节点的数据,直到找到对应于inject的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++) { // 遍历所有inject为其赋值
const key = keys[i]
// #6574 in case the inject object is observed...
if (key === '__ob__') continue
const provideKey = inject[key].from
let source = vm
while (source) { // 核心原理:通过$parent一层一层向上查找祖先节点的provide,找到则对inject进行赋值
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
}
}
initProvide函数,该方法单纯把组件注册的provide值,赋值给vm._provided,resolveInject中有使用到。
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
总结
依赖注入,其核心原理就是通过$parent向上查找祖先组件中的provide,找到则赋值给对应的inject即可。 仔细一思量,老铁们会发想,依赖注入原理和JavaScript中的instanceof操作符原理有异曲同工之处。在instanceof中,通过__proto__向原型链中查找,如果__proto__与构造函数的prototype相等则返回true。哈哈哈哈哈,这就是研究原理的有趣之处。