Hello, my name is Dmitry Karlovsky and I ... a wealthy person. I have a state on the server, there are states in local storages, there is a browser window state, there is a domain model state, there is an interface state. And all this variety of states must be kept synchronized. If one state changes somehow, the other states associated with it should be updated as soon as possible. A special piquancy of the situation is given by the fact that synchronization with the server can take seconds, and the user interface can only be blocked for a split second.
Then you will learn how reactivity conquers asynchrony, how imperative reactivity gets along with functional, how simple abstractions allow you to write reliable and fast code, and how I once switched to the idempotent side of force and wrap everything
First of all, it is necessary to determine the concept of "reactivity" (reactivity). This is a phenomenon when a change in one state leads to a cascade change in other states. Reactive programming uses this principle to describe the rules for changing one state when another changes. In the following, the unit of the reactive state for simplicity will be called the "atom".
If you have never programmed in the reactive paradigm, then I can recommend to read my article two years ago , which explains why it is needed at all. Nevertheless, it is slightly out of date, since $ mol_atom implements more advanced logic.
[ State1 ]----/ Rule1-2 /---->[ State2 ]----/ Rule2-3 /---->[ State3 ]
Reactive rules can be described in three paradigms:
Restarts . The rules are stored separately from the state and are a procedure that changes some states based on other states. When any state involved by this procedure changes, the procedure automatically restarts. The problem of this approach is in the stationarity of synchronization rules, large overheads and the risk of going into an infinite loop.
[ State ]<-------->[ Rule1-2( State ) ] [ State ]<-------->[ Rule2-3( State ) ]
Examples of implementations: AngularJS @ 1 , MeteorJS .
Pushing . Inside the atom are stored the rules for the formation of states of other atoms. Usually the rules are set during application initialization and do not change in the future, and if they change, then manually. The APIs of the libraries implementing this approach are greatly inflated — tens and hundreds of methods solve its many problems: immediate execution of the rules, dependence on a single atom, response even if the new value is equal to the old one and others.
[ State1 => Rule2( State1 ) ]-------->[ State2 => Rule3( State2 ) ]-------->[ State3 ]
Examples of implementations: BaconJS , KefirJS , RxJS .
Pulling . Inside the atom is stored the rule of formation of its state as a function of the states of other atoms. The state is formed lazily, at the moment of addressing it. If someone ceases to depend on the state of the atom, then he can “fall asleep” losing dependence on other atoms. In the "laziness", dynamic automatic activation and deactivation of the rules the main advantage of this approach, therefore, the further narration will go about it.
[ State1 ]-------->[ State2 = Rule2( State1 ) ]-------->[ State3 = Rule3( State2 ) ]
Examples of implementations: KnockOutJS , MobXJS , CellX and $ mol_atom itself .
It should be noted that the above division of principles is conditional and reflects the basic principle of the work of the respective libraries.
Create a pair of mutable atoms with a calculated default value:
const userName = new $mol_atom( 'userName' , next => next || 'Anonymous' ) const showName = new $mol_atom( 'showName ' , next => next || false )
Create a computed atom that does not allow a direct change in its value:
const greeting = new $mol_atom( 'greeting' , next => { if( !showName.value() ) return 'Hello!' return `Hello, ${ userName.value() }!` } )
Create a presentation atom that displays a greeting to the console whenever something in the data changes:
const presenting = new $mol_atom( 'presenting' , next => { console.log( greeting.value() ) } )
Forcibly activate the presentation atom:
presenting.value() //Hello!
We change 2 atoms at once, but there will be only one output:
showName.value( true ) userName.value( 'John' ) //Hello, John!
Forcibly present after each change of data:
userName.value( 'Jin' ) presenting.value() // Hello, Jin! showName.value( false ) presenting.value() // Hello!
We are trying to change the greeting directly - nothing comes out:
greeting.value( 'Hi!' )
Forcibly set a greeting to bypass the rules:
greeting.value( 'Hi!' , $mol_atom_force ) //Hi!
Change the original data - zero reaction:
showName.value( false )
Force update the value of greeting according to the rules:
greeting.value( void null , $mol_atom_force ) //Hello!
We enable logging of all atoms and change one of them:
$mol_log.filter( '' ) showName.value( true ) //21:44:11 showName.value() ["push", true, false] //21:44:11 greeting.value() ["obsolete"] //21:44:11 $mol_atom.sync [] //21:44:11 userName.value() ["push", "Anonymous", undefined] //21:44:11 greeting.value() ["push", "Hello, Anonymous!", "Hello!"] //21:44:11 presenting.value() ["obsolete"] //Hello, Anonymous!
Turn off logging:
$mol_log.filter( null )
It would seem that it could be simpler: when changing one state, to cause recalculation of dependent states. But..
First of all, you need to decide what depends on.
Suppose you are doing a blog and on the page of some post you need to display the title of the post in the window title, and on the page of posts filtered by tag - the name of the tag. It turns out that the state of the window title dynamically changes its dependencies. It depends on the name of the post, then on the name of the tag, but in both cases it depends on some condition that determines what it needs to depend on.
Already in this simple example, one can see how important the support of dynamic dependencies is. Therefore, we do not accept such an abstraction as "stream", around which the popular "push" libraries are built.
A typical "pull" implementation works as follows:
When an atom calculates its state, it first puts itself in a global variable, which allows other atoms to understand which atom is being calculated.
Next, he creates an empty list of his dependencies, in which all atoms will be registered, to which one way or another will be addressed.
Only now is the execution of the formula - a normal function that returns some result.
In the process of executing a formula, there may be a call to any other functions, objects, and browser interfaces.
If in the process of executing a formula, the value of another atom is addressed, then the one, taking the dependent atom from the global variable, links them in such a way that both will know that the value of one depends on the value of the other.
After the formula is calculated, the resulting value is stored in the atom, so that later it can be returned immediately, without a relatively long calculation of the formula.
Finally, a new list of dependencies is compared with the old one, in order to “separate” the atoms that are no longer dependent on each other.
Thus, we always have an actual network of atoms, which is dynamically rebuilt, reflecting real dependencies at a given time.
It is worth noting that the formulas (and, as a result, all the functions called in one way or another) are required to be idempotent. That is, if no dependency has changed, then the result of the function should remain unchanged. The result here is understood not only the value returned by the formula, but also the side effects produced in the calculation process.
Thanks to ideponentnosti, it is possible to cache the result for an indefinite period and reset the cache only when it is either no longer needed or not relevant anymore.
But what if there is an exception when calculating the value? If it is not intercepted, then the current "calculated atom" placed in the global variable will remain so there that in the future work of the application may lead to the appearance of very strange dependencies, which is not quite easy to debug. In addition, every time an atom is accessed, a formula will be calculated and an exception will pop up, which not only clogs the console on large amounts of data, but also causes non-weak brakes. And the cherry on the cake of problems will be that some of the atoms will remain in irrelevant condition.
In simple terms, the state of the application will no longer be consistent and will start to work unstably. To prevent this, each atom must intercept the exception and save it in itself instead of the value and allow it to surface further. Thus, the next time the atom is accessed, this exception will be thrown again. If the exception was the result of incorrect data, then as soon as the data becomes correct, the atom will recalculate its value and, instead of throwing an exception, will return the current value.
But at what point should the dependent atoms recalculate their values? If you do this right away when you receive a notice of obsolescence, an atom can fulfill a formula many times in vain, when several of its dependencies sequentially change. Therefore, in good implementations, the atom is immediately only marked out-of-date, but now it is already deferred.
Generally speaking, the problem of extra calculations is not as harmless as it may seem. It can lead to the following unpleasant consequences:
There are several strategies for deferred actualization of atoms:
In order of obsolescence. At the time of obsolescence, the atom is added to the end of the queue for recalculation. The simplest strategy. However, it leaves a fairly large number of unnecessary recalculations.
In order of creation. Each atom is given a numeric identifier. Later created atoms have a larger identifier value. Atoms are counted starting at the atom with the lowest identifier. It turns out that dependent atoms are recalculated earlier than those on which they depend, since they are often created in the process of calculating dependent ones. All this again leads to unnecessary calculations.
In order of increasing depth. Atoms are sorted by maximum depth of dependencies. At first, atoms without dependencies are recalculated, then dependent on atoms without dependencies, then on atoms that depend on atoms without dependencies, and so on. With this scheme, there is almost no extra recalculation. However, sometimes there are incidents when the atom has a shallow depth, but its existence is determined by an atom with greater depth. Thus, he recounts his value first, and later removes it, since it is no longer necessary.
To ensure the correct order of actualization, each atom can be in one of 4 states:
Obsolete . At the next call, its value will be calculated by the formula. When an atom enters this state, it notifies the dependent atoms that they are "possibly outdated."
Perhaps outdated (checking). On the next appeal, he will first make sure that all his dependencies are up to date. As soon as one of them changes its value, the atom will become "obsolete" with all the consequences. Otherwise, it will become "relevant" without recalculating the value. When an atom enters the state of "possibly outdated", it immediately notifies the dependent atoms that they are also "possibly outdated." Thus, this state is cascaded to the entire dependent subtree. If nobody depends on this atom, then it adds itself to the queue for deferred update.
Actual . When referencing, returns the memorized value. If during the transition to the current state, its value has changed, then it notifies the dependent atoms that they are "obsolete".
This logic may seem overly complex and redundant, but it does guarantee that:
Here and further examples are in TypeScript, which is ES6 with the addition of typing after the colon.
Working with atoms directly is not very convenient. They need to be given unique names so that something meaningful is displayed in the logs, and not just abstract numbers. It is necessary to store them somewhere and not to create once again. To get rid of this routine, we introduce the concept of "property" as a polymorphic method, which, depending on the number of parameters, "returns" or "sets and immediately returns" some value.
A common property has the following interface:
{ < Value >() : Value < Vlaue >( nextValue : Value ) : Value }
For example:
class App { title( next? : string ) { if( next !== void null ) document.title = next return document.title } }
It can be made reactive (cached with automatic cache invalidation) by simply adding the $mol_mem()
decorator:
class App { @ $mol_mem() title( next? : string ) { if( next !== void null ) document.title = next return document.title } }
Let's make the code of the greeting application as a class (for convenience, we use $ mol_object to help generate the correct names, but you can not use it by defining the toString method of the object manually):
class App extends $mol_object { @ $mol_mem() userName( next? : string ) { return next || 'Anonymous' } @ $mol_mem() showName( next? : boolean ) { return next || false } greeting() { if( !this.showName() ) return 'Hello!' return `Hello, ${ this.userName() }!` } @ $mol_mem() presenting() { console.log( this.greeting() ) } }
As you can see, the greeting
property is not reactive, so this method will be called every time you access it. But showName
is reactive, so the method will be called only at the first reading of the default value and when the new value is passed to it.
We could declare properties in the spirit of MobX, but for this, we would have to write more cumbersome code with duplication of the name of the property:
class App { @observable get userName() { return 'Anonymous' } set userName( next : string ) { return next } @observable get showName() { return false } set showName( next : boolean ) { return next } get greeting() { if( !this.showName ) return 'Hello!' return `Hello, ${ this.userName }!` } @computed get presenting { console.log( this.greeting ) } }
In addition, it would not be as easy and simple to overload the property as a whole from the outside, as in the following example:
class My extends $mol_object { @ $mol_mem() static instance() { return new this } name(){ return `Jin #${ Date.now() }` } @ $mol_mem() showName( next ) { return ( next === void null ) ? true : next } @ $mol_mem() app() { const app = new App app.userName = ()=> this.name() app.showName = ( next )=> this.showName( next ) return app } } My.instance().app().presenting() //Hello, Jin #1481383086982!
Here we used property overloading to elegantly create one-sided binding for the userName
property and two-sided for the showName
property. This is a very powerful technique that allows you to fine-tune the behavior of any object, immediately after its creation, without the risk of breaking it.
It is worth paying attention to exceptionally clear logs, which clearly show which states have changed as:
$mol_log.filter( '' ) My.instance().app().showName( false ) $mol_log.filter( null ) //11:27:58 My.instance().showName() ["push", false, true] //11:27:58 My.instance().app().presenting() ["obsolete"] //Hello!
It is often necessary to work not with one value, but with a whole family of values ​​distinguished by a certain key, but according to the same logic. For these cases, the following interface is used:
{ < Key , Value >( key : Kay ) : Value < Key , Vlaue >( key : Key , nextValue : Value ) : Value }
For example, let's create the simplest class that allows using REST resources:
class Rest extends $mol_object { @ $mol_mem_key() static resource( uri : string , next? : any , force : $mol_atom_force ) { const request = new XMLHttpRequest const method = ( next === void null ) ? 'get' : 'put' request.onload = ( event : Event )=> { this.resource( uri , request.responseText , $mol_atom_force ) } request.onerror = ( event : ErrorEvent )=> { setTimeout( ()=> { this.resource( uri , event.error || new Error( 'Unknown HTTP error' ) , $mol_atom_force ) } ) } request.open( method , uri ) request.send( next ) throw new $mol_atom_wait( `${ method } ${ uri }` ) } }
Separate atoms will be created for different uri. But when referring to the same uri, the same atom will be used. As you can see, the logic for obtaining data and setting values ​​is completely the same. The only difference is that when requesting data, the http-method "get" will be used, and during data transfer - "put".
Since atoms are able to work adequately with exceptional situations, here we used this property of theirs to abstract the application code from asynchrony. Since the data is not yet loaded, we cannot immediately return it. Instead, a special exception is thrown and the calculation of properties dependent on this property is interrupted pending any changes. When a response comes from the server, it is set as the property value and the dependent atoms "come to life", but this time their calculation is not interrupted. It is worth noting that the error handler can be called by the browser synchronously, so the property is set deferred to ensure that it is installed, and not $ mol_atom_wait, which rushes further.
Let's implement an application that draws all the emoji that the githab supports:
class App extends $mol_object { @ $mol_mem() static presenting() { const emojis = JSON.parse( Rest.resource( 'http://api.github.com/emojis' ) ) document.body.innerHTML = '' for( let id in emojis ) { const image = document.createElement( 'img' ) image.src = emojis[ id ] document.body.appendChild( image ) } } } App.presenting()
As you can see, we did not have to dance with callbacks, promises, generators, streams, asynchronous functions and other hellish creatures. Instead, our code is simple and straightforward.
A more complex example of parallel file downloads is discussed in a detailed article about $ mol .
Light version - only 250KB.
All this is only to write combinators of combinators of a heap of small closures instead of sequential code. Maintainers of AngularJS @ 2, after all, cannot be mistaken. Speaker with * JsConfTalksMeetUpDays waving his hands convincingly about it. This is how you need to write code in 2k16:
const greeting = showName .select( showName => { if( showName ) return userName.map( userName => `Hello, ${ userName }!` ) return Rx.Observable.from([ 'Hello!' ]) } ) .switch()
And for such an obsolete product, you need to tear off your hands:
greeting() { if( this.showName() ) return `Hello, ${ this.userName() }!` else return 'Hello!' }
Built into almost every browser, they allow you to write ... chains of chains of small closures. After all, this is what the code of this signor looks like:
let _config const getConfig = ()=> { if( _config ) return _config return _config = $.get( 'config.json' ).then( JSON.parse ) } let _profile const getProfile = ()=> { if( _profile ) return _profile return _profile = $.get( 'profile.json' ).then( JSON.parse ) } const getGreeting = ()=> getConfig() .then( config => { if( !config.showName ) return 'Hello!' return getProfile() .then( profile => `Hello, ${profile.userName}!` ) } )
This same code is simply impossible to maintain:
@ $mol_mem() config() { return JSON.parse( Rest.resource( 'config.json' ) ) } @ $mol_mem() profile() { return JSON.parse( Rest.resource( 'profile.json' ) ) } @ $mol_mem() greeting() { if( !this.config().showName ) return 'Hello!' return `Hello, ${ this.profile().userName }!` }
You are at the cutting edge of technology. On so acute that many browsers do not understand what you are writing:
let _config const getConfig = async ()=> { if( _config ) return _config return _config = JSON.parse( await $.get( 'config.json' ) ) } let _profile const getProfile = async ()=> { if( _profile ) return _profile return _profile = JSON.parse( await $.get( 'profile.json' ) ) } const getGreeting = async ()=> { if( !( await getConfig() ).showName ) return 'Hello!' return `Hello, ${ ( await getProfile() ).userName }!` }
'use strict'; function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } var _config = void 0; var getConfig = function () { var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee() { return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: if (!_config) { _context.next = 2; break; } return _context.abrupt('return', _config); case 2: _context.t0 = JSON; _context.next = 5; return $.get('config.json'); case 5: _context.t1 = _context.sent; return _context.abrupt('return', _config = _context.t0.parse.call(_context.t0, _context.t1)); case 7: case 'end': return _context.stop(); } } }, _callee, undefined); })); return function getConfig() { return _ref.apply(this, arguments); }; }(); var _profile = void 0; var getProfile = function () { var _ref2 = _asyncToGenerator(regeneratorRuntime.mark(function _callee2() { return regeneratorRuntime.wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: if (!_profile) { _context2.next = 2; break; } return _context2.abrupt('return', _profile); case 2: _context2.t0 = JSON; _context2.next = 5; return $.get('profile.json'); case 5: _context2.t1 = _context2.sent; return _context2.abrupt('return', _profile = _context2.t0.parse.call(_context2.t0, _context2.t1)); case 7: case 'end': return _context2.stop(); } } }, _callee2, undefined); })); return function getProfile() { return _ref2.apply(this, arguments); }; }(); var getGreeting = function () { var _ref3 = _asyncToGenerator(regeneratorRuntime.mark(function _callee3() { return regeneratorRuntime.wrap(function _callee3$(_context3) { while (1) { switch (_context3.prev = _context3.next) { case 0: _context3.next = 2; return getConfig(); case 2: if (_context3.sent.showName) { _context3.next = 4; break; } return _context3.abrupt('return', 'Hello!'); case 4: _context3.next = 6; return getProfile(); case 6: _context3.t0 = _context3.sent.userName; _context3.t1 = 'Hello, ' + _context3.t0; return _context3.abrupt('return', _context3.t1 + '!'); case 9: case 'end': return _context3.stop(); } } }, _callee3, undefined); })); return function getGreeting() { return _ref3.apply(this, arguments); }; }();
, - :
@ $mol_mem() config() { return JSON.parse( Rest.resource( 'config.json' ) ) } @ $mol_mem() profile() { return JSON.parse( Rest.resource( 'profile.json' ) ) } @ $mol_mem() greeting() { if( !this.config().showName ) return 'Hello!' return `Hello, ${ this.profile().userName }!` }
, config
, !
$mol_atom $mol . , . , (-, ) , . , . , , . $mol_atom+$mol_mem 25KB .
Source: https://habr.com/ru/post/317360/
All Articles