React.js + RxJS:为何仅在Store中订阅Observable才能让生命游戏应用正常运行?
我正在尝试用React.js结合RxJS实现带“炸弹”功能的简易生命游戏。但遇到个奇怪的问题:只在React组件里订阅状态Observable时,应用完全没反应;但在数据Store里额外加个订阅后,整个应用就正常运行了。只要添加注释“Just add this and it starts to work!”下面的订阅代码就能生效。
组件代码
import styles from './GOLGrid.module.css'; import { GOLState, useGOLContext } from '../../../../../../store/GOLStore'; import { useObservable } from 'rxjs-hooks'; import { useEffect, useState } from 'react'; export function GOLGrid() { const context = useGOLContext() const state = useObservable(() => context.GOLState$); // const [state, setState] = useState<GOLState>(); // useEffect(() => { // const subscription = context.GOLState$.subscribe({ // next: (state) => setState(state), // error: (err) => console.error('Error:', err), // complete: () => console.log('Complete'), // }); // return () => subscription.unsubscribe(); // },[]); const buildElements = () => { return state?.alife.map( (alife, i) => <div key={i} className={`${styles.golElement} ${alife ? styles.golElementLiving : ''}`}/>); } return <div className={styles.GOLGrid} style={{ gridTemplateColumns: `repeat(${state?.columns ?? 0},auto)`, gridTemplateRows: `repeat(${state?.rows ?? 0},auto)`}}> {buildElements()} </div>; }
Store代码
import { createContext } from 'react'; import React from 'react'; import { BehaviorSubject, Observable, combineLatest, filter, interval, map, merge, switchMap, tap, } from 'rxjs'; import { bomb, createStartConfig, newGeneration } from '../services/GOLService'; export type GOLState = { rows: number, columns: number, alife: Array<boolean>, } const numberOfRows$ = new BehaviorSubject(8); const numberOfColumns$ = new BehaviorSubject(8); const delay$ = new BehaviorSubject(1000); const bombs$ = new BehaviorSubject<number | null>(null) const GOLStateLatest$ = new BehaviorSubject<GOLState>({ rows: numberOfRows$.value, columns: numberOfColumns$.value, alife: createStartConfig(numberOfRows$.value, numberOfColumns$.value), }); const bombed$ = bombs$.pipe( filter((e) => e !== null), map((e) => bomb(GOLStateLatest$.value!, e!)), ); const newGeneration$ = delay$.pipe( switchMap((e) => interval(e).pipe( map(() => newGeneration(GOLStateLatest$.value)), )) ) const GOLState$ = merge(bombed$, newGeneration$).pipe( tap((e) => GOLStateLatest$.next(e)), ); // Just add this and it starts to work! GOLState$.subscribe({ next: (state) => null, error: (err) => console.error('Error:', err), complete: () => console.log('Complete'), }); type GOLContent = { GOLState$: Observable<GOLState>, } const GOLContext = createContext<GOLContent>({ GOLState$: GOLState$, }) export const useGOLContext = () => React.useContext(GOLContext); type Props = { children?: React.ReactNode }; export const GOLProvider: React.FC<Props> = ({children}) => ( <GOLContext.Provider value={{ GOLState$: GOLStateLatest$, }} > {children} </GOLContext.Provider> )
这本质是RxJS中冷Observable的特性导致的,拆解核心逻辑:
冷Observable的“惰性”:你代码里的
newGeneration$基于interval创建,而interval属于冷Observable——这类Observable不会主动生成数据,必须等到有至少一个订阅者存在时,才会启动并开始发射数据流。没有订阅的话,它就一直处于“休眠”状态。GOLState$是状态更新的核心管道:
GOLState$合并了bombed$和newGeneration$,并且通过tap操作符把计算出的新状态推给GOLStateLatest$(也就是组件实际订阅的BehaviorSubject)。但如果GOLState$没有被订阅,整个管道都不会执行:interval不会启动定时任务,newGeneration不会计算新状态,tap里的GOLStateLatest$.next(e)也不会触发,GOLStateLatest$自然不会有新值输出,组件也就看不到任何变化。组件订阅的是GOLStateLatest$而非GOLState$:你的
GOLProvider给组件提供的是GOLStateLatest$,但这个BehaviorSubject的更新完全依赖GOLState$管道的执行。当你在Store里给GOLState$加了那个空订阅后,GOLState$的管道被激活:interval开始定时发射值,新状态不断计算并通过tap推给GOLStateLatest$,组件订阅的GOLStateLatest$就能拿到更新,应用也就正常运行了。
如果不想保留这个“空订阅”,可以给GOLState$加上share()操作符把它转成热Observable,这样只要有一个订阅者(比如组件的订阅)就能激活整个管道,避免额外的订阅。
内容的提问来源于stack exchange,提问作者Thom

