Skip to content

Commit cd2b6e0

Browse files
committed
perf: only update when component depend effect loading
1 parent f81dfa8 commit cd2b6e0

File tree

3 files changed

+121
-34
lines changed

3 files changed

+121
-34
lines changed

src/core/Container.ts

+56-8
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,68 @@
11
import { shallowEqual } from '../utils/shallowEqual'
2-
import { Subscriber } from '../types'
2+
import { StateSubscriber, EffectSubscriber } from '../types'
3+
4+
type SubscribeType = 'state' | 'effect'
35

46
export class Container<T> {
5-
public subscribers: Subscriber<T>[] = []
7+
private stateSubscribers: StateSubscriber<T>[] = []
8+
private effectSubscribers: EffectSubscriber[] = []
9+
10+
public subscribe(type: 'state', payload: StateSubscriber<T>): void
11+
public subscribe(type: 'effect', payload: EffectSubscriber): void
12+
public subscribe(type: SubscribeType, payload: StateSubscriber<T> | EffectSubscriber): void {
13+
if (type === 'state') {
14+
const stateSubscriber = payload as StateSubscriber<T>
15+
16+
if (this.stateSubscribers.indexOf(stateSubscriber) === -1) {
17+
this.stateSubscribers.push(stateSubscriber)
18+
}
19+
} else if (type === 'effect') {
20+
const effectSubscriber = payload as EffectSubscriber
21+
22+
if (this.effectSubscribers.indexOf(effectSubscriber) === -1) {
23+
this.effectSubscribers.push(effectSubscriber)
24+
}
25+
}
26+
}
627

7-
notify(payload?: T): void {
8-
for (let i = 0; i < this.subscribers.length; i++) {
9-
const { dispatcher, mapStateToProps, prevState } = this.subscribers[i]
28+
public notify(payload?: T): void {
29+
if (payload) {
30+
for (let i = 0; i < this.stateSubscribers.length; i++) {
31+
const { dispatcher, mapStateToProps, prevState } = this.stateSubscribers[i]
1032

11-
const newState = payload ? mapStateToProps(payload) : prevState
33+
const newState = mapStateToProps(payload)
1234

13-
this.subscribers[i].prevState = newState
35+
this.stateSubscribers[i].prevState = newState
1436

15-
if (!shallowEqual(prevState, newState)) {
37+
if (!shallowEqual(prevState, newState)) {
38+
dispatcher(Object.create(null))
39+
}
40+
}
41+
} else {
42+
for (let i = 0; i < this.effectSubscribers.length; i++) {
43+
const dispatcher = this.effectSubscribers[i]
1644
dispatcher(Object.create(null))
1745
}
1846
}
1947
}
48+
49+
public unsubscribe(type: 'state', payload: StateSubscriber<T>): void
50+
public unsubscribe(type: 'effect', payload: EffectSubscriber): void
51+
public unsubscribe(type: SubscribeType, payload: StateSubscriber<T> | EffectSubscriber): void {
52+
if (type === 'state') {
53+
if (this.stateSubscribers.length === 0) {
54+
return
55+
}
56+
57+
const index = this.stateSubscribers.indexOf(payload as StateSubscriber<T>)
58+
this.stateSubscribers.splice(index, 1)
59+
} else if (type === 'effect') {
60+
if (this.effectSubscribers.length === 0) {
61+
return
62+
}
63+
64+
const index = this.effectSubscribers.indexOf(payload as EffectSubscriber)
65+
this.effectSubscribers.splice(index, 1)
66+
}
67+
}
2068
}

src/core/Model.tsx

+54-17
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
1-
import { useState, useRef, useEffect } from 'react'
1+
import { useState, useRef, useEffect, Dispatch } from 'react'
22
import produce from 'immer'
3-
import { ConfigReducer, ContextPropsModel, MapStateToProps, ModelConfig, ModelContextProps, ModelEffect, Subscriber } from '../types'
3+
import {
4+
ConfigReducer,
5+
ContextPropsModel,
6+
MapStateToProps,
7+
ModelConfig,
8+
ModelConfigEffect,
9+
ModelContextProps,
10+
StateSubscriber
11+
} from '../types'
412
import { invariant } from '../utils/invariant'
513
import { isObject } from '../utils/type'
614
import { createProvider } from './createProvider'
715
import { Container } from './Container'
16+
import { noop } from '../utils/func'
817

918
interface ModelOptions<C extends ModelConfig> {
1019
storeName: string
@@ -20,7 +29,11 @@ export class Model<C extends ModelConfig> {
2029

2130
private model: ContextPropsModel<C>
2231
private initialState: C['state']
32+
2333
private container: Container<C['state']>
34+
private currentDispatcher: Dispatch<any> = noop
35+
private isInternalUpdate = false
36+
2437
private useContext: () => ModelContextProps
2538

2639
constructor(private options: ModelOptions<C>) {
@@ -40,12 +53,13 @@ export class Model<C extends ModelConfig> {
4053

4154
public useModel(mapStateToProps: MapStateToProps<C['state']>): any {
4255
const [, dispatcher] = useState()
43-
const { subscribers } = this.container
4456
const { model } = this.useContext()
4557

46-
const subscriberRef = useRef<Subscriber<C['state']>>()
58+
this.currentDispatcher = dispatcher
59+
60+
const subscriberRef = useRef<StateSubscriber<C['state']>>()
4761

48-
// 组件初始化时注册监听函数,使用 useRef 保证每个组件只注册一次
62+
// make sure only subscribe once
4963
if (!subscriberRef.current) {
5064
const subscriber = {
5165
dispatcher,
@@ -54,15 +68,14 @@ export class Model<C extends ModelConfig> {
5468
}
5569

5670
subscriberRef.current = subscriber
57-
subscribers.push(subscriber)
71+
this.container.subscribe('state', subscriber)
5872
}
5973

60-
/* eslint-disable-next-line */
6174
useEffect(() => {
6275
return (): void => {
63-
// 组件卸载时解绑监听函数
64-
const index = subscribers.indexOf(subscriberRef.current!)
65-
subscribers.splice(index, 1)
76+
// unsubscribe when component unmount
77+
this.container.unsubscribe('state', subscriberRef.current as StateSubscriber<C['state']>)
78+
this.container.unsubscribe('effect', dispatcher)
6679
}
6780
}, [])
6881

@@ -116,7 +129,7 @@ export class Model<C extends ModelConfig> {
116129
Object.create(null)
117130
)
118131

119-
// 如果用户没有定义 setValue 则内置该方法
132+
// internal reducer setValue
120133
if (!reducers.setValue) {
121134
reducers.setValue = (key, value): void => {
122135
const newState = this.produceState(this.model.state, draft => {
@@ -128,7 +141,7 @@ export class Model<C extends ModelConfig> {
128141
}
129142
}
130143

131-
// 如果用户没有定义 setValues 则内置该方法
144+
// internal reducer setValues
132145
if (!reducers.setValues) {
133146
reducers.setValues = (partialState): void => {
134147
const newState = this.produceState(this.model.state, draft => {
@@ -143,7 +156,7 @@ export class Model<C extends ModelConfig> {
143156
}
144157
}
145158

146-
// 如果用户没有定义 reset 则内置该方法
159+
// internal reducer reset
147160
if (!reducers.reset) {
148161
reducers.reset = (key): void => {
149162
const newState = this.produceState(this.model.state, draft => {
@@ -168,12 +181,13 @@ export class Model<C extends ModelConfig> {
168181
return Object.keys(config.effects).reduce((effects, name) => {
169182
const originEffect = config.effects[name]
170183

171-
const effect: ModelEffect<typeof originEffect> = async (...payload: any): Promise<void> => {
184+
const effect: ModelConfigEffect<typeof originEffect> = async (...payload: any): Promise<void> => {
172185
try {
173186
effect.identifier++
187+
188+
this.isInternalUpdate = true
174189
effect.loading = true
175-
176-
this.container.notify(null)
190+
this.isInternalUpdate = false
177191

178192
const result = await originEffect(...payload)
179193
return result
@@ -184,14 +198,37 @@ export class Model<C extends ModelConfig> {
184198

185199
/* istanbul ignore else */
186200
if (effect.identifier === 0) {
201+
this.isInternalUpdate = true
187202
effect.loading = false
188-
this.container.notify(null)
203+
this.isInternalUpdate = false
189204
}
190205
}
191206
}
192207

193208
effect.loading = false
194209
effect.identifier = 0
210+
211+
let value = false
212+
const that = this
213+
214+
Object.defineProperty(effect, 'loading', {
215+
configurable: false,
216+
enumerable: true,
217+
218+
get() {
219+
that.container.subscribe('effect', that.currentDispatcher)
220+
return value
221+
},
222+
223+
set(newValue) {
224+
// avoid modify effect loading out of internal
225+
if(newValue !== value && that.isInternalUpdate) {
226+
value = newValue
227+
that.container.notify()
228+
}
229+
}
230+
})
231+
195232
effects[name] = effect
196233

197234
return effects

src/types.ts

+11-9
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,7 @@ export interface Configs {
6363
}
6464

6565
interface ModelEffectState {
66-
loading: boolean
67-
identifier: number
66+
readonly loading: boolean
6867
}
6968

7069
interface BuildInReducers<S = any> {
@@ -119,6 +118,13 @@ export interface ModelConfig<S = any> {
119118
effects: { [key: string]: ConfigEffect }
120119
}
121120

121+
export type ModelConfigEffect<E extends ConfigEffect> = (E extends (...payload: infer P) => infer R
122+
? (...payload: P) => R
123+
: any) & {
124+
loading: boolean
125+
identifier: number
126+
}
127+
122128
export interface ContextPropsModel<C extends ModelConfig = any> {
123129
state: C['state']
124130
reducers: C['reducers'] extends ConfigReducers<C['state']>
@@ -147,24 +153,20 @@ export type StoreProvider<C extends Configs> = React.FC<
147153
React.PropsWithChildren<StoreProviderOptions<C>>
148154
>
149155

150-
export interface StoreOptions<C extends Configs> {
151-
name: string
152-
autoReset: boolean | UnionToTuple<keyof C>
153-
devTools: boolean | UnionToTuple<keyof C>
154-
}
155-
156156
export type HOC<InjectProps = any> = <P>(
157157
Component: React.ComponentType<P & InjectProps>
158158
) => React.ComponentType<P>
159159

160160
export type Optionality<T extends K, K> = Omit<T, keyof K>
161161

162-
export interface Subscriber<T> {
162+
export interface StateSubscriber<T> {
163163
mapStateToProps: MapStateToProps<any>
164164
prevState: T
165165
dispatcher: Dispatch<any>
166166
}
167167

168+
export type EffectSubscriber = Dispatch<any>
169+
168170
export interface MapStateToProps<M extends Model<any>, S = any> {
169171
(state: M['state']): S
170172
}

0 commit comments

Comments
 (0)