背景
使用sigi 共享 react query API 时,人类某切图仔再次忆起被sigi、rx scheduler、react scheduler支配的恐惧。怎么事件流顺序这么诡异,还有高手?谁打的太极、谁的狮吼功?某切图仔一度陷入精神状态混乱中。
为了杜绝这种容易薅掉自己头发的场景继续出现,切图仔决定从源码开始分析这个黑盒子。保护发际线人人有责。
调试SIGI源码
git clone https://github.com/sigi-framework/sigi.git
yarn
yarn start:example xxxxxxxxxxx
注意:sigi默认取得ssr依赖,需要手动在demo中修改
import { useModule } from '@sigi/react/src/index.browser'
SIGI逻辑简述
简单来说,sigi体系就是加了rx.js的redux。
通过sigi/core的EffectModule+装饰器收集action、reducer。通过rx.js派发任务和合并reducer,最后通过react/useSyncExternalStore 更新UI。
所以重点分析core的代码,理清楚sigi的事件流即可。逻辑图如下:
sigi/core
metadata.ts
这个是装饰器相关内容,不展开赘述。
module.ts
核心部分EffectModule,Module核心部分,重要的用橙色标明,包括:
| 概念/名称 | 作用 | 拓展 |
|---|---|---|
| effect\epic | 事件流,派发新的effect事件流,可以理解成会引起数据变化的副作用函数。 | 主要逻辑在combineEffects 按RX.JS的返回值顺序merge自动派发任务 |
| store | 把module理解成依赖收集的话,store才是redux行为逻辑的体现 | this.store = new Store |
| reducer | 原理与redux基本一致 | 主要逻辑在combineReducers |
| state$ | state的rx/ReplaySubject | ReplaySubject这个很重要 |
| action$ | action的rx/subject | |
| actions | 所有的actions | 默认:reset\terminate\noop, 其中noop在后续更新中将取代项目中的 createNoopAction,可以理解成:其本身也是noop的另一个别名。 |
| state | 当前store的state |
在 EffectModule 中,做了effect收集、actions收集、定义redux功能。真正的功能实现在store.ts中。
store.ts
这个模块是redux/rx.js行为逻辑
重点在dispatch、subscribeAction两个API,最后通过this.reducer处理state更新UI。
subscribeAction只在Effect任务情况下才会触发,本质上只是个merge的rx实例,不停吐出type。这个角度看来,rx是确保了执行顺序的。
subscribeAction
这里监听派发的action type,触发this.disptach
这里需要结合combineEffects API的代码才能搞懂,epic概念
private subscribeAction() {
this.actionSub = this.epic$
.pipe(
switchMap(
(epic) => epic(this.action$).pipe(
tap((ac) => { console.log(ac, 'action subscribeAction epic') }),
takeUntil(this.action$.pipe(
// 防止多次派发effect,也只有Effect逻辑走这里才会生效,主要为了epic 服务。虽然非Effect任务也会执行
tap((ac) => { console.log(ac, 'action subscribeAction takeUntil') }),
last(null, null)
))
))
)
.subscribe({
next: (action) => {
console.log(action, 'action subscribeAction')
try {
this.dispatch(action)
} catch (e) {
if (process.env.NODE_ENV === 'development') {
console.error(e)
}
this.action$.error(e)
}
},
error: (e) => {
if (!this.action$.closed) {
this.action$.error(e)
}
},
})
}
dispatch
主要作用是触发this.reducer、阻止重复的effect被epic
dispatch(action: Action) {
// ignore noop action
if (action.type === NOOP_ACTION_TYPE_SYMBOL) {
return
}
if (action.store !== this) {
action.store.dispatch(action)
return
}
console.log(action, 'action dispatch')
const prevState = this.internalState
const newState = this.reducer(prevState, action)
console.log(newState, 'newState dispatch')
if (newState !== prevState) {
if (process.env.NODE_ENV !== 'production' && newState === undefined) {
console.warn(`${action.type} produced an undefined state, you may forget to return new State in @Reducer`)
}
this.internalState = newState
// 不等于时,更新,但不是分散更新
this.state$.next(newState)
}
this.log(action)
// 为了终结 action
console.log(action, 'action dispatch 为了终结')
this.action$.next(action)
}
log(action: Action) {
if (action.type !== TERMINATE_ACTION_TYPE_SYMBOL) {
logStoreAction(action)
}
}
逻辑:
- 通过dispatch触发type,
- 如果是Effect任务的话则会触发subscribeAction逻辑,通过merge 返回的rx shceduler返回type,一次执行 this.dispatch(action)。如果普通reducer任务则直接执行。
- 每次执行都会触发this.reducer(prevState, action)
- 更新数据
react/sigi
在this.dispatch函数中,这段代码就是更新的逻辑
if (newState !== prevState) {
if (process.env.NODE_ENV !== 'production' && newState === undefined) {
console.warn(`${action.type} produced an undefined state, you may forget to return new State in @Reducer`)
}
this.internalState = newState
// 不等于时,更新,但不是分散更新
this.state$.next(newState)
}
ReplaySubject
ReplaySubject 特性分析:
多次被连续订阅,会根据设定缓存值缓存起来,向订阅者发送旧数据与新数据。但是在源码中作用是啥?已经自动跳过缓存更新,纯属娱乐么?
具体逻辑在_useModuleState中。准确来说在useSyncExternalStoreWithSelector,最终实现好像还是指向react本身的API。
_useModuleState
最终代码还是以useSyncExternalStore方式驱动更新,这里也不展开赘述。不过疑问点,为什么要skip(1)?
重点:
sigi的订阅多次执行onStoreChange,但只会执行一次UI更新,这个应该是与react本身这个hooks实现的原理有关,进行了任务调度。这里不继续分析react源码。简直是个黑洞。
function _useModuleState<S, U = S>(
store: IStore<S>,
// @ts-expect-error valid assignment
selector: StateSelectorConfig<S, U>['selector'] = identity,
equalFn = shallowEqual,
): S | U {
const state = useSyncExternalStoreWithSelector(
(onStoreChange) => {
// store更新后就更新对应的fiber
console.log('onStoreChange')
const sub = store.state$.pipe(skip(1)).subscribe(
(v)=>{
// 每次数据不同都会执行onStoreChange,但UI只会更新一次,大概是react本身的任务调度处理的
console.log(v,3838)
console.log('onStoreChange 3939')
onStoreChange()
}
)
return () => sub.unsubscribe()
},
() => store.state,
() => store.state,
selector,
equalFn,
)
useDebugValue(state)
return state
}
useSyncExternalStore 的更新原理
function subscribeToStore(fiber, inst, subscribe) {
var handleStoreChange = function () {
// The store changed. Check if the snapshot changed since the last time we
// read from the store.
if (checkIfSnapshotChanged(inst)) {
// Force a re-render.
forceStoreRerender(fiber);
}
}; // Subscribe to the store and return a clean-up function.
return subscribe(handleStoreChange);
}
总结
sigi本质上与redux无区别,并没有什么黑科技。独特之处在于使用rx处理异步,所以最大的区别在Effect事件,基本整个sigi代码都是围绕着effect事件流开展的。在effect 的epic事件流中,rx是按序执行的action type,也就是在rx的scheduler顺序是能确定的。
redux要做到类似功能的话,需要其他生态库支持,比如rtk、redux-saga。可读性没rx这么强。不过rx的波粒二象性与量子纠缠现象也挺耗心智。
如果使用sigi+antdTable hooks或者其他结合react API使用的时候,需要注意,rx的调度器和react fiber 的调度器并不同步,所以可以考虑使用delay(尽量>50ms,这是react任务超时最大时间)或者实时传参解决数据同步问题。