Flux-Core-Concept
Stephen Cui ... 2019-10-14 13:39:34 About 4 min
# Flux
# Data Flow Concept
* -------- Action ----- *
| |
v ^
Action ---> Dispatcher ---> Store ---> View
1
2
3
4
2
3
4
# Core Feature
- Action
- Define
actionType
and actiondata
byaction creator
- Invoke an action with a dispatcher
- Define
- Dispatcher
- Only one
dispatcher
Register/unregister
subscriber to DispatcherDispatch
action with payload to subscribers if store not pendingWaitFor
is used to send action to group of ids
- Only one
- Store
- Register store into
dispatcher
- Receive 'all actions' but just response to what they need by some
reducer
- Should be emmit an 'change' event if Store data changed(after
reducer
processed)
- Register store into
- View
- Use Flux
Container
to wrapper a view and Container withsubscriptions
to listenerStore
data change - Subscribe a 'change' event for the data which the view need(bind)
- Invoke component
setState
after receive the 'change' event. - View will do rerender if property changed, optimise is use
ShouldComponentUpdate
- Use Flux
# Source Code Analysis
# Dispatcher
- Register/unregister 'ActionDispatcher' in to dispatcher and return unique id
/**
* Registers a callback to be invoked with every dispatched payload. Returns
* a token that can be used with `waitFor()`.
*/
register(callback: (payload: TPayload) => void): DispatchToken {
var id = _prefix + this._lastID++;
this._callbacks[id] = callback;
return id;
}
/**
* Removes a callback based on its token.
*/
unregister(id: DispatchToken): void {
invariant(
this._callbacks[id],
'Dispatcher.unregister(...): `%s` does not map to a registered callback.',
id
);
delete this._callbacks[id];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- waitFor: check circular dependency and invoke registered callback by group of ids which described in
_invokeCallback
function
/**
* Waits for the callbacks specified to be invoked before continuing execution
* of the current callback. This method should only be used by a callback in
* response to a dispatched payload.
*/
waitFor(ids: Array<DispatchToken>): void {
invariant(
this._isDispatching,
'Dispatcher.waitFor(...): Must be invoked while dispatching.'
);
for (var ii = 0; ii < ids.length; ii++) {
var id = ids[ii];
if (this._isPending[id]) {
invariant(
this._isHandled[id],
'Dispatcher.waitFor(...): Circular dependency detected while ' +
'waiting for `%s`.',
id
);
continue;
}
invariant(
this._callbacks[id],
'Dispatcher.waitFor(...): `%s` does not map to a registered callback.',
id
);
this._invokeCallback(id);
}
}
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
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
- dispatch action: need 'action' payload and send action event to all subscriber(Store) which described in
_invokeCallback
function
/**
* Dispatches a payload to all registered callbacks.
*/
dispatch(payload: TPayload): void {
invariant(
!this._isDispatching,
'Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch.'
);
this._startDispatching(payload);
try {
for (var id in this._callbacks) {
if (this._isPending[id]) {
continue;
}
this._invokeCallback(id);
}
} finally {
this._stopDispatching();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Store
- Emitter from fb
const {EventEmitter} = require('fbemitter');
// Example
var emitter = new EventEmitter();
var subscription = emitter.addListener('event', function(x, y) { console.log(x, y); });
emitter.emit('event', 5, 10); // Listener prints "5 10".
subscription.remove();
emitter.emit('event', 5, 10); // Listener not invoke
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
addListener
addListener(callback: (eventType?: string) => void): {remove: () => void} {
return this.__emitter.addListener(this.__changeEvent, callback);
}
1
2
3
2
3
# Action
- Action Types define a group a constant
const ActionTypes = {
ADD_TODO: 'ADD_TODO',
DELETE_COMPLETED_TODOS: 'DELETE_COMPLETED_TODOS',
DELETE_TODO: 'DELETE_TODO',
EDIT_TODO: 'EDIT_TODO',
START_EDITING_TODO: 'START_EDITING_TODO',
STOP_EDITING_TODO: 'STOP_EDITING_TODO',
TOGGLE_ALL_TODOS: 'TOGGLE_ALL_TODOS',
TOGGLE_TODO: 'TOGGLE_TODO',
UPDATE_DRAFT: 'UPDATE_DRAFT',
};
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
Actions
need dispatcher to dispatch an event withtype
andpayload
data
editTodo(id, text) {
TodoDispatcher.dispatch({
type: TodoActionTypes.EDIT_TODO,
id,
text,
});
},
1
2
3
4
5
6
7
2
3
4
5
6
7
# View
- Listen
store
change event and get newest data fromstore
and then update view
componentDidMount: function() {
ListStore.bind( 'change', this.listChanged );
},
listChanged: function() {
// Since the list changed, trigger a new render.
this.setState({
items: ListStore.getAll()
});
},
componentWillUnmount: function() {
ListStore.unbind( 'change', this.listChanged );
},
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# Advance - Flux Util
# Container
- Bind view data from any store
- Bind actions
- Create new component wrapper with
FunctionalContainer
method - Options for pure component, with props, with context
/**
* This is a way to connect stores to a functional stateless view. Here's a
* simple example:
*
* // FooView.js
*
* function FooView(props) {
* return <div>{props.value}</div>;
* }
*
* module.exports = FooView;
*
*
* // FooContainer.js
*
* function getStores() {
* return [FooStore];
* }
*
* function calculateState() {
* return {
* value: FooStore.getState();
* };
* }
*
* module.exports = FluxContainer.createFunctional(
* FooView,
* getStores,
* calculateState,
* );
*
*/
function createFunctional<Props, State, A, B>(
viewFn: (props: State) => React.Element<State>,
getStores: (props?: ?Props, context?: any) => Array<FluxStore>,
calculateState: (prevState?: ?State, props?: ?Props, context?: any) => State,
options?: Options,
): ReactClass<Props> {
class FunctionalContainer extends Component<void, Props, State> {
state: State;
static getStores(props?: ?Props, context?: any): Array<FluxStore> {
return getStores(props, context);
}
static calculateState(
prevState?: ?State,
props?: ?Props,
context?: any,
): State {
return calculateState(prevState, props, context);
}
render(): React.Element<State> {
return viewFn(this.state);
}
}
// Update the name of the component before creating the container.
const viewFnName = viewFn.displayName || viewFn.name || 'FunctionalContainer';
FunctionalContainer.displayName = viewFnName;
return create(FunctionalContainer, options);
}
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
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
# ReduceStore
- InitStore, init data
- Reduce store data by action payload, it is functional, just like
Reduce
/**
* This is the basic building block of a Flux application. All of your stores
* should extend this class.
*
* class CounterStore extends FluxReduceStore<number> {
* getInitialState(): number {
* return 1;
* }
*
* reduce(state: number, action: Object): number {
* switch(action.type) {
* case: 'add':
* return state + action.value;
* case: 'double':
* return state * 2;
* default:
* return state;
* }
* }
* }
*/
class FluxReduceStore<TState> extends FluxStore {
_state: TState;
constructor(dispatcher: Dispatcher<Object>) {
super(dispatcher);
this._state = this.getInitialState();
}
/**
* Getter that exposes the entire state of this store. If your state is not
* immutable you should override this and not expose _state directly.
*/
getState(): TState {
return this._state;
}
/**
* Constructs the initial state for this store. This is called once during
* construction of the store.
*/
getInitialState(): TState {
return abstractMethod('FluxReduceStore', 'getInitialState');
}
/**
* Used to reduce a stream of actions coming from the dispatcher into a
* single state object.
*/
reduce(state: TState, action: Object): TState {
return abstractMethod('FluxReduceStore', 'reduce');
}
/**
* Checks if two versions of state are the same. You do not need to override
* this if your state is immutable.
*/
areEqual(one: TState, two: TState): boolean {
return one === two;
}
__invokeOnDispatch(action: Object): void {
this.__changed = false;
// Reduce the stream of incoming actions to state, update when necessary.
const startingState = this._state;
const endingState = this.reduce(startingState, action);
// This means your ending state should never be undefined.
invariant(
endingState !== undefined,
'%s returned undefined from reduce(...), did you forget to return ' +
'state in the default case? (use null if this was intentional)',
this.constructor.name,
);
if (!this.areEqual(startingState, endingState)) {
this._state = endingState;
// `__emitChange()` sets `this.__changed` to true and then the actual
// change will be fired from the emitter at the end of the dispatch, this
// is required in order to support methods like `hasChanged()`
this.__emitChange();
}
if (this.__changed) {
this.__emitter.emit(this.__changeEvent);
}
}
}
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
89
90
91
92
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
89
90
91
92
# Container - Subscriptions
- Register emitter listener so that can set internal varible
changed
to true - Log Store history in DEV environment
class FluxContainerSubscriptions {
_callbacks: Array<() => void>;
_storeGroup: ?FluxStoreGroup;
_stores: ?Array<FluxStore>;
_tokens: ?Array<{remove: () => void}>;
constructor() {
this._callbacks = [];
}
setStores(stores: Array<FluxStore>): void {
if (this._stores && shallowArrayEqual(this._stores, stores)) {
return;
}
this._stores = stores;
this._resetTokens();
this._resetStoreGroup();
let changed = false;
let changedStores = [];
if (__DEV__) {
// Keep track of the stores that changed for debugging purposes only
this._tokens = stores.map(store => store.addListener(() => {
changed = true;
changedStores.push(store);
}));
} else {
const setChanged = () => { changed = true; };
this._tokens = stores.map(store => store.addListener(setChanged));
}
const callCallbacks = () => {
if (changed) {
this._callbacks.forEach(fn => fn());
changed = false;
if (__DEV__) {
// Uncomment this to print the stores that changed.
// console.log(changedStores);
changedStores = [];
}
}
};
this._storeGroup = new FluxStoreGroup(stores, callCallbacks);
}
addListener(fn: () => void): void {
this._callbacks.push(fn);
}
reset(): void {
this._resetTokens();
this._resetStoreGroup();
this._resetCallbacks();
this._resetStores();
}
_resetTokens() {
if (this._tokens) {
this._tokens.forEach(token => token.remove());
this._tokens = null;
}
}
_resetStoreGroup(): void {
if (this._storeGroup) {
this._storeGroup.release();
this._storeGroup = null;
}
}
_resetStores(): void {
this._stores = null;
}
_resetCallbacks(): void {
this._callbacks = [];
}
}
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
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
# Resource
https://github.com/voronianski/flux-comparison https://github.com/facebook/flux https://blog.andrewray.me/flux-for-stupid-people/ https://www.cnblogs.com/fliu/articles/5245923.html
# History
- 20191023 add flux view
- 20210205 update flux concept