Vue中跨多层级组件传递数据,可使用provide和inject。从provide和inject字面理解,类似于依赖注入,但这种模式使用起来太碎片化,缺乏智能提示,子组件根本不知道父级组件到底通过provide提供了哪些数据。
例如App.vue
提供了key为name、version的provide:
csharp
代码解读
复制代码
import { ref, provide } from 'vue' const count = ref(0) provide('name', 'useContext') provide('version', '1.0.0')
子组件使用inject获取配置:
javascript
代码解读
复制代码
import { inject } from 'vue' const name = inject('name', '') const version = inject('version', '0.0.1')
这种使用方式的缺点:provide使用显得碎片化,子组件不能感知到上级节点到底提供了哪些注入信息。
那么问题来了:能不能像React一样使用createContext、useContext方式通过高阶组件注入信息?
先看下React
是如何使用useContext的:
javascript
代码解读
复制代码
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:
typescript
代码解读
复制代码
// context.ts文件 import { createContext } from '@vueuse/core' export interface ThemeState { type: string; } export const ThemeContext = createContext<ThemeState>()
2. 在容器组件中像React一样使用Provider高阶组件:
xml
代码解读
复制代码
// 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:
xml
代码解读
复制代码
// 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>
类型。
r
代码解读
复制代码
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组件,如:
ruby
代码解读
复制代码
<Context.Provider :provider="{...}"></Context.Provider>
而setState为数据更新函数。
csharp
代码解读
复制代码
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
函数实现:
javascript
代码解读
复制代码
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函数,获取透传数据
javascript
代码解读
复制代码
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数据,例如:
scss
代码解读
复制代码
const { state, setState } = useContext(ThemeContext)
对外API:createContext、useContext
上文介绍的都是内部实现逻辑,而提供给研发使用的仅需要createContext和useContext两个函数即可。
arduino
代码解读
复制代码
// index.ts export { createContext, useContext, }
在使用state时,由于其类型已知,因此能感知到包含的属性,这样也解决了不能感知的问题。
下步计划:将useContext提交给vueuse
useContext基于vueuse
实现,也是按vueuse
要求的格式编写,包含markdown、test、demo。接下来打算将其提交给vueuse
,下次给面试官吹牛,俺也是开源贡献者😄😄😄😄😄😄。
什么是vueuse?可参考《Vue无处不use的VueUse: Composition工具集,代码减半神器!》了解。
完整代码
- index.ts:
typescript
代码解读
复制代码
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:
javascript
代码解读
复制代码
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, } }
原文链接:https://juejin.cn/post/7416006613552939059