Flux analysis and practice

Origin:

After nearly a year of React applet development, there are some changes in thinking about the application’s state management framework. Project technology selection is to use Mobx at the beginning, the reason is that at that time to participate in the selection of classmates think Redux of template code and the learning curve doesn’t fit, it was under the background of this there is no problem (project members are Native client, for the front-end technology stack is not in-depth understanding, combined with project scheduling is very nervous at that time). Due to the complexity of the project itself, my feeling towards Mobx began to change. At the beginning, I thought it was a right decision to give up Redux and choose Mobx without any reason. Later, I found that considering the business scenario, Mobx may not be a good solution. Of course, I have not used Redux in actual projects, so I can’t directly say which one is better than the other. Therefore, I hope to pay attention to, learn and understand Redux from a personal perspective, and see whether Redux can solve some problems existing in Mobx, which is not only to make up for the shortcomings, but also to review the maintenance experience of the whole half year.

For the study of Redux, the book “Redux Actual Combat” is taken as an example.

Because of the non-professional front end, a lot of things still have to start with the simplest, such as Flux when it comes to Redux.

Body:

Front-end no matter Web or Android, the concept of architecture has developed for so many years. In my opinion, one of the core points still lies in how to make the relationship between View layer and Model layer clear. Thus, MVC, MVP, MVVM and other development frameworks are born. There are some practical differences between traditional Android MVC and Web MVC, but the concept is the same.

As shown above, these are ALL MVC or VARIATIONS of MVC. However, in terms of specifications, we prefer not to operate the Model layer directly in the View layer. Changes in the Model layer are triggered by the Controller layer. Then the Model layer notifies the View layer when changes are made to update the View.

However, if the specification is not mandatory, it is easy for later people to not comply with the specification. Rapid business iteration and scheduling pressure make many or many times, everyone is just as fast as possible. I can directly change the Model layer in the View layer, why do I need to do this through the Controller?

And Flux concept diagram:

  • View the View layer
  • Store Data manager
  • Dispatcher Event Dispatcher for Action
  • Action Action, used for interaction or other View layer operations

It clearly defines a paradigm, that is, how Flux programs should be written, Store updates can only be handled by Dispatcher, and Store notifies the View layer to render new views.

However, Flux is so old that there is no need to start over. Just find a demo to figure out how it works and prepare for Redux’s learning.

GitHub – ruanyf/extremely-simple-flux-demo: Learn flux from an extremely simple demo

About engineering structure:

MyButtonController is what the page shows.

MyButton is a Component that contains a UL tag and a button tag. When you click a button, add an item to the UL tag.

Button click events:

var MyButtonController = React.createClass({
  getInitialState: function () {
    return {
      items: ListStore.getAll()
    };
  },
  // Register listeners while the component is mounted
  componentDidMount: function() {
    ListStore.addChangeListener(this._onChange);
  },

  // Unbind listeners while uninstalling components
  componentWillUnmount: function() {
    ListStore.removeChangeListener(this._onChange);
  },

  // Listen for the callback and reset setState to trigger the view's re-rendering
  _onChange: function () {
    this.setState({
      items: ListStore.getAll()
    });
  },

  // Click the event to trigger the addNewItem pure function call of ButtonActions
  createNewItem: function (event) {
    ButtonActions.addNewItem('new item');
  },

  render: function() {
    return <MyButton
      items={this.state.items}
      onClick={this.createNewItem}
    />; }});Copy the code

ButtonActions defines various actions, each of which is a function

var ButtonActions = {

  addNewItem: function (text) {
    AppDispatcher.dispatch({
      actionType: 'ADD_NEW_ITEM'.text: text }); }};Copy the code

AddNewItem forwards an object to the Dispatch method of AppDispatcher that contains the type of the action and the content passed by the action.

var AppDispatcher = new Dispatcher();
AppDispatcher.register(function (action) {
  switch(action.actionType) {
    case 'ADD_NEW_ITEM':
      ListStore.addNewItemHandler(action.text);
      ListStore.emitChange();
      break;
    default:
      // no op}})Copy the code

AppDispatcher is a global Dispatcher object, and the implementation of the Dispatcher will be analyzed later. In short, we register a callback with the Dispatcher, and then send time through the AppDispatcher. Switch to the corresponding actionType, it will go to the corresponding case and complete the update of Store in it, that is:

ListStore.addNewItemHandler(action.text);
ListStore.emitChange();
Copy the code

Let’s look at the ListStore implementation:

var ListStore = assign({}, EventEmitter.prototype, {
  items: [].getAll: function () {
    return this.items;
  },

  // Add a text
  addNewItemHandler: function (text) {
    this.items.push(text);
  },

  // Submit the change event
  emitChange: function () {
    this.emit('change');
  },

  addChangeListener: function(callback) {
    this.on('change', callback);
  },

  removeChangeListener: function(callback) {
    this.removeListener('change', callback); }});Copy the code

After adding a text, the emitChange method is called in the APPDispatcher. Using the capabilities of EventEmitter, we can send a change event.

When the MyButtonController component is mounted, a listener for the change event is added to the ListStore, so finally, the _onChange method of MyButtonController is called and the view is updated via setState.

Let’s go back to this picture:

Throughout the workflow:

The View layer click event triggers an Action, which will be distributed through the Dispatcher and then execute the corresponding logic to update the Store data. After the Store is updated, the setState of the View will be triggered to re-render the View.

EventEmitter

EventEmmiter is a module in NodeJS that acts like EventBus. In the ListStore above, we used EventEmitter’s emit, on, and removeListener functions:

EventEmitter/EventEmitter. Js at master, Olical/EventEmitter, dead simple

; (function (exports) {
    'use strict';
    function EventEmitter() {}

    // Shortcuts to improve speed and size
    var proto = EventEmitter.prototype;
    var originalGlobalValue = exports.EventEmitter;

    function indexOfListener(listeners, listener) {
        var i = listeners.length;
        while (i--) {
            if (listeners[i].listener === listener) {
                returni; }}return -1;
    }
		// Support calling a function through an alias
    function alias(name) {
        return function aliasClosure() {
            return this[name].apply(this.arguments);
        };
    }

    proto.getListeners = function getListeners(evt) {
        var events = this._getEvents();
        var response;
        var key;

        // Return a concatenated array of all matching events if
        // the selector is a regular expression.
        if (evt instanceof RegExp) {
            response = {};
            for (key in events) {
                if(events.hasOwnProperty(key) && evt.test(key)) { response[key] = events[key]; }}}else {
            response = events[evt] || (events[evt] = []);
        }

        return response;
    };

    /**
     * Takes a list of listener objects and flattens it into a list of listener functions.
     *
     * @param {Object[]} listeners Raw listener objects.
     * @return {Function[]} Just the listener functions.
     */
    proto.flattenListeners = function flattenListeners(listeners) {
        var flatListeners = [];
        var i;

        for (i = 0; i < listeners.length; i += 1) {
            flatListeners.push(listeners[i].listener);
        }

        return flatListeners;
    };

    /**
     * Fetches the requested listeners via getListeners but will always return the results inside an object. This is mainly for internal use but others may find it useful.
     *
     * @param {String|RegExp} evt Name of the event to return the listeners from.
     * @return {Object} All listener functions for an event in an object.
     */
    proto.getListenersAsObject = function getListenersAsObject(evt) {
        var listeners = this.getListeners(evt);
        var response;

        if (listeners instanceof Array) {
            response = {};
            response[evt] = listeners;
        }

        return response || listeners;
    };

    function isValidListener (listener) {
        if (typeof listener === 'function' || listener instanceof RegExp) {
            return true
        } else if (listener && typeof listener === 'object') {
            return isValidListener(listener.listener)
        } else {
            return false}}/** * Register an event listener */
    proto.addListener = function addListener(evt, listener) {
        if(! isValidListener(listener)) {throw new TypeError('listener must be a function');
        }

        var listeners = this.getListenersAsObject(evt);
        var listenerIsWrapped = typeof listener === 'object';
        var key;

        for (key in listeners) {
            if (listeners.hasOwnProperty(key) && indexOfListener(listeners[key], listener) === -1) {
                listeners[key].push(listenerIsWrapped ? listener : {
                    listener: listener,
                    once: false}); }}return this;
    };

    // Alias calls the addListener function
    proto.on = alias('addListener');

    / /...
  
  	// 
    proto.removeListener = function removeListener(evt, listener) {
        var listeners = this.getListenersAsObject(evt);
        var index;
        var key;

        for (key in listeners) {
            if (listeners.hasOwnProperty(key)) {
                index = indexOfListener(listeners[key], listener);

                if(index ! = = -1) {
                    listeners[key].splice(index, 1); }}}return this;
    };

    /** * Alias of removeListener */
    proto.off = alias('removeListener');

    /** * An event is submitted, after which all callbacks listening for the event will be executed */
    proto.emitEvent = function emitEvent(evt, args) {
        var listenersMap = this.getListenersAsObject(evt);
        var listeners;
        var listener;
        var i;
        var key;
        var response;

        for (key in listenersMap) {
            if (listenersMap.hasOwnProperty(key)) {
                listeners = listenersMap[key].slice(0);

                for (i = 0; i < listeners.length; i++) {
                    // If the listener returns true then it shall be removed from the event
                    // The function is executed either with a basic call or an apply if there is an args array
                    listener = listeners[i];

                    if (listener.once === true) {
                        this.removeListener(evt, listener.listener);
                    }

                    response = listener.listener.apply(this, args || []);

                    if (response === this._getOnceReturnValue()) {
                        this.removeListener(evt, listener.listener); }}}}return this;
    };

    /** * Alias of emitEvent */
    proto.trigger = alias('emitEvent');

    proto.emit = function emit(evt) {
        // Extract the parameters
        var args = Array.prototype.slice.call(arguments.1);
        return this.emitEvent(evt, args);
    };
		
    / /...} (typeof window! = ='undefined' ? window : this| | {}));Copy the code

EventEmitter provides the eventBus capability, so that after a Store changes data, the Component in the View layer can be triggered to perform the corresponding setState to update the View by sending the corresponding event.

Dispatcher

Note that Flux is only a development specification, so some implementations of Flux are not unique. Let’s take a look at the implementation of Dispatcher:

Flux source code analysis – Jianshu.com

import { _classCallCheck, invariant } from './util'

class Dispatcher {
  constructor() {
    _classCallCheck(this, Dispatcher)
    this._callbacks = {}; / / callback map
    this._isDispatching = false;  // Whether an action is being sent
    this._isHandled = {}; / / state
    this._isPending = {}; / / wait state
    this._lastID = 1; // Callback map sequence number
  }

  // Throw a callback over
  register (callback) {
    const _prefix = 'ID_';
    var id = _prefix + this._lastID++;
    this._callbacks[id] = callback;
    return id;
  }

  // Delete a callback dictionary method based on id
  unregister (id) {
    !this._callbacks[id] ? process.env.NODE_ENV ! = ='production' ? invariant(false.'Dispatcher.unregister(...) : `%s` does not map to a registered callback.', id) : invariant(false) : undefined;
    delete this._callbacks[id];
  }

  // When dispatches (action) are triggered in the register order of the callback
  // If the waitFor method is called in the callback, the callback with the corresponding ID is processed first
  // When two stores are waiting for each other, they enter a deadlock state. That is, the waiting ID is pending.
  // But not finished (! IsHandled), which throws an exception (development) and skips it in production
  waitFor (ids) {
    !this._isDispatching ? process.env.NODE_ENV ! = ='production' ? invariant(false.'Dispatcher.waitFor(...) : Must be invoked while dispatching.') : invariant(false) : undefined;
    for (var ii = 0; ii < ids.length; ii++) {
      var id = ids[ii];
      if (this._isPending[id]) {
        !this._isHandled[id] ? process.env.NODE_ENV ! = ='production' ? invariant(false.'Dispatcher.waitFor(...) : Circular dependency detected while ' + 'waiting for `%s`.', id) : invariant(false) : undefined;
        continue; }!this._callbacks[id] ? process.env.NODE_ENV ! = ='production' ? invariant(false.'Dispatcher.waitFor(...) : `%s` does not map to a registered callback.', id) : invariant(false) : undefined;
      this._invokeCallback(id); }}// Distribute events
  dispatch (payload) {
    !!this._isDispatching ? process.env.NODE_ENV ! = ='production' ? invariant(false.'Dispatch.dispatch(...) : Cannot dispatch in the middle of a dispatch.') : invariant(false) : undefined;
    // Js is a single thread, so you can use a member variable to record the payload
    this._startDispatching(payload);
    try {
      // Iterate through all the callback
      for (var id in this._callbacks) {
        if (this._isPending[id]) {
          continue;
        }
        // Execute each callback
        this._invokeCallback(id); }}finally {
      this._stopDispatching();
    }
  }

  isDispatching () {
    return this._isDispatching;
  }

  _invokeCallback (id) {
    this._isPending[id] = true;
    // Callbacks are a map, so you can call the function payload directly
    this._callbacks[id](this._pendingPayload);
    this._isHandled[id] = true;
  }

  _startDispatching (payload) {
    for (var id in this._callbacks) {
      this._isPending[id] = false;
      this._isHandled[id] = false;
    }
    this._pendingPayload = payload;
    this._isDispatching = true;
  }

  _stopDispatching () {
    delete this._pendingPayload;
    this._isDispatching = false; }}export default Dispatcher
Copy the code

conclusion

The principle of Flux is actually very simple, more is to provide a one-way data flow development specification.