sigi源码分析

293 阅读3分钟

背景

使用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的事件流即可。逻辑图如下:

store.png

sigi/core

metadata.ts

这个是装饰器相关内容,不展开赘述。

module.ts

核心部分EffectModule,Module核心部分,重要的用橙色标明,包括:

概念/名称作用拓展
effect\epic事件流,派发新的effect事件流,可以理解成会引起数据变化的副作用函数。主要逻辑在combineEffects
按RX.JS的返回值顺序merge自动派发任务
store把module理解成依赖收集的话,store才是redux行为逻辑的体现this.store = new Store(this.moduleName, reducer, epic)
reducer原理与redux基本一致主要逻辑在combineReducers
state$state的rx/ReplaySubjectReplaySubject这个很重要
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)
    }
  }

逻辑:

  1. 通过dispatch触发type,
  2. 如果是Effect任务的话则会触发subscribeAction逻辑,通过merge 返回的rx shceduler返回type,一次执行 this.dispatch(action)。如果普通reducer任务则直接执行。
  3. 每次执行都会触发this.reducer(prevState, action)
  4. 更新数据

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任务超时最大时间)或者实时传参解决数据同步问题。