Immutable (English immutable) is an object whose state can not be changed after creation. The result of any modification of such an object will always be a new object, while the old object will not change.
var mutableArr = [1, 2, 3, 4]; arr.push(5); console.log(mutableArr); // [1, 2, 3, 4, 5] //Use seamless-immutable.js var immutableArr = Immutable([1, 2, 3, 4]); var newImmutableArr = immutableArr.concat([5]); console.log(immutableArr); //[1, 2, 3, 4]; console.log(newImmutableArr); //[1, 2, 3, 4, 5];
This is not about deep copying: if an object has a nested structure, then all nested objects that have not been modified will be reused.
//Use seamless-immutable.js var state = Immutable({ style : { color : { r : 128, g : 64, b : 32 }, font : { family : 'sans-serif', size : 14 } }, text : 'Example', bounds : { size : { width : 100, height : 200 }, position : { x : 300, y : 400 } } }); var nextState = state.setIn(['style', 'color', 'r'], 99); state.bounds === nextState.bounds; //true state.text === nextState.text; //true state.style.font === state.style.font; //true
In memory, objects will be represented as follows:
This feature is actively used in conjunction with today's popular VirtualDOM ( React , Mithril , Riot ) to speed up the redrawing of web pages.
Take the example of state
, just above. After modifying the state
object, you need to compare it with the nextState
object and find out exactly what has changed in it. Immunity greatly simplifies the task for us: instead of comparing the value of each field of each nested state
object with the corresponding value from the nextState
, you can simply compare references to the corresponding objects and thus filter out the entire nested branches of comparisons.
state === nextState //false state.text === nextState.text //true state.style === nextState.style //false state.style.color === nextState.style.color //false state.style.color.r === nextState.style.color.r //false state.style.color.g === nextState.style.color.g //true state.style.color.b === nextState.style.color.b //true state.style.font === nextState.style.font; //true //state.style.font.family === nextState.style.font.family; //true //state.style.font.size === nextState.style.font.size; //true state.bounds === nextState.bounds //true //state.bounds.size === nextState.bounds.size //true //state.bounds.size.width === nextState.bounds.size.width //true //state.bounds.size.height === nextState.bounds.size.height //true //state.bounds.position === nextState.bounds.position //true //state.bounds.position.x === nextState.bounds.position.x //true //state.bounds.position.y === nextState.bounds.position.y //true
Inside the bounds
and style.font
objects, style.font
comparison operation is necessary, since they are immutable, and the references to them have not changed.
There are often cases when the data passed to the function can be accidentally corrupted, and it is very difficult to track down such situations.
var arr = [2, 1, 3, 5, 4, 0]; function render(items) { return arr .sort(function(a, b) {return a < b ? -1 : a > b ? 1 : 0}) .map(function(item){ return '<div>' + item + '</div>'; }); } render(arr); console.log(arr); // [0, 1, 2, 3, 4, 5]
Here immobile data would save the situation. The sort
function would be disabled.
//Use seamless-immutable.js var arr = [2, 1, 3, 5, 4, 0]; function render(items) { return items .sort(function(a, b) {return a < b ? -1 : a > b ? 1 : 0}) .map(function(item){ return '<div>' + item + '</div>'; }); } render(arr); //Uncaught Error: The sort method cannot be invoked on an Immutable data structure. console.log(arr);
Or would return a new sorted array without changing the old one:
//Use immutable.js var arr = Immutable.fromJS([2, 1, 3, 5, 4, 0]); function render(items) { return arr .sort(function(a, b) {return a < b ? -1 : a > b ? 1 : 0}) .map(function(item){ return '<div>' + item + '</div>'; }); } render(arr); console.log(arr.toJS()); // [2, 1, 3, 5, 4, 0]
Each time an immutable object is modified, a copy of it is created with the necessary changes. This leads to more memory consumption than when working with a regular object. But since immutable objects never change, they can be implemented using a strategy called “structural sharing”, which generates a much lower cost of memory costs than one would expect. Compared to embedded arrays and objects, costs will still exist, but it will have a fixed value and can usually be compensated for by other benefits available due to immutability.
In most cases, it will not be easier to cache. This example will clarify the situation:
var step_1 = Immutable({ data : { value : 0 } }); var step_2 = step_1.setIn(['data', 'value'], 1); var step_3 = step_2.setIn(['data', 'value'], 0); step_1.data === step_3.data; //false
Despite the fact that data.value
from the first step does not differ from data.value
from the last step, the data
object itself is different, and the link to it also changed.
This is also not true:
function test(immutableData) { var value = immutableData.get('value'); window.title = value; return immutableData.set('value', 42); }
There is no guarantee that the function will become pure , or that it will have no side effects .
Not everything is so obvious here, and performance depends on the concrete implementation of the immutable data structures with which you have to work. But if you take and simply freeze the object using Object.freeze
, then access to it and its properties will not become faster, and in some browsers it will become even slower.
JavaScript is single-threaded, and there's nothing to talk about here. Many people confuse asynchrony and multithreading - this is not the same thing.
By default, there is only one thread that asynchronously serves the message queue.
In a browser for multithreading there is WebWorkers, but the only possible communication between threads is through sending strings or serialized JSON; it is not possible to refer to the same variables from different workers.
Using const
instead of var
or let
does not mean that the value is constant or that it is immutable (immutable). The const
keyword simply tells the compiler to ensure that no other value is assigned to the variable anymore.
If const
used, modern JavaScript engines can perform a number of additional optimizations.
Example:
const obj = { text : 'test'}; obj.text = 'abc'; obj.color = 'red'; console.log(obj); //Object {text: "abc", color: "red"} obj = {}; //Uncaught TypeError: Assignment to constant variable.(…)
The Object.freeze
method freezes an object. This means that it prevents the addition of new properties to an object, the removal of old properties from an object, and the modification of existing properties or the values ​​of their enumeration, customizability, and recordability attributes. In essence, the object becomes effectively unchanged. The method returns a frozen object.
The library offers immutable data structures that are backward compatible with regular arrays and objects. That is, access to values ​​by key or index will not differ from the usual, standard cycles will work, and all this can be used in conjunction with specialized high-performance libraries for data manipulation, such as Lodash or Underscore .
var array = Immutable(["totally", "immutable", {hammer: "Can't Touch This"}]); array[1] = "I'm going to mutate you!" array[1] // "immutable" array[2].hammer = "hm, surely I can mutate this nested object..." array[2].hammer // "Can't Touch This" for (var index in array) { console.log(array[index]); } // "totally" // "immutable" // { hammer: 'Can't Touch This' } JSON.stringify(array) // '["totally","immutable",{"hammer":"Can't Touch This"}]'
This library uses Object.freeze
, and also prohibits the use of methods that can change data.
Immutable([3, 1, 4]).sort() // This will throw an ImmutableError, because sort() is a mutating method.
Some browsers, such as Safari, have performance problems when working with objects frozen using Object.freeze
, so this is disabled in the production
assembly for increased performance.
Thanks to Facebook's promotion, this library for working with immunity data has become the most common and popular among web developers. It provides the following immutable data structures:
var list = Immutable.List([1, 3, 2, 4, 5]); console.log(list.size); //5 list = list.pop().pop(); //[1, 3, 2] list = list.push(6); //[1, 3, 2, 6] list = list.shift(); //[3, 2, 6] list = list.concat(9, 0, 1, 4); //[3, 2, 6, 9, 0, 1, 4] list = list.sort(); //[0, 1, 2, 3, 4, 6, 9]
var stack = new Immutable.Stack(); stack = stack.push( 2, 1, 0 ); stack.size; stack.get(); //2 stack.get(1); //1 stack.get(2); //0 stack = stack.pop(); // [1, 0]
var map = new Immutable.Map(); map = map.set('value', 5); //{value : 5} map = map.set('text', 'Test'); //{value : 5, text : "Test"} map = map.delete('text'); // {value : 5}
var map = new Immutable.OrderedMap(); map = map.set('m', 5); //{m : 5} map = map.set('a', 1); //{m : 5, a : 1} map = map.set('p', 8); //{m : 5, a : 1, p : 8} for(var elem of map) { console.log(elem); }
var s1 = Immutable.Set( [2, 1] ); var s2 = Immutable.Set( [2, 3, 3] ); var s3 = Immutable.Set( [1, 1, 1] ); console.log( s1.count(), s2.size, s3.count() ); // 2 2 1 console.log( s1.toJS(), s2.toArray(), s3.toJSON() ); // [2, 1] [2, 3] [1] var s1S2IntersectArray = s1.intersect( s2 ).toJSON(); // [2]
var s1 = Immutable.OrderedSet( [2, 1] ); var s2 = Immutable.OrderedSet( [2, 3, 3] ); var s3 = Immutable.OrderedSet( [1, 1, 1] ); var s1S2S3UnionArray = s1.union( s2, s3 ).toJSON();// [2, 1, 3] var s3S2S1UnionArray = s3.union( s2, s1 ).toJSON();// [1, 2, 3]
var Data = Immutable.Record({ value: 5 }); var Test = Immutable.Record({ text: '', data: new Data() }); var test = new Test(); console.log( test.get('data').get('value') ); //5 the default value
A library that adds persistent data structures from ClojureScript (Lists, Vectors, Maps, etc.) to JavaScript.
Differences from Immutable.js:
Usage example:
var inc = function(n) { return n+1; }; mori.intoArray(mori.map(inc, mori.vector(1,2,3,4,5))); // => [2,3,4,5,6] //Efficient non-destructive updates! var v1 = mori.vector(1,2,3); var v2 = mori.conj(v1, 4); v1.toString(); // => '[1 2 3]' v2.toString(); // => '[1 2 3 4]' var sum = function(a, b) { return a + b; }; mori.reduce(sum, mori.vector(1, 2, 3, 4)); // => 10 //Lazy sequences! var _ = mori; _.intoArray(_.interpose("foo", _.vector(1, 2, 3, 4))); // => [1, "foo", 2, "foo", 3, "foo", 4]
It will be about using Immutable.js (with Mori everything is also about). If you work with Seamless-Immutable , you will not have such problems due to backward compatibility with native JavaScript structures.
The fact is that in most cases the server API accepts and returns data in JSON format, which corresponds to standard objects and arrays from JavaScript. This means that it will be necessary to somehow convert the Immutable data to normal and vice versa.
Immutable.js for converting normal data into immutable offers the following function:
Immutable.fromJS(json: any, reviver?: (k: any, v: Iterable<any, any>) => any): any
where using the reviver
function reviver
can add your own conversion rules and manage existing ones.
Suppose the server API returns the following object to us:
var response = [ {_id : '573b44d91fd2f10100d5f436', value : 1}, {_id : '573dd87b212dc501001950f2', value : 2}, {_id : '5735f6ae2a380401006af05b', value : 3}, {_id : '56bdc2e1cee8b801000ff339', value : 4} ]
Most conveniently such an object will be represented as an orderedmap. reviver
appropriate reviver
:
var state = Immutable.fromJS(response, function(k, v){ if(Immutable.Iterable.isIndexed(v)) { for(var elem of v) { if(!elem.get('_id')) { return elem; } } var ordered = []; for(var elem of v) { ordered.push([elem.get('_id'), elem.get('value')]); } return Immutable.OrderedMap(ordered); } return v; }); console.log(state.toJS()); //Object {573b44d91fd2f10100d5f436: 1, 573dd87b212dc501001950f2: 2, 5735f6ae2a380401006af05b: 3, 56bdc2e1cee8b801000ff339: 4}
Suppose we need to change the data and send it back to the server:
state = state.setIn(['573dd87b212dc501001950f2', 5]); console.log(state.toJS()); //Object {573b44d91fd2f10100d5f436: 1, 573dd87b212dc501001950f2: 5, 5735f6ae2a380401006af05b: 3, 56bdc2e1cee8b801000ff339: 4}
Immutable.js for converting immobile data into regular data offers the following function:
toJS(): any
As you can see, the reviver
missing, which means that you have to write your own external immutableHelper
. And he should somehow be able to distinguish the usual OrderMap from the one that matches the structure of your source data. You cannot inherit from OrderMap either. In this application, the structures are likely to be nested, which will add to your additional difficulties.
You can, of course, use only List and Map in development, but then why is everything else? And what are the advantages of using Immutable.js specifically?
If the project used to work with native data structures, then it is not easy to switch to immutable data. We'll have to rewrite all the code that somehow interacts with the data.
Immutable.js offers us nothing but functions fromJS
, toJS
, which work like this:
var set = Immutable.Set([1, 2, 3, 2, 1]); set = Immutable.fromJS(set.toJS()); console.log(Immutable.Set.isSet(set)); //false console.log(Immutable.List.isList(set)); //true
That is absolutely useless for serialization / deserialization.
There is a third-party library transit-immutable-js . An example of its use:
var transit = require('transit-immutable-js'); var Immutable = require('immutable'); var m = Immutable.Map({with: "Some", data: "In"}); var str = transit.toJSON(m); console.log(str) // ["~#cmap",["with","Some","data","In"]] var m2 = transit.fromJSON(str); console.log(Immutable.is(m, m2));// true
Benchmarks were written for performance testing. To run them at home, run the following commands:
git clone https://github.com/MrCheater/immutable-benchmarks.git cd ./immutable-benchmarks npm install npm start
The results of benchmarks can be seen on the graphs (repeats / ms). The longer the execution time, the worse the result.
When reading, native data structures and Seamless-immutable turned out to be the fastest.
When recording was the fastest Mori. Seamless-immutable showed the worst result.
This article will be useful to JavaScript developers who are faced with the need to use immune data in their applications to improve performance. In particular, this applies to frontend developers who work with frameworks using VirtualDOM ( React , Mithril , Riot ), as well as Flux / Redux solutions.
Summing up, it can be said that among the considered libraries for immobility in JavaScript the most fast, convenient and easy to use is Seamless-immutable . The most stable and common is Immutable.js . The fastest to write and the most unusual is Mori . I hope this study will help you choose a solution for your project. Good luck.
Source: https://habr.com/ru/post/302118/
All Articles