Flux-Core-Concept

Stephen Cui ... 2019-10-14 13:39:34
  • Flux
About 4 min

# Flux

# Data Flow Concept

      	         * -------- Action ----- *
                 |                       |
                 v                       ^
Action ---> Dispatcher ---> Store ---> View
1
2
3
4

# Core Feature

  • Action
    • Define actionType and action data by action creator
    • Invoke an action with a dispatcher
  • Dispatcher
    • Only one dispatcher
    • Register/unregister subscriber to Dispatcher
    • Dispatch action with payload to subscribers if store not pending
    • WaitFor is used to send action to group of ids
  • 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)
  • View
    • Use Flux Container to wrapper a view and Container with subscriptions to listener Store 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

# Source Code Analysis

# Dispatcher

  1. 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
  1. 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
  1. 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

# Store

  1. 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
  1. addListener
  addListener(callback: (eventType?: string) => void): {remove: () => void} {
    return this.__emitter.addListener(this.__changeEvent, callback);
  }
1
2
3

# Action

  1. 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
  1. Actions need dispatcher to dispatch an event with type and payload data
editTodo(id, text) {
    TodoDispatcher.dispatch({
      type: TodoActionTypes.EDIT_TODO,
      id,
      text,
    });
  },
1
2
3
4
5
6
7

# View

  • Listen store change event and get newest data from store 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

# 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

# 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

# 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

# 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
Last update: October 7, 2021 04:52
Contributors: Stephen Cui