面试官:来谈谈Vue3的provide和inject实现多级传递的原理
前言没有看过provide和inject函数源码的小伙伴可能觉得他们实现数据多级传递非常神秘,其实他的源码非常简单,这篇文章欧阳来讲讲provide和inject函数是如何实现数据多级传递的。ps:本文中使用的Vue版本为3.5.13。
关注公众号:【前端欧阳】,给自己一个进阶vue的机会
看个demo
先来看个demo,这个是父组件,代码如下:
<template>
<template>
<GrandChild />
</template><ChildDemo />
</template>在父组件中使用provide为后代组件注入一个count响应式变量。
再来看看子组件child.vue代码如下:
<template>
<template>
<GrandChild />
</template><ChildDemo />
</template>从上面的代码可以看到在子组件中什么事情都没做,只渲染了孙子组件。
我们再来看看孙子组件grand-child.vue,代码如下:
从上面的代码可以看到在孙子组件中使用inject函数拿到了父组件中注入的count响应式变量。
provide函数
我们先来debug看看provide函数的代码,给父组件中的provide函数打个断点,如下图:
刷新页面,此时代码将会停留在断点处。让断点走进provide函数,代码如下:
function provide(key, value) {<template>
<template>
<GrandChild />
</template><ChildDemo />
</template>if (!currentInstance) {<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>if (!!(process.env.NODE_ENV !== "production")) {<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>warn$1(`provide() can only be used inside setup().`);<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>}<template>
<template>
<GrandChild />
</template><ChildDemo />
</template>} else {<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>let provides = currentInstance.provides;<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>const parentProvides = currentInstance.parent && currentInstance.parent.provides;<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>if (parentProvides === provides) {<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>provides = currentInstance.provides = Object.create(parentProvides);<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>}<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>provides = value;<template>
<template>
<GrandChild />
</template><ChildDemo />
</template>}}首先判断currentInstance是否有值,如果没有就说明当前没有vue实例,也就是说当前调用provide函数的地方是不在setup函数中执行的,然后给出警告provide只能在setup中使用。
然后走进else逻辑中,首先从当前vue实例中取出存的provides属性对象。并且通过currentInstance.parent.provides拿到父组件vue实例中的provides属性对象。
这里为什么需要判断if (parentProvides === provides)呢?
因为在创建子组件时会默认使用父组件的provides属性对象作为父组件的provides属性对象。代码如下:
const instance: ComponentInternalInstance = {<template>
<template>
<GrandChild />
</template><ChildDemo />
</template>uid: uid++,<template>
<template>
<GrandChild />
</template><ChildDemo />
</template>vnode,<template>
<template>
<GrandChild />
</template><ChildDemo />
</template>type,<template>
<template>
<GrandChild />
</template><ChildDemo />
</template>parent,<template>
<template>
<GrandChild />
</template><ChildDemo />
</template>provides: parent ? parent.provides : Object.create(appContext.provides),<template>
<template>
<GrandChild />
</template><ChildDemo />
</template>// ...省略} 从上面的代码可以看到如果有父组件,那么创建子组件实例的时候就直接使用父组件的provides属性对象。
所以这里在provide函数中需要判断if (parentProvides === provides),如果相等说明当前父组件和子组件是共用的同一个provides属性对象。此时如果子组件调用了provide函数,说明子组件需要创建自己的provides属性对象。
并且新的属性对象还需要能够访问到父组件中注入的内容,所以这里以父组件的provides属性对象为原型去创建一个新的子组件的,这样在子组件中不仅能够访问到原型链中注入的provides属性对象,也能够访问到自己注入进去的provides属性对象。
最后就是执行provides = value将当前注入的内容存到provides属性对象中。
inject函数
我们再来看看inject函数是如何隔了一层子组件从父组件中如何取出数据的,还是一样的套路,给孙子组件中的inject函数打个断点。如下图:
将断点走进inject函数,代码如下:
export function inject(<template>
<template>
<GrandChild />
</template><ChildDemo />
</template>key: InjectionKey | string,<template>
<template>
<GrandChild />
</template><ChildDemo />
</template>defaultValue?: unknown,<template>
<template>
<GrandChild />
</template><ChildDemo />
</template>treatDefaultAsFactory = false,) {<template>
<template>
<GrandChild />
</template><ChildDemo />
</template>// fallback to `currentRenderingInstance` so that this can be called in<template>
<template>
<GrandChild />
</template><ChildDemo />
</template>// a functional component<template>
<template>
<GrandChild />
</template><ChildDemo />
</template>const instance = currentInstance || currentRenderingInstance<template>
<template>
<GrandChild />
</template><ChildDemo />
</template>// also support looking up from app-level provides w/ `app.runWithContext()`<template>
<template>
<GrandChild />
</template><ChildDemo />
</template>if (instance || currentApp) {<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>const provides = currentApp<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>? currentApp._context.provides<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>: instance<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>? instance.parent == null<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>? instance.vnode.appContext && instance.vnode.appContext.provides<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>: instance.parent.provides<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>: undefined<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>if (provides && key in provides) {<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>return provides<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>} else if (arguments.length > 1) {<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>return treatDefaultAsFactory && isFunction(defaultValue)<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>? defaultValue.call(instance && instance.proxy)<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>: defaultValue<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>} else if (__DEV__) {<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>warn(`injection "${String(key)}" not found.`)<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>}<template>
<template>
<GrandChild />
</template><ChildDemo />
</template>} else if (__DEV__) {<template>
<template>
<GrandChild />
</template><ChildDemo />
</template><template>
<template>
<GrandChild />
</template><ChildDemo />
</template>warn(`inject() can only be used inside setup() or functional components.`)<template>
<template>
<GrandChild />
</template><ChildDemo />
</template>}}首先拿到当前渲染的vue实例赋值给本地变量instance。接着使用if (instance || currentApp)判断当前是否有vue实例,如果没有看看有没有使用app.runWithContext手动注入了上下文,如果注入了那么currentApp就有值。
接着就是一串三元表达式,如果使用app.runWithContext手动注入了上下文,那么就优先从注入的上下文中取出provides属性对象。
如果没有那么就看当前组件是否满足instance.parent == null,也就是说当前组件是否是根节点。如果是根节点就取app中注入的provides属性对象。
如果上面的都不满足就去取父组件中注入的provides属性对象,前面我们讲过了在inject函数阶段,如果子组件内没有使用inject函数,那么就会直接使用父组件的provides属性对象。如果子组件中使用了inject函数,那么就以父组件的provides属性对象为原型去创建一个新的子组件的provides属性对象,从而形成一条原型链。
所以这里的孙子节点的provides属性对象中当然就能够拿到父组件中注入的count响应式变量,那么if (provides && key in provides)就满足条件,最后会走到return provides中将父组件中注入的响应式变量count原封不动的返回。
还有就是如果我们inject一个没有使用provide存入的key,并且传入了第二个参数defaultValue,此时else if (arguments.length > 1)就满足条件了。
在里面会去判断是否传入第三个参数treatDefaultAsFactory,如果这个参数的值为true,说明第二个参数defaultValue可能是一个工厂函数。那么就执行defaultValue.call(instance && instance.proxy)将defaultValue的当中工厂函数的执行结果进行返回。
如果第三个参数treatDefaultAsFactory的值不为true,那么就直接将第二个参数defaultValue当做默认值返回。
总结
这篇文章讲了使用provide和inject函数是如何实现数据多级传递的。
在创建vue组件实例时,子组件的provides属性对象会直接使用父组件的provides属性对象。如果在子组件中使用了provide函数,那么会以父组件的provides属性对象为原型创建一个新的provides属性对象,并且将provide函数中注入的内容塞到新的provides属性对象中,从而形成了原型链。
在孙子组件中,他的parent就是子组件。前面我们讲过了如果没有在组件内使用provide注入东西(很明显这里的子组件确实没有注入任何东西),那么就会直接使用他的父组件的provides属性对象,所以这里的子组件是直接使用的是父组件中的provides属性对象。所以在孙子组件中可以直接使用inject函数拿到父组件中注入的内容。
关注公众号:【前端欧阳】,给自己一个进阶vue的机会
另外欧阳写了一本开源电子书vue3编译原理揭秘,看完这本书可以让你对vue编译的认知有质的提升。这本书初、中级前端能看懂,完全免费,只求一个star。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页:
[1]