依赖注入
# 题目:
首先来看到这个题目的描述,这里说的是我们要使用 组合式 API: 依赖注入 来完成这个挑战。
# 分析:
我们看到官方文档,首先要了解什么是依赖注入,通过 inject 这个 api 我们可以注入一个由祖先组件或整个应用 (通过 app.provide()) 提供的值,那么理所当然就会有 provide 这个 api 提供一个值给后代组件注入。
// App.vue
<script setup lang="ts">
import { ref, provide } from "vue"
import Child from "./Child.vue"
const count = ref(1)
provide("count", count)
setInterval(() => {
count.value++
}, 1000)
</script>
<template>
<Child />
</template>
可以看到官方文档对于provide的描述, 我们可以看到,provide 接收两个参数,第一个参数是 key,第二个参数是 value。提供一个值给后代组件注入。
# 解决:
import { inject } from "vue";
const count = inject("count");
所以这里要解决我们的挑战就简单明了,直接用 inject 将count
值注入子组件。
# 思考
provide/inject的实现原理? 要了解 provide/inject 的实现原理,我们首先需要了解原型链。
# 原型链
prototype与__proto__
prototype
一般被称为显示原型,__proto__
一般被称为隐式原型。每个函数在创建后都会默认有一个prototype
属性,这个属性表示函数的原型对象。
概念
当我们试图访问JS对象中的某个属性时,JS会先在这个对象定义的属性里面找,如果该对象本身没有这个属性,那么就会去它的__proto__
上找也就是它构造函数的prototype
属性,如果这个对象也没有,那么就会去它的__proto__
的__proto__
上找,直到找到Object.prototype
为止。Object.prototype
的隐式原型指向 null
为结束。听起来是不是有点晦涩难懂,那我们通过一个例子来理解吧。
举例
function Fn() {}
Fn.prototype.name = 'Xwen'
let fn1 = new Fn()
fn1.age = 18
console.log(fn1.name) // Xwen
console.log(fn1.age) // 18
这里我们来慢慢分析,Fn是一个构造函数,fn1是Fn函数new的一个实例对象。那么根据原型链的知识,当我们输出fn1.name时,JS会先在fn1对象中找,没有找到,就会去它构造函数的prototype
中找,找到了,输出Xwen。同理当输出fn1.age时,JS会先在fn1对象中找,这里找到了就输出18。
function Fn() {}
Fn.prototype.name = 'Xwen'
let fn1 = new Fn()
fn1.name = 'XwenHaHa'
console.log(fn1.name) // XwenHaHa
这里我们打印fn1.name时,JS会先在fn1对象中找,这里是直接找到了就输出XwenHaHa。不会走构造函数的prototype
。
# provide原理
那么温习完原型链的知识,我们来看看Vue3的provide是如何实现的provide源码 (opens new window)。
export function provide<T, K = InjectionKey<T> | string | number>(
key: K,
value: K extends InjectionKey<infer V> ? V : T
) {
if (!currentInstance) {
if (__DEV__) {
warn(`provide() can only be used inside setup().`)
}
} else {
// 获取当前组件实例上provides属性
let provides = currentInstance.provides
// 获取当前父级组件的provides属性
const parentProvides =
currentInstance.parent && currentInstance.parent.provides
// 如果当前的provides和父级的provides相同则说明还没赋值
if (parentProvides === provides) {
// Object.create() es6创建对象的另一种方式,可以理解为继承一个对象, 添加的属性是在原型下。
provides = currentInstance.provides = Object.create(parentProvides)
}
provides[key as string] = value
}
}
那么这里我们分析下就知道,provide Api就是通过获取当前组件实例上的provides
属性,然后将外部传进来的key和value赋值给当前组件实例上的provides
。并且通过Object.create()
这个Api将父级组件的provides
属性设置到当前的组件实例对象的provides
属性的原型对象上。
# inject原理
接下来我们看看inject是如何实现的inject源码 (opens new window)
export function inject(
key: InjectionKey<any> | string,
defaultValue?: unknown,
treatDefaultAsFactory = false
) {
// 获取当前组件实例对象
const instance = currentInstance || currentRenderingInstance
if (instance || currentApp) {
// 如果intance位于根目录下,则返回到appContext的provides,否则就返回父组件的provides
const provides = instance
? instance.parent == null
? instance.vnode.appContext && instance.vnode.appContext.provides
: instance.parent.provides
: currentApp!._context.provides
if (provides && (key as string | symbol) in provides) {
return provides[key as string]
} else if (arguments.length > 1) {
// 如果存在1个参数以上
return treatDefaultAsFactory && isFunction(defaultValue)
// 如果默认内容是个函数的,就执行并且通过call方法把组件实例的代理对象绑定到该函数的this上
? defaultValue.call(instance && instance.proxy)
: defaultValue
} else if (__DEV__) {
warn(`injection "${String(key)}" not found.`)
}
} else if (__DEV__) {
warn(`inject() can only be used inside setup() or functional components.`)
}
}
通过分析,我们可以看到inject
Api就是通过获取当前组件实例对象,然后判断当前组件实例对象是否存在,然后判断是否根组件,如果是根组件则返回到appContext
的provides
,否则就返回父组件的provides
。
如果当前获取的key
在provides
上有值,那么就返回该值,如果没有则判断是否存在默认内容,默认内容如果是个函数,就执行并且通过call
方法把组件实例的代理对象绑定到该函数的this上,否则就直接返回默认内容。