之前我写过一篇关于如何在Vue2中实现Teleport组件的文章,还没看的小伙伴可以去看看。最近我又发现,这样的实现无法继承全局定义的provide,接下来我们就来讨论下如何实现继承provide。
Demo
首先来看一个demo:
- 第一步定义
Provider
组件,提供一个全局的 provide 数据
<!-- Provider.vue -->
<template>
<div>
<slot></slot>
</div>
</template>
<script lang="ts" setup>
import { provide } from 'vue'
provide('message', 'hello')
</script>
- 定义
Child
组件并消费Provider
提供的数据
<!-- Child.vue -->
<template>
<div>Child</div>
</template>
<script setup lang="ts">
import { inject } from 'vue'
const msg = inject('message')
console.log('child', msg)
</script>
- 定义
Parent
组件,在Template
中将Child
组件包裹在Teleport
组件下
<!-- Parent.vue -->
<template>
<Teleport to="body">
<Child />
</Teleport>
</template>
<script setup lang="ts">
import { inject } from 'vue'
import { Teleport } from './Teleport.tsx'
import Child from './Child.vue'
const msg = inject('message')
console.log('parent', msg)
</script>
- 定义
App.vue
将上面的组件组合起来
<!-- App.vue -->
<script setup lang="ts">
import Provider from './demo/teleport/Provider.vue'
import Parent from './demo/teleport/Parent.vue'
</script>
<template>
<Provider>
<Parent />
</Provider>
</template>
<style>
table {
background-color: #333;
color: #fff;
}
</style>
最终在控制台你会发现 Child
组件无法正确的获取传递过来的 message
。这是因为自定义的 Teleport
组件无法继承 provide
导致的,而同样的 demo
使用 Vue3
则同时会打印 parent hello
child hello
。
如何解决
那么该如何解决呢?
首先可以排除在 Teleport
组件中使用 inject
来实现,因为作为组件你无法知道在应用中会提供哪些 key
。
虽然直接使用 inject
行不通,但是我们可以参考下 inject
的源码:
function inject(key, defaultValue, treatDefaultAsFactory) {
if (treatDefaultAsFactory === void 0) { treatDefaultAsFactory = false; }
// fallback to `currentRenderingInstance` so that this can be called in
// a functional component
var instance = currentInstance;
if (instance) {
// #2400
// to support `app.use` plugins,
// fallback to appContext's `provides` if the instance is at root
var provides = instance.$parent && instance.$parent._provided;
if (provides && key in provides) {
// TS doesn't allow symbol as index type
return provides[key];
}
else if (arguments.length > 1) {
return treatDefaultAsFactory && isFunction(defaultValue)
? defaultValue.call(instance)
: defaultValue;
}
else if (process.env.NODE_ENV !== 'production') {
warn$2("injection \"".concat(String(key), "\" not found."));
}
}
else if (process.env.NODE_ENV !== 'production') {
warn$2("inject() can only be used inside setup() or functional components.");
}
}
从上面的代码中不难看出 instance.$parent._provided
就是应用中 provide
传递的所有数据。那么我们只需要把这个对象再通过 provide
选项传递到 Teleport
组件内部的 new Vue
上就可以了。
直接在组件内部获取私有属性是比较丑陋的做法,而且每次需要获取时都要写这么一段丑陋的代码,显然并不合适。我们可以封装一个自定义hook:
import { getCurrentInstance } from 'vue'
export function useProvides<T>(): Record<symbol | string, T> {
const vm = getCurrentInstance()?.proxy
if (!vm || !vm.$parent) return {}
return (vm.$parent as any)._provided || {}
}
ok!接下来我们只需要在 Teleport
组件内部加上这么一段就大功告成了!
import Vue, { defineComponent, onMounted, onUnmounted, onUpdated } from 'vue';
import type { PropType, VueConstructor } from 'vue'
import { useProvides } from './useProvides'
export const Teleport = defineComponent({
props: {
to: {
type: [String, Object] as PropType<string | HTMLElement>,
default: () => document.body,
}
},
setup(props, { slots }) {
const container = typeof props.to === 'string'
? document.querySelector(props.to)
: props.to
let div: HTMLDivElement | null = document.createElement('div')
container?.appendChild(div)
let vm: InstanceType<VueConstructor> | null = null;
// 获取 provide
const provide = useProvides();
onMounted(() => {
if (div) {
vm = new Vue({
el: div,
// 重新传递 provide
provide: () => provide,
render: () => {
return slots.default?.()[0]
}
})
}
})
onUnmounted(() => {
if (vm) {
vm.$destroy()
div && document.body.removeChild(div)
div = null
vm = null
}
})
onUpdated(() => {
vm?.$forceUpdate();
})
return () => null
}
})
总结
总体来说要实现这个功能并不难,主要是要善于利用源码。