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.