文章

跨组件状态共享方案深度解析

全面剖析前端框架中跨组件状态共享的多种方案,从 Context API 到 provide/inject 到事件总线,对比每种方案的优劣与适用场景。

跨组件状态共享方案深度解析

一句话概括

跨组件状态共享是前端架构的核心挑战,React 的 Context API、Vue 的 provide/inject、传统的事件总线以及全局 Store 方案各有优劣,关键在于根据状态变化的频率和影响范围选择合适的通信机制。

背景与意义

在组件化开发中,组件之间的通信是不可回避的问题。父子组件可以通过 props/emit 通信,这是最简单直接的方案。但当组件嵌套层级变深,或者两个组件在组件树中位置相距很远时,逐层传递 props 就会变成噩梦——这就是所谓的「props drilling」问题。

以一个典型的三层订单页面为例:

1
2
3
4
5
6
App
└── DashboardPage
    └── OrderPanel
        └── OrderList
            └── OrderItem
                └── OrderActionButtons (需要触发全局通知)

如果 OrderActionButtons 需要触发页头部的通知栏,按照 props 逐层传递的方式,需要经过 5 层组件传递回调函数。这不仅增加了代码耦合,还让中间层组件承载了它并不关心的「透传 props」。

跨组件状态共享方案的目标正是:让相距遥远的组件能够直接共享和通信,而无需经过中间层。

概念与定义

Props Drilling(属性透传):通过组件的 props 将数据逐层向下传递,导致中间层组件承载了大量与自身无关的 props。

依赖注入(Dependency Injection):允许「在上层组件中提供数据,在下层组件中直接注入使用」,而不需要逐层传递。React 的 Context 和 Vue 的 provide/inject 都是依赖注入的实现。

事件总线(Event Bus):一个独立的事件发布/订阅中心,任何组件都可以发射事件或监听事件。在 Vue 2 中常用 new Vue() 作为事件总线。

全局 Store:将状态提升到应用的最顶层,通过一个全局状态容器管理,所有组件都可以读写。

最小示例

React Context API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建 Context
const ThemeContext = React.createContext('light')

// 提供者
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  )
}

// 消费者(函数组件使用 useContext)
function ThemedButton() {
  const theme = useContext(ThemeContext)
  return <button className={theme}>按钮</button>
}

Vue provide/inject

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 提供者 -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme)
</script>

<!-- 消费者 -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme', 'light') // 第二个参数是默认值
</script>

核心知识点拆解

1. React Context API:内置依赖注入

React 的 Context API 是 React 提供的内置跨组件通信方案,从 React 16.3 开始稳定可用。

Context 的创建与使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 1. 创建 Context
const AuthContext = createContext(null)

// 2. Provider 提供数据
function AuthProvider({ children }) {
  const [user, setUser] = useState(null)
  const [permissions, setPermissions] = useState([])

  const login = async (credentials) => {
    const { user, token } = await authService.login(credentials)
    setUser(user)
    setPermissions(user.permissions)
    localStorage.setItem('token', token)
  }

  const logout = () => {
    setUser(null)
    setPermissions([])
    localStorage.removeItem('token')
  }

  const hasPermission = (permission) => permissions.includes(permission)

  // 使用 useMemo 包装 value,避免每次渲染都创建新对象
  const value = useMemo(() => ({
    user,
    permissions,
    isAuthenticated: !!user,
    login,
    logout,
    hasPermission,
  }), [user, permissions])

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  )
}

// 3. 自定义 Hook(推荐做法)
function useAuth() {
  const context = useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider')
  }
  return context
}

// 4. 在任何层级使用
function UserMenu() {
  const { user, logout, isAuthenticated } = useAuth()

  if (!isAuthenticated) return <LoginButton />

  return (
    <div className="user-menu">
      <span>{user.name}</span>
      <button onClick={logout}>退出</button>
    </div>
  )
}

Context 的问题与优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// ❌ Context 的经典问题:所有 Consumer 都会重新渲染
const AppContext = createContext()

function AppProvider({ children }) {
  const [count, setCount] = useState(0)
  const [text, setText] = useState('hello')

  // count 变化时,即使 setText 相关的组件不需要更新
  // 所有 useAppContext() 的组件都会重新渲染
  return (
    <AppContext.Provider value={{ count, text, setCount, setText }}>
      {children}
    </AppContext.Provider>
  )
}

// ✅ 优化:拆分 Context
const CountContext = createContext()
const TextContext = createContext()

function AppProvider({ children }) {
  return (
    <CountProvider>
      <TextProvider>
        {children}
      </TextProvider>
    </CountProvider>
  )
}

// 或者使用 useMemo 优化 value
function AppProvider({ children }) {
  const [count, setCount] = useState(0)
  const [text, setText] = useState('hello')

  const countValue = useMemo(() => ({ count, setCount }), [count])
  const textValue = useMemo(() => ({ text, setText }), [text])

  return (
    <CountContext.Provider value={countValue}>
      <TextContext.Provider value={textValue}>
        {children}
      </TextContext.Provider>
    </CountContext.Provider>
  )
}

2. React 多层次 Context 组合

在大型应用中,通常需要多个 Context 共同工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 多层次 Context 组合
function AppProviders({ children }) {
  return (
    <ThemeProvider>
      <AuthProvider>
        <LocaleProvider>
          <NotificationProvider>
            {children}
          </NotificationProvider>
        </LocaleProvider>
      </AuthProvider>
    </ThemeProvider>
  )
}

// 每个 Provider 关注不同领域
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')
  const value = useMemo(() => ({ theme, setTheme }), [theme])
  return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
}

function LocaleProvider({ children }) {
  const [locale, setLocale] = useState('zh-CN')
  const value = useMemo(() => ({ locale, setLocale }), [locale])
  return <LocaleContext.Provider value={value}>{children}</LocaleContext.Provider>
}

3. Vue 3 provide/inject:响应式注入

Vue 3 的 provide/inject 相比 React 的 Context 有一个天然优势——Vue 的响应式系统可以让注入的值自动更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<!-- App.vue - 提供者 -->
<script setup>
import { provide, ref, readonly, computed } from 'vue'

// 用户状态
const user = ref(null)
const permissions = ref([])

// 使用 readonly 防止子组件意外修改
provide('user', readonly(user))
provide('permissions', readonly(permissions))

// 提供修改方法
function login(credentials) {
  // ... 登录逻辑
  user.value = { name: 'Alice', role: 'admin' }
  permissions.value = ['read', 'write', 'delete']
}

function logout() {
  user.value = null
  permissions.value = []
}

provide('auth', { login, logout })

// Symbol 作为注入 key(防止命名冲突)
export const THEME_KEY = Symbol('theme')
const theme = ref('light')
provide(THEME_KEY, theme)
</script>

<!-- 深层子组件 - 消费者 -->
<script setup>
import { inject } from 'vue'
import { THEME_KEY } from './App.vue'

const user = inject('user')
const permissions = inject('permissions')
const { login, logout } = inject('auth')
const theme = inject(THEME_KEY, 'light') // 带默认值

// 自动响应式:当 user 变化时,这里自动更新
// 不需要像 React 那样通过 Context 重新渲染来触发
</script>

<template>
  <div :class="theme">
    <p v-if="user">欢迎, {{ user.name }}</p>
    <button v-if="permissions.includes('delete')">删除</button>
  </div>
</template>

Vue provide 与 React Context 的核心差异

特性React ContextVue provide/inject
响应式依赖 React 重新渲染Vue 响应式系统
性能优化需要 useMemo / 拆分 Context响应式链路本身精确
修改方式传递 setState提供修改方法
类型安全泛型 <T>InjectionKey<T>
嵌套覆盖同 key 覆盖同 key 覆盖

4. Event Bus:传统的事件通信

Event Bus 在 Vue 2 生态中广泛应用,在 React 中则不太常见。它是一种「不依赖组件树」的通信方式。

TypeScript 泛型 Event Bus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// event-bus.ts
type EventHandler = (...args: any[]) => void

class EventBus {
  private events: Map<string, Set<EventHandler>> = new Map()

  on(event: string, handler: EventHandler): () => void {
    if (!this.events.has(event)) {
      this.events.set(event, new Set())
    }
    this.events.get(event)!.add(handler)

    // 返回取消订阅函数
    return () => this.off(event, handler)
  }

  off(event: string, handler: EventHandler): void {
    this.events.get(event)?.delete(handler)
  }

  emit(event: string, ...args: any[]): void {
    this.events.get(event)?.forEach((handler) => {
      try {
        handler(...args)
      } catch (error) {
        console.error(`EventBus handler error for event "${event}":`, error)
      }
    })
  }

  once(event: string, handler: EventHandler): () => void {
    const wrapper = (...args: any[]) => {
      handler(...args)
      this.off(event, wrapper)
    }
    return this.on(event, wrapper)
  }

  clear(event?: string): void {
    if (event) {
      this.events.delete(event)
    } else {
      this.events.clear()
    }
  }

  // 获取某个事件的监听器数量(用于调试)
  listenerCount(event: string): number {
    return this.events.get(event)?.size || 0
  }
}

export const globalEventBus = new EventBus()

在 React 中使用 Event Bus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// NotificationBell.tsx - 监听事件
import { useEffect, useState } from 'react'
import { globalEventBus } from './event-bus'

function NotificationBell() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const unsubscribe = globalEventBus.on('notification', (notification) => {
      setCount((c) => c + 1)
    })

    return unsubscribe // 组件卸载时取消订阅
  }, [])

  return <span className="bell">{count > 0 && <badge>{count}</badge>}</span>
}

// OrderActionButtons.tsx - 发射事件
function OrderActionButtons({ orderId }) {
  const handleCheckout = async () => {
    await api.checkout(orderId)
    // 发射事件——任何地方都可以监听
    globalEventBus.emit('notification', {
      type: 'success',
      message: '订单提交成功',
    })
    globalEventBus.emit('order:updated', { orderId, status: 'paid' })
  }

  return <button onClick={handleCheckout}>提交订单</button>
}

Event Bus 的缺点

  1. 难以追踪:谁发射的事件?谁在监听?IDE 无法通过静态分析找到
  2. 内存泄漏:组件卸载时忘记取消订阅会导致回调继续执行
  3. 不可预测的顺序:多个监听器的执行顺序不确定
  4. 类型不安全:TypeScript 约束有限

5. 全局 Store + 选择器模式

结合 Context 和 Store 各自的优势的混合方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// createSharedStore.js - 通用的跨组件 Store
import { createContext, useContext, useSyncExternalStore } from 'react'

export function createSharedStore(initialState) {
  let state = initialState
  const listeners = new Set()

  function getState() { return state }

  function setState(partial) {
    const next = typeof partial === 'function' ? partial(state) : partial
    if (next !== state) {
      state = { ...state, ...next }
      listeners.forEach((l) => l())
    }
  }

  function subscribe(listener) {
    listeners.add(listener)
    return () => listeners.delete(listener)
  }

  return { getState, setState, subscribe }
}

// 结合 Context 的 React 绑定
const StoreContext = createContext(null)

export function StoreProvider({ store, children }) {
  return (
    <StoreContext.Provider value={store}>
      {children}
    </StoreContext.Provider>
  )
}

// 自定义 Hook:选择器模式
export function useStoreValue(selector) {
  const store = useContext(StoreContext)
  if (!store) throw new Error('useStoreValue must be used within StoreProvider')

  return useSyncExternalStore(
    store.subscribe,
    () => selector(store.getState())
  )
}

// 使用示例
const appStore = createSharedStore({
  user: null,
  notifications: [],
  theme: 'light',
})

// 组件 A:只关心用户
function UserAvatar() {
  const user = useStoreValue((state) => state.user)
  return user ? <img src={user.avatar} /> : <LoginPrompt />
}

// 组件 B:只关心通知
function NotificationBadge() {
  const count = useStoreValue((state) => state.notifications.length)
  return <span>{count > 0 ? count : ''}</span>
}

实战案例:项目管理看板

以下是一个项目管理看板的跨组件通信完整实现。看板包含:侧边栏项目列表、看板视图、任务详情面板、全局搜索、通知中心。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
// 1. 定义 Context 层
// contexts/project.js
const ProjectContext = createContext()
const BoardContext = createContext()
const SearchContext = createContext()
const NotificationContext = createContext()

// 2. 组合 Provider
function KanbanProviders({ children }) {
  return (
    <NotificationProvider>
      <SearchProvider>
        <ProjectProvider>
          <BoardProvider>
            {children}
          </BoardProvider>
        </ProjectProvider>
      </SearchProvider>
    </NotificationProvider>
  )
}

// 3. 侧边栏(项目列表)需要更新当前项目
function Sidebar() {
  const { projects, selectProject } = useProjects()

  return (
    <div className="sidebar">
      {projects.map((project) => (
        <div key={project.id} onClick={() => selectProject(project.id)}>
          {project.name}
        </div>
      ))}
    </div>
  )
}

// 4. 看板视图(响应项目变化)
function BoardView() {
  const { currentProject, isLoading } = useProjects()

  if (!currentProject) return <EmptyState />

  return (
    <div className="board">
      <BoardHeader project={currentProject} />
      <BoardColumns projectId={currentProject.id} />
    </div>
  )
}

// 5. 任务详情面板(响应拖拽和选中变化)
function TaskDetailPanel() {
  const { selectedTask } = useBoard()

  if (!selectedTask) return null

  return (
    <SlideOver>
      <TaskDetails task={selectedTask} />
    </SlideOver>
  )
}

// 6. 通知中心(响应全局事件)
function NotificationCenter() {
  const { notifications, markAsRead } = useNotifications()

  return (
    <div className="notification-list">
      {notifications.map((n) => (
        <NotificationCard key={n.id} notification={n} onRead={markAsRead} />
      ))}
    </div>
  )
}

// 7. 全局搜索(响应搜索状态变化)
function GlobalSearch() {
  const { query, results, isSearching, setQuery } = useSearch()

  return (
    <CommandPalette open={isSearching} onClose={() => setQuery('')}>
      <SearchInput value={query} onChange={setQuery} />
      <SearchResults results={results} />
    </CommandPalette>
  )
}

底层原理(含源码分析)

1. React Context 的 Fiber 层实现

React Context 的 Provider-Consumer 机制依赖 Fiber 架构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// React 源码中 Context 的实现简化
function createContext(defaultValue) {
  const context = {
    _currentValue: defaultValue,
    _currentRenderer: null,
    Provider: null,
    Consumer: null,
  }

  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  }

  context.Consumer = {
    $$typeof: REACT_CONTEXT_TYPE,
    _context: context,
  }

  return context
}

// 在 Fiber reconciler 中
function beginWork(current, workInProgress, renderLanes) {
  switch (workInProgress.tag) {
    case ContextProvider: {
      // Provider 更新:将新值写入 context._currentValue
      const context = workInProgress.type._context
      const newValue = workInProgress.pendingProps.value
      context._currentValue = newValue

      // 向下传递变化
      propagateContextChange(workInProgress, context, renderLanes)
      break
    }
    case ContextConsumer: {
      // Consumer 读取:获取当前值
      const context = workInProgress.type._context
      const value = context._currentValue
      // 对比新值和旧值
      if (value !== oldValue) {
        // 标记需要更新
      }
      break
    }
  }
}

Context 的工作原理:Provider 更新时,React 会在 Fiber 树上向下遍历,找到所有订阅了该 Context 的 Consumer 节点,并逐个检查是否需要更新。这与 Vue 的响应式系统「谁用谁更新」的模式有本质区别。

2. Vue provide/inject 的响应式链接

Vue 3 的 provide/inject 本质上是一个「继承链路」——子组件通过 inject 会沿着组件树向上查找最近的 provide

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Vue 3 provide/inject 源码简化
function provide(key, value) {
  // 获取当前组件的 provides 对象
  let provides = currentInstance.provides

  // 每个组件都有指向父组件 provides 的原型链
  // 这使得子组件注入时能沿着原型链找到祖先提供的值
  const parentProvides = currentInstance.parent?.provides

  if (provides === parentProvides) {
    // 当前组件还没提供过值,创建原型链
    provides = Object.create(parentProvides)
    currentInstance.provides = provides
  }

  provides[key] = value
}

function inject(key, defaultValue) {
  const instance = currentInstance

  // 沿着原型链查找
  if (instance?.provides && key in instance.provides) {
    return instance.provides[key]
  }

  return defaultValue
}

使用原型链的巧妙之处在于:

  1. 每个组件的 provides 继承父组件的 provides(原型链)
  2. 当子组件查找 inject(key) 时,沿着原型链向上查找
  3. 同名的 provide 会「覆盖」祖先的值(原型链的遮蔽机制)
  4. 响应式对象的引用传递——如果 provide 了一个 ref,所有 inject 该 ref 的子组件共享同一个响应式引用

高频面试题解析

Q1: React Context 默认会导致所有 Consumer 组件重新渲染,如何优化?

考察点:对 React 渲染机制和 Context 局限性的理解。

答案核心

Context 更新时,所有使用了该 Context 的 Consumer 组件都会被 React 标记为「需要更新」。这是一个「全量通知」而非「按需通知」的机制。

优化方案(按推荐度排序):

  1. 拆分 Context:将频繁变化的数据和不常变化的数据分到不同的 Context
    1
    2
    3
    
    const ThemeContext = createContext() // 几乎不变
    const UserContext = createContext()  // 登录/登出时变化
    const UiContext = createContext()    // 频繁变化
    
  2. useMemo 稳定 value:避免不必要的 Context 更新
    1
    
    const value = useMemo(() => ({ user, login }), [user])
    
  3. 将 Consumer 组件包裹在 React.memo 中:只有当关心的 props 变化时才重新渲染

  4. 使用第三方方案替代:Context 不适合频繁变化的状态,此时考虑 Zustand 或 Jotai

Q2: Vue 的 provide/inject 与 React Context 相比有什么根本性的设计差异?

考察点:对两种框架设计哲学的深入理解。

答案核心

根本差异在于「响应式系统的工作方式」:

  1. 响应式链路 vs 组件树对比:Vue 的 provide/inject 利用原型链继承,Inject 是「沿着原型链查找」,天然是响应式的——如果 provide 传递了一个 ref,inject 获得的是同一个 ref 引用,任何修改都自动生效;React Context 的 Consumer 获取的是 Provider value 的快照,Provider value 变化时需要重新渲染整个 Consumer 子树

  2. 修改方式:Vue 中 provide 一个 ref,子组件通过 .value 直接修改,或者通过 provide 传递修改函数;React 中通常传递 setState 函数,且 value 变化时通过重新渲染传递

  3. 性能模型:Vue 是「组件粒度更新」,provide 值变化只更新使用了该 inject 的组件;React 是「子树粒度更新」,Context 变化会重新渲染所有 Consumer

Q3: Event Bus 已经被主流框架「边缘化」了,它还有什么存在的价值?

考察点:对传统通信模式与框架机制的辩证思考。

答案核心

Event Bus 虽然已经不再是推荐方案,但在以下场景中仍有其独特价值:

  1. 跨应用通信:在微前端架构中,不同子应用之间需要通信,但各应用有自己的组件树和 Context,Event Bus 是实现「跨框架通信」的轻量级选择

  2. 非 UI 模块通信:比如一个请求拦截器需要在收到 401 时通知安全模块重新认证,这些逻辑在 React 组件树之外,Event Bus 比 Context 更易用

  3. 简易的全局广播:某些场景(如「全局提示」)只需要广播信号,不需要响应式绑定,「emit → on」的模式是最简单直接的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 微前端中的 Event Bus
// 主应用
window.__MICRO_EVENTS__ = new EventBus()

// 子应用 A(React)
useEffect(() => {
  return window.__MICRO_EVENTS__.on('user:login', handleLogin)
}, [])

// 子应用 B(Vue)
onMounted(() => {
  window.__MICRO_EVENTS__.on('user:login', handleLogin)
})
onUnmounted(() => {
  window.__MICRO_EVENTS__.off('user:login', handleLogin)
})

但 Event Bus 必须谨慎使用——最好包装成「有类型约束」的事件中心,并确保组件卸载时取消所有订阅。

总结与扩展

跨组件状态共享没有一个「万能方案」。本文详细分析了 React Context、Vue provide/inject、Event Bus、全局 Store 四种方案的实现原理和适用场景。

方案选型指南

1
2
3
4
5
6
7
8
9
状态共享范围|推荐方案
───────────|──────────
父子组件   | props / emit
兄弟组件   | 共同父级提升 + props
深层嵌套   | Context API / provide + inject
全局低频   | Store(Zustand / Pinia)
全局高频   | Store 原子化(Jotai / Recoil)
跨应用     | Event Bus / 广播通道
临时状态   | Event Bus(有节制使用)

扩展思考

React 19 的 use() Hook 可能改变 Context 的使用模式——它允许在条件分支或循环中使用 Context,不再受「Hook 必须在顶层运行」的限制。这为更灵活的 Context 使用提供了可能。

另一个值得关注的趋势是「Signals」模式——Solid.js、Preact Signals 以及 Vue 3.4+ 的响应式系统都在探索更细粒度的反应式状态共享方式。Signals 比 Context 更轻量,比 Store 更灵活,可能是未来跨组件通信的主流方向。

本文由作者按照 CC BY 4.0 进行授权