Vue中跨多层级组件传递数据,可使用provide和inject。从provide和inject字面理解,类似于依赖注入,但这种模式使用起来太碎片化,缺乏智能提示,子组件根本不知道父级组件到底通过provide提供了哪些数据。
例如App.vue
提供了key为name、version的provide:
import { ref, provide } from 'vue'
const count = ref(0)
provide('name', 'useContext')
provide('version', '1.0.0')
子组件使用inject获取配置:
import { inject } from 'vue'
const name = inject('name', '')
const version = inject('version', '0.0.1')
这种使用方式的缺点:provide使用显得碎片化,子组件不能感知到上级节点到底提供了哪些注入信息。
那么问题来了:能不能像React一样使用createContext、useContext方式通过高阶组件注入信息?
先看下React
是如何使用useContext的:
import { createContext, useContext } from 'react';
// 使用createContext创建一个Context,其默认provider为`{}`对象
const ThemeContext = createContext({});
export default function MyApp() {
return (
// 使用Provider高阶组件提供value
<ThemeContext.Provider value={ name: 'use-context' }>
<Panel />
</ThemeContext.Provider>
)
}
function Panel({ title, children }) {
// 通过useContext获取高阶组件传递的数据
const theme = useContext(ThemeContext);
const className = 'panel-' + theme;
return (
<section className={className}>
<h1>{title}</h1>
{children}
</section>
)
}
功能实现的几个关键信息:
- 使用createContext创建Context上下文对象
- Context上下文包含Provider高阶组件
- 使用useContext获取注入的数据
- 支持数据更新
先看效果:Vue版useContext Demo:
- 定义State和ThemeContext:
// context.ts文件
import { createContext } from '@vueuse/core'
export interface ThemeState {
type: string;
}
export const ThemeContext = createContext<ThemeState>()
2. 在容器组件中像React一样使用Provider高阶组件:
// App.vue
<script lang="ts" setup>
import { ThemeContext, ThemeState } from './context'
import Child from './Child.vue'
import { Ref, ref } from 'vue';
const themeState: Ref<ThemeState> = ref({ type: 'light' })
</script>
<template>
<ThemeContext.Provider :provider="themeState">
<Child></Child>
</ThemeContext.Provider>
</template>
3. 在子组件中读取、更新state:
// Child.vue
<script lang="ts" setup>
import { useContext } from '@vueuse/core'
import { ThemeContext } from './context'
const { state, setState } = useContext(ThemeContext)
</script>
<template>
<div :class="['container', `theme-${state.type}`]">
<span>当前主题:{{ state.type }}</span>
<button @click="setState({ type: state.type === 'dark' ? 'light' : 'dark' })">切换主题</button>
</div>
</template>
<style scoped>
.theme-light {
background-color: #777;
color: #000;
}
.theme-dark {
background-color: #333;
color: #fff;
}
</style>
实现效果如下,点击按钮,调用setState函数,主题在dark、light之间切换, 而state作为响应式对象实时更新。
实现源码
createContext高阶组件Provider
先看createContext
函数签名,defaultValue
为注入value的缺省值,返回Context<T>
类型。
export interface Context<T> {
[x: string]: any
Provider: Component<T>
setState: (state: T) => void
}
function createContext<T>(defaultValue?: T): Context<T>;
Context<T>
提供了Provider和setState,Provider为Vue组件,如:
<Context.Provider :provider="{...}"></Context.Provider>
而setState为数据更新函数。
function createContext<T>(defaultValue?: T): Context<T> {
const { injectionKey, Provider, setState } = createContextProvider(defaultValue)
const context = {
_injectionKey: injectionKey,
Provider,
setState,
} as Context<T>
return context
}
createContext实现也就几行代码,调用createContextProvider
函数返回三个属性:
- injectionKey:为
provide(key, value)
中的key; - Provider:为支持数据透传的高阶组件;
- setState:用于数据更新;
接下来看createContextProvider
函数实现:
import { defineComponent, isRef, provide, ref } from 'vue-demi'
export function createContextProvider<T>(defaultValue: T) {
const injectionKey = Symbol('')
// 使用类型为ref的存储注入信息
const state = ref<T>(defaultValue)
// 动态定义Component,组件提供provider属性
const Provider = defineComponent({
props: ['provider'],
setup(props, { slots }) {
const originalValue = props.provider || state.value
// 如果原始值为Ref类型,则解构
state.value = isRef(originalValue) ? originalValue.value : originalValue
provide(injectionKey, state)
return () => {
if (slots.default) {
return slots.default()
}
}
},
})
// 数据更新函数
const setState = (value: T) => {
state.value = value
}
return {
injectionKey,
Provider,
setState,
}
}
定义类型为Ref<T>
的state,用于保存从Provider组件传入的数据,并在setState对其更新, 使用类型Ref的目的是支持响应式。
Provider是通过defineComponent
函数动态生成的组件,当执行setup
时:
- 先将传入的
props.provider
赋值给state.value,如果原始值是Ref类型,则将其解构,其目的是将数据当做plain object使用。 - 然后调用
provide
函数将key为injectionKey的state注入到组件,便于后续通过inject
获取。 - 最后返回默认插槽内容,也就是子组件。
createContextProvider
函数除了返回Provider、setState外,还返回内部使用的injectionKey,其目的是提供给给后续的useContext
函数获取注入的数据。
useContext函数,获取透传数据
function useContext<T>(context: Context<T>): { state: Ref<T>, setState: ((value: T) => void) } {
const { setState } = context
return {
state: inject(context._injectionKey) as Ref<T>,
setState: (value: T) => setState?.(value),
}
}
useContext
函数接受的参数为上文定义的Context<T>
类型,返回信息包含state
对象、setState
函数,其中state调用inject(context._injectionKey)
函数后区,而_injectionKey
即为上文中createContext
返回的injectionKey字段。
一般在子组件中调用useContext
函数获取state数据,例如:
const { state, setState } = useContext(ThemeContext)
对外API:createContext、useContext
上文介绍的都是内部实现逻辑,而提供给研发使用的仅需要createContext和useContext两个函数即可。
// index.ts
export {
createContext,
useContext,
}
在使用state时,由于其类型已知,因此能感知到包含的属性,这样也解决了不能感知的问题。
下步计划:将useContext提交给vueuse
useContext基于vueuse
实现,也是按vueuse
要求的格式编写,包含markdown、test、demo。接下来打算将其提交给vueuse
,下次给面试官吹牛,俺也是开源贡献者😄😄😄😄😄😄。
什么是vueuse?可参考《Vue无处不use的VueUse: Composition工具集,代码减半神器!》了解。
完整代码
- index.ts:
import type { Component, Ref } from 'vue-demi'
import { inject } from 'vue-demi'
import { createContextProvider } from './provider'
export interface Context<T> {
[x: string]: any
Provider: Component<T>
setState: (state: T) => void
}
function createContext<T>(defaultValue?: T): Context<T> {
const { injectionKey, Provider, setState } = createContextProvider(defaultValue)
const context = {
_injectionKey: injectionKey,
Provider,
setState,
} as Context<T>
return context
}
function useContext<T>(context: Context<T>): { state: Ref<T>, setState: ((value: T) => void) } {
const { setState } = context
return {
state: inject(context._injectionKey) as Ref<T>,
setState: (value: T) => setState?.(value),
}
}
export {
createContext,
useContext,
}
- provider.ts:
import { defineComponent, isRef, provide, ref } from 'vue-demi'
export function createContextProvider<T>(defaultValue: T) {
const injectionKey = Symbol('')
const state = ref<T>(defaultValue)
const Provider = defineComponent({
props: ['provider'],
setup(props, { slots }) {
const originalValue = props.provider || state.value
state.value = isRef(originalValue) ? originalValue.value : originalValue
provide(injectionKey, state)
return () => {
if (slots.default) {
return slots.default()
}
}
},
})
const setState = (value: T) => {
state.value = value
}
return {
injectionKey,
Provider,
setState,
}
}
我是
前端下饭菜
,原创不易,各位看官动动手,帮忙关注、点赞、收藏、评轮!