Vue3 竟然可以像React的useContext,通过高阶组件透传数据

149 篇文章 0 订阅
149 篇文章 0 订阅

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

  1. 定义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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值