Иммутабельность в JavaScript

  • Tutorial

habracut


Что такое иммутабельность


Неизменяемым (англ. immutable) называется объект, состояние которого не может быть изменено после создания. Результатом любой модификации такого объекта всегда будет новый объект, при этом старый объект не изменится.


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];

Речь не идет о глубоком копировании: если объект имеет вложенную структуру, то все вложенные объекты, не подвергшиеся модификации, будут переиспользованы.


//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


Правда или ложь? Иммутабельные данные в JavaScript


Простое и быстрое отслеживание изменений


Эту возможность активно используют в связке с популярным нынче VirtualDOM (React, Mithril, Riot) для ускорения перерисовки web-страниц.


Возьмем пример с state, приведенный чуть выше. После модификации объекта state нужно сравнить его с объектом nextState и узнать, что конкретно в нем изменилось. Иммутабельность сильно упрощает нам задачу: вместо того, чтобы сравнивать значение каждого поля каждого вложенного в state объекта с соответствующим значением из nextState, можно просто сравнивать ссылки на соответствующие объекты и отсеивать таким образом целые вложенные ветки сравнений.


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

Внутри объектов bounds и style.font операции сравнения производить не нужно, так как они иммутабельны, и ссылки на них не изменились.


Безопаснее использовать и легче тестировать


Нередки случаи, когда переданные в функцию данные могут быть случайно испорчены, и отследить такие ситуации очень сложно.


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]

Здесь иммутабельные данные спасли бы ситуацию. Функция sort была бы запрещена.


//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);

Или вернула бы новый отсортированный массив, не меняя старый:


//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]

Больший расход памяти


Каждый раз при модификации иммутабельного объекта создается его копия с необходимыми изменениями. Это приводит к большему расходу памяти, чем при работе с обычным объектом. Но поскольку иммутабельные объекты никогда не меняются, они могут быть реализованы с помощью стратегии, называемой «общие структуры» (structural sharing), которая порождает гораздо меньшую издержку в затратах на память, чем можно было бы ожидать. В сравнении со встроенными массивами и объектами издержка все еще будет существовать, но она будет иметь фиксированную величину и обычно может компенсироваться другим преимуществами, доступными благодаря неизменяемости.


Легче кешировать (мемоизировать)


В большинстве случаев кешировать легче не станет. Этот пример прояснит ситуацию:


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

Несмотря на то, что data.value из первого шага не отличается от data.value из последнего шага, сам объект data уже другой, и ссылка на него тоже изменилась.


Отсутствие побочных эффектов


Это тоже неправда:


function test(immutableData) {
   var value = immutableData.get('value');
   window.title = value;
   return immutableData.set('value', 42);
}

Гарантий того, что функция станет чистой, или что у нее будут отсутствовать побочные эффекты — нет.


Ускорение кода. Больше простора для оптимизаций


Здесь не все так очевидно, и производительность зависит от конкретной реализации иммутабельных структур данных, с которой приходится работать. Но если взять и просто заморозить объект при помощи Object.freeze, то обращение к нему и его свойствам быстрее не станет, а в некоторых браузерах станет даже медленее.


Thread safety


JavaScript — однопоточный, и говорить тут особо не о чем. Многие путают асинхронность и многопоточность — это не одно и тоже.
По умолчанию есть только один поток, который асинхронно обслуживает очередь сообщений.
В браузере для многопоточности есть WebWorkers, но единственное возможное общение между потоками осуществляется через отправку строк или сериализованного JSON; к одним и тем же переменным из разных воркеров обратиться нельзя.


Возможности языка


Ключевое слово const


Использование const вместо var или let не говорит от том, что значение является константой или что оно иммутабельно (неизменяемо). Ключевое слово const просто указывает компилятору следить за тем, что переменной больше не будет присвоено никаких других значений.


В случае использования const современные JavaScript-движки могут выполнить ряд дополнительных оптимизаций.


Пример:


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.(…)

Object.freeze


Метод Object.freeze замораживает объект. Это значит, что он предотвращает добавление новых свойств к объекту, удаление старых свойств из объекта и изменение существующих свойств или значения их атрибутов перечисляемости, настраиваемости и записываемости. В сущности, объект становится эффективно неизменным. Метод возвращает замороженный объект.


Сторонние библиотеки


Seamless-Immutable


Библиотека предлагает иммутабельные структуры данных, обратно совместимые с обычными массивами и объектами. То есть доступ к значениям по ключу или по индексу не будет отличаться от привычного, будут работать стандартные циклы, а также все это можно использовать в связке со специализированными высокопроизводительными библиотеками для манипуляций с данными, вроде Lodash или 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"}]'

Для работы эта библиотека использует Object.freeze, а также запрещает использование методов, которые могут изменить данные.


 Immutable([3, 1, 4]).sort()
// This will throw an ImmutableError, because sort() is a mutating method.

Некоторые браузеры, например Safari, имеют проблемы с производительностью при работе с замороженными при помощи Object.freeze объектами, так что в production сборке это отключено для увеличения производительности.


Immutable.js


Благодаря продвижению со стороны Facebook эта библиотека для работы с иммутабельными данными стала самой распространенной и популярной среди web-разработчиков. Она предоставляет следующие неизменяемые структуры данных:


  • List — иммутабельный аналог JavaScript Array
    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]
  • Stack — иммутабельный список элементов, организованных по принципу LIFO (last in — first out, «последним пришел — первым вышел»)
    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]
  • Map — иммутабельный аналог JavaScript Object
    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}
  • OrderedMap — иммутабельный аналог JavaScript Object, гарантирующий такой же порядок обхода элементов, какой он был при записи
    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);
    }
  • Set — иммутабельное множество для хранения уникальных значений
    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]
  • OrderedSet — иммутабельное множество для хранения уникальных значений, гарантирующее такой же порядок обхода элементов, какой он был при записи.
    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]
  • Record — конструктор иммутабельных данных со значениями по умолчанию
    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

Mori


Библиотека, которая привносит персистентные структуры данных из ClojureScript (Lists, Vectors, Maps и т.д.) в JavaScript.


Отличия от Immutable.js:


  • Функциональное API без публичных методов
  • Быстрее
  • Больший размер библиотеки

Пример использования:


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]

Проблемы при разработке, с которыми вы столкнетесь


Речь пойдет об использовании Immutable.jsMori все примерно также). В случае работы с Seamless-Immutable таких проблем у вас не возникнет из-за обратной совместимости с нативными структурами JavaScript.


Работа с серверным API


Дело в том, что в большинстве случаев серверное API принимает и возвращает данные в формате JSON, который соответствует стандартным объектам и массивам из JavaScript. Это значит, что нужно будет каким-то образом преобразовывать Immutable-данные в обычные и наоборот.


Immutable.js для конвертации обычных данных в иммутабельные предлагает следующую функцию:


Immutable.fromJS(json: any, reviver?: (k: any, v: Iterable<any, any>) => any): any

где с помощью функции reviver можно добавлять собственные правила преобразования и управлять существующими.


Предположим, серверное API вернуло нам следующий объект:


var response = [
    {_id : '573b44d91fd2f10100d5f436', value : 1},
    {_id : '573dd87b212dc501001950f2', value : 2},
    {_id : '5735f6ae2a380401006af05b', value : 3},
    {_id : '56bdc2e1cee8b801000ff339', value : 4}
]

Удобнее всего такой объект будет представить как OrderedMap. Напишем соответствующий 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}

Предположим, нам нужно изменить данные и отправить их обратно на сервер:


state = state.setIn(['573dd87b212dc501001950f2', 5]);
console.log(state.toJS());
//Object {573b44d91fd2f10100d5f436: 1, 573dd87b212dc501001950f2: 5, 5735f6ae2a380401006af05b: 3, 56bdc2e1cee8b801000ff339: 4}

Immutable.js для конвертации иммутабельных данных в обычные предлагает следующую функцию:


toJS(): any

Как вы видите, reviver отсутствует, а это значит, что придется писать собственный внешний immutableHelper. И он каким-то образом должен уметь отличать обычный OrderMap от того, который соответствует структуре ваших исходных данных. Унаследоваться от OrderMap вы тоже не можете. В настоящем приложении структуры, скорее всего, окажутся вложенными, что добавит вам дополнительных сложностей.


Можно, конечно, использовать при разработке только List и Map, но тогда зачем же все остальное? И в чем плюсы использования конкретно Immutable.js?


Иммутабельность везде


Если проект раньше работал с нативными структурами данных, то легко перейти на иммутабельные не получится. Придется переписывать весь код, который хоть как-то взаимодействует с данными.


Сериализация/Десериализация


Immutable.js не предлагает нам ничего кроме функций fromJS, toJS, которые работают следующим образом:


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

То есть абсолютно бесполезны для сериализации/десериализации.


Существует сторонняя библиотека transit-immutable-js. Пример ее использования:


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

Производительность


Для тестирования производительности были написаны бенчмарки. Чтобы запустить их у себя, выполните команды:


git clone https://github.com/MrCheater/immutable-benchmarks.git
cd ./immutable-benchmarks
npm install
npm start

Результаты бенчмарков можно увидеть на графиках (repeats / ms). Чем больше время выполнения, тем хуже результат.


При чтении самыми быстрыми оказались нативные структуры данных и Seamless-immutable.


Read


При записи самым быстрым оказался Mori. Seamless-immutable показал наихудший результат.


Write


Заключение


Эта статья будет полезна JavaScript-разработчикам, столкнувшимся с необходимостью использовать иммутабельные данные в своих приложениях для повышения производительности. В частности, это касается frontend-разработчиков, которые работают с фреймворками, использующими VirtualDOM (React, Mithril, Riot), а также Flux/Redux решения.


Подводя итоги, можно сказать, что среди рассмотренных библиотек для иммутабельности в JavaScript самая быстрая, удобная и простая в использовании это Seamless-immutable. Самая стабильная и распространенная — Immutable.js. Самая быстрая на запись и самая необычная — Mori. Надеюсь данное исследование поможет выбрать вам решение для своего проекта. Удачи.

Developer Soft 79,21
Компания
Поделиться публикацией
Комментарии 56
    +6
    Давно ждал подробный разбор полётов, спасибо за проделанную работу!
      +5
      Может я не заметил, но еще стоило бы указать про getter/setter в качестве варианта для создания иммутабельных полей объекта(хотя сам объект будет все еще мутабельным):

      var obj = {
        get youcantmodifyme() {
          return 1;
        }
      }
      
      obj.youcantmodifyme=5;
      console.log(obj.youcantmodifyme); //1
      
      obj={};
      console.log(obj.youcantmodifyme); //undefined
      


      Для массивов можно провернуть некислой хаковатости трюк(так делать не надо!):

      var q=[1,2,3];
      var settings={};
      for(var i in q){
        settings[i]={
          value: q[i],
          writeable: false
        }
      }
      var p=Object.defineProperties({}, settings); //а затем из Array тырим нужные методы по типу push. P, кстати говоря, Object, а не Array, зато поля немодифицируемые(ну как немодифицируемые - те, что есть, немодифицируемые, новые добавлять можно спокойно).
      


      А самый красивый, пожалуй, способ для ES6(причем и для массивов, хотя и возвращает объект) — Proxy(спасибо stackoverflow за примеры):

      var s=[1,2,3];
      var arr = new Proxy(s, {
          get: function(target, name) {
              return target[name];
          },
          set: function(target, name){
             return 1;
          }
      });
      
      arr.push(1); //[1,2,3]
      arr[1]=100; //[1,2,3]
      arr[10000]=100000000; //а вот хрен, все еще [1,2,3]
      


      (Оригинал — http://stackoverflow.com/questions/2449182/getter-setter-on-javascript-array).

      А вообще, спасибо за статью, познавательно. Про Object.freeze вообще не знал.
        0

        Спасибо за предложение — про иммутабельные поля на getter/setter обязательно добавлю. Про Proxy — не знаю, стоит ли. Поддержка в браузерах пока, что оставляет желать лучшего http://caniuse.com/#feat=proxy

          –1
          При необходимости можно и шимы использовать + эта статья справедлива и для ноды, у которой поддержка ES6 очень даже неплохая(прокси, если не ошибаюсь, должны быть)
            +2

            Да — для node.js использовать Proxy — не проблема. А вот насчет шимов и полифилов — для Proxy они невозможны

              0
              Гугля дает https://github.com/tvcutsem/harmony-reflect. Насколько работоспособно — не в курсе, пока проверить не могу.
                0
                Там полифил только с флагом, вообще в хроме прокси частично можно эмулировать с помощью не принятного Object.observe
                  0
                  Нельзя — obsolete, его убрали уже(https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/observe).
                  Object.watch есть еще в Gecko вроде бы(https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/watch), но тоже не очень рекомендуется.
                    +1
                    Ну конечно убрали, он же превратился в Proxy в итоге, просто в старых версиях им можно пользоваться для эмуляции.
                      0
                      Да, справедливо.
        +2
        Небольшая поправочка: vuejs на данный момент не использует virtual DOM — обещают в 2.0 (вот-вот на подходе).
        0
        Спасибо за статью! Есть еще FreezerJS.
          +2
          Простое и быстрое отслеживание изменений


          Тоже ложь:
          function threshold (before) {
           return {
            r : before.r > 100 ? 255 : 0,
            g : before.g > 100 ? 255 : 0,
            b : before.b > 100 ? 255 : 0,
           };
          }
          
          const idealBefore = { r: 105, g: 0, b: 0 };
          const idealAfter = threshold(idealBefore);
          
          idealBefore === idealAfter //false
          idealBefore.r === idealAfter.r //false
          idealBefore.g === idealAfter.g //true
          idealBefore.b === idealAfter.b //true
          
          // epic fail:
          const before = { r: 0, g: 0, b: 0 };
          const after = threshold(before);
          
          before === after //false
          before.r === after.r //true
          before.g === after.g //true
          before.b === after.b //true
          


          Так что процедуру `areColorsEqual` писать придется, как и в любом другом подходе.
            0
            Так const не решает проблему просто. В нормальной реализации объект {r: 0, g: 0, b: 0} будет зарезирвирован в памяти и обе переменные получат ссылку именно на него. Тут на самом деле будет перенос сложности из сравнения в создание.
              0
              const тут вообще ни при чем =)

              В нормальной реализации объект {r: 0, g: 0, b: 0} будет зарезирвирован в памяти

              А есть такие реализации? О.о
              А если это коодината в игре с флоат-значениями { x: 0.1, y: 0.2 } — тоже все кешироваться будут?
                0
                Не знаю есть ли но можно сделать и вместо return {...} будет return new Immutable({...}) а в конструкторе уже нужно будет проверять есть такой объект или его нужно создавать и возвращать ссылку. Я б для координаты в игре вообще не пользовался бы иммутабельными структурами, но в принципе тоже можно, просто кешь нельзя только лишь заполнять, его и чистить нужно Immutable.destroy({x: 0.1, y: 0.2}).
                  0
                  А как чистить? Вручную писать что-то вроде «сборщика мусора»? Как вы будете знать, что нигде не осталось куска, который потом захочет проверить на равенство?
                    0
                    Ну это не совсем сборщик мусора, сборщик как раз родной браузерный, но для нашего кеша — в конструкторах счетчик ссылок на объекте увеличиваем, в деструкторе — уменьшаем, при достижении нуля — чистим область кеша. Если что-то захочет проверить на равенство ему прийдется или держать инстанс объекта (счетчик ссылок в кеше на него будет не нулевым) или создать новый (тогда он или создастся или возьмется из кеша).
                      0
                      И такие сложности зачем?
                        0
                        Это должно быть в библиотеке так что конечному пользователю это действительно незачем и он об этом и не думает (ну разве что деструктор непривычный добавляется). А в общем — есть разные подходы к разработке, многие переходят в жс из других языков и тянут свои идеи, цезарю — цезарево.

                        А профит от кеша — быстрое сравнение естественно.
                          0

                          Но медленное создание. Оно того стоит?

                            0
                            От задачи зависит.
                      0

                      Кстати, в $mol мы решаем эту проблему довольно элегантно. Например, можно ввести глобальный реестр точек:


                      class $mol_point extends $mol_object {
                          @ $mol_mem_key()
                          item( point : { x : number , y : number } ) {
                              return point
                          }
                      }

                      Обращение к реестру с одними и теми же параметрами будет возвращать один и тот же объект:


                      $mol_point.item({ x : 1 , y : 2 }) === $mol_point.item({ x : 1 , y : 2 }) // true

                      Под капотом $mol_mem_key создаёт $mol_atom, который отслеживает зависимости от этой точки и если зависимостей не останется — соответствующий кеш будет подчищен.

                        0
                        не понимаю, как $mol_atom может знать, что больше никто не запросит $mol_point.item({ x : 1 , y : 2 })
                          0

                          Что никто не запросит — не знает. А вот, что нигде сейчас не используется (при учёте, что все, кому надо обеспечить кешируемость, дёрнули $mol_point.item) — вполне.


                          Например, рендерим в компонент координаты:


                          class $my_hint extends $mol_view {
                          
                              @ $mol_mem()
                              point( next : { x : number , y : number } ) {
                                  return $mol_point.item( next || { 0 : 0 , y : 0 } )
                              }
                          
                              sub() {
                                  console.log( this.point() )
                                  return [ JSON.stringify( this.point() ) ]
                              }
                          }

                          Теперь, если мы при передадим эквивалентный объект, то ничего не произойдёт:


                          hint.point({ x : 0 , y : 0 })

                          А если неэквивалентный, то будет ререндеринг:


                          hint.point({ x : 1 , y : 2 })
                          //{ x : 1 , y : 2 }

                          Соответственно, $mol_view рендерится в рамках вычисления другого атома, который "держит" свои зависимости от самоуничтожения, поэтому при следующем обновлении соответствующий объект будет найден в кеше и возвращён вместо того, что мы передали в $mol_point.item и никакого ререндеринга произведено не будет:


                          hint.point({ x : 1 , y : 2 })

                          Соответственно, в этот момент в кеше будет только { x: 1, y: 2 }, а { x: 0, y: 0 } — нет, так как в рантайме нигде сейчас не используется.

                            0
                            Сложно как-то. Как по мне, то проще было бы классически добавить метод `equals` у класса Point. И тогда кешировать не надо, инвалидировать кеш не надо и можно написать любую логику.
                              0

                              Только надо везде вызывать этот equals с полным сравнением внутренностей, а тут при создании точки пишешь $mol_point.item({ ... }) вместо new $mol_point({ ... }) и можешь везде использовать быстрое сравнение point1 === point2 вместо медленного и некрасивого point1.equals( point2 ).

                                0
                                пишешь $mol_point.item({… })

                                И внутри проделываешь тот же фарш со всеми внутреностями. И каждый раз при вызове вместо того, чтобы один раз при сравнении.
                                  0

                                  Не понял, это вы о чём?

              +4
              Спасибо за статью!
              Всегда придерживался концепции использования immutable-структур только там, где без них никак, и эта статья лишний раз убедила меня в правильном выборе концепции.

              В принципе, за исключением особых случаев, скажу так: иммутабельность в JavaScript — еще один ненужный велосипед, коих в последнее время ну очень много.
                0
                Безопаснее использовать и легче тестировать

                В описании есть аргумент только для «Безопаснее». А чем легче тестировать?
                  0

                  Безопаснее использовать, и поэтому легче тестировать.
                  Предположим, первый тест вам массив случайно испортил, отсортировал его, а второй тест ожидает элементы в исходном порядке, а на отсортированном массиве он сломается. Т.е. ситуация такая — обвалился второй тест, а виноват первый. Причину поломки будет найти не так просто.

                    0
                    Ну вообще обычно тесты независимы, один никак не влияет на второй и запускаются вообще в отдельных окружениях.
                    А в пределах одного теста слишком мало изменений, чтобы сложно было найти поломку.
                    И аргумент вообще выглядит как «ну потому что мутабельность это плохо в целом». А тесты сюда притянуты за компанию.
                      +1

                      Я не фанат иммутабельности. Просто мне приходится использовать React + Redux. А там без иммутабельности никуда, на ней все держится. Я прекрасно понимаю, что мутабельность в большинстве случаев быстрее и лучше. Но не в React + Redux, а это нынче модное направление, и многим людям придется столкнуться иммутабельностью из-за этого.
                      Ситуация с тестами все таки возможна, и даже если найти поломку получится быстро — все равно это не очень приятный момент

                        +2
                        Да, знаю. Кстати, как раз в связке React+Redux с подобной проблемой и сталкивался, когда один тест влиял на другой)
                  +2
                  Не понимаю, почему все так помешались на концепции «отслеживания изменений». Да, так работают Angular и React, но это не значит, что это лучший подход. Старый добрый биндинг через события чем плох?

                  Для меня есть только один весомый аргумент в пользу иммутабельности: потокобезопасность. Да, в JS потоков нет, и отсюда вывод: в JS иммутабельность не нужна.

                  Конечно, понятность кода и защита от дурака тоже важны, но не ценой тех накладных расходов, которые иммутабельность порождает.

                  Старшему разработчику достаточно написать коммент при объявлении переменной: // immutable, — вот и вся реализация. А младший разработчик, использующий иммутабельные структуры данных, будет писать работающий код, но очень неэффективный (то есть всего лишь одной строкой кода он может совершенно ненарочно породить жуткие тормоза). Получается, ни тому, ни другому строгая реализация иммутабельности не поможет.

                  Все равно, обзор очень интересный, спасибо!
                    0

                    Биндинг через события порождает много хрупкого кода, который сложно поддерживать. Подробнее в статье про атомы.

                      0
                      Да, у событий есть недостатки. Сам с каскадным обновлением неоднократно мучался. Но, по-моему, у метода отслеживания изменений недостатков больше. Взять тот же Angular: жесткое ограничение на количество вотчеров в программе, сложность отладки зависимостей, неочевидный порядок запуска вотчеров и т. д. Больше возни с фреймворком, чем программирования.

                      Я не говорю, что Angular плох, просто он не для каждого приложения подходит.
                        0
                        1. Ангуляр не отслеживает зависимости.
                        2. У Ангуляра нет ограничений на число вотчеров.
                        3. События отлаживать не менее сложно.
                        4. События триггериться тоже могут в неочевидном порядке.
                          +2
                          1. Я этого и не говорил
                          2. Есть. Попробуйте завести 100000+ вотчеров на странице и запустить дайджест
                          3. Менее. Стек вызовов для обработчика события покажет, где оно возникло, если только вы не используете отложенную обработку
                          4. Приведите пример
                            0
                            1. "у метода отслеживания изменений недостатков больше. Взять тот же Angular"
                            2. Так и что произойдёт? В любом случае 100к — не похоже на "жёсткое" ограничение.
                            3. И конечно же он покажет, почему обработчик события вызывается в контексте уничтоженного объекта. :-)
                            4. Например, в порядке создания/активации вьюшек пользователем, а не в порядке их следования в коде.
                              +1
                              1. Дык отслеживание изменений, а не зависимостей
                              2. Тормоза будут все сильнее с каждым новым вотчем. С событиями же никаких тормозов не будет, пока память не закончится. Тормоза не накапливаются
                              3. В каком смысле «уничтоженного»? Что такое «уничтоженный объект»?
                              4. Если речь о синхронном создании вьюшек, то они создаются/активируются в том порядке, в котором это написано в коде. Если об асинхронном — другое дело, согласен, придется повозиться, чтобы разобраться, что к чему. Но отслеживание изменений в качестве альтернативы событиям здесь не поможет — скорее, наоборот, усугубит проблему
                                0
                                1. А, тогда сори. :-)
                                2. Ну, то есть никакого ограничения всё же нет. :-) То, что там алгоритмическая сложность выше — другой вопрос. Это плата за простоту отслеживания изменений.
                                3. Например, это вьюшка удалённая из дерева.
                                4. Используя функциональную парадигму совершенно не важно в каком порядке вызываются обработчики. А вот с императивной это да, проблема.
                                  0
                                  3. Почему объект не отписался от события — это другой вопрос. Я говорю про отладку процесса генерации и обработки событий. Это обычный последовательный вызов коллбеков, который легко поддается отладке. Вотчи ангуляра отлаживать куда труднее.
                                  4. А, я наконец-то понял, о чем вы. Ну, да, если вы математически докажете чистоту функций всех своих зависимостей, то порядок обработки будет неважен. О чем-то подобном, вроде бы, идет речь во втором ангуляре, но я с этим пока не ознакомился. В первом же это была головная боль.
                                    0
                                    Используя функциональную парадигму совершенно не важно в каком порядке вызываются обработчики. А вот с императивной это да, проблема.

                                    Простите, а почему не важно? Вот у меня есть пошаговая игра (я делал что-то похожее).
                                    Абилка А увеличивает урон на 2, абилка Б уменьшает урон в два раза.

                                    Был урон 4. Сработало событие НаноситсяУрон.

                                    Вариант 1.
                                    Срабатывает абилка А потом Б. Урон = (4+2)/2 = 3
                                    Вариант 2.
                                    Срабатывает абилка Б потом А. Урон = 4/2+2 = 4

                                    Тут хоть ФП, хоть ООП. Результат зависим от порядка.

                                    Или, скажем (исскуственный пример) событие «Добавить текст». Один обработчик очищает от html-тегов, второй заменяет markdown-code на html-теги. Функции вполне будут чистыми, но порядок важен. Объясните, почему вы говорите, что в ФП порядок не важен?
                                      +1

                                      У вас оба варианта императивны и как раз показывают проблему императивного кода — порядок важен :-)


                                      С ФП будет что-то типа:
                                      Срабатывают абилка А, потом Б. Урон = вычислить_урон( список абилок, состояние ) = 4 +2 +4/2 = 8
                                      Срабатывают абилка Б, потом А. Урон = вычислить_урон( список абилок, состояние ) = 4 +2 +4/2 = 8


                                      Либо нам нужно явно задать порядок применения абилок.
                                      Срабатывают сначала аддитивные абилки, потом мультипликативные. Урон = (4+2)/2 = 3


                                      По второму примеру та же ситуация — вы императивно меняете состояния. В функциональном у вас будет 3 отдельных свойства:
                                      this.sourceText() { '<b>hello'</b>' } — исходный текст
                                      this.sanitizedtext() { return this.sourceText().replace( /<.*?>/g, '' ) ) } — очищенный текст
                                      this.sanitizedAndMarkdownedText() { return Markdown.makeHTML( this.sanitizedText() ) ) } — готовый html.


                                      Разумеется в данном случае никакие перестановки не возможны. Переставлять можно лишь функционально независимые вычисления.

                                        0
                                        У вас оба варианта императивны и как раз показывают проблему императивного кода — порядок важен :-)

                                        Подождите, там вообще кода нету. Пока все на уровне дизайна, который говорит, что порядок срабатывания абилок важен.
                                        Я, к сожалению, не понял, как у вас получилась восьмерка.

                                        У нас есть, допустим, бизнес-требование — абилки срабатывают в порядке, как техника выводится на поле. При событийной модели они как раз будут подписываться в таком порядке, при функциональном подходе — в новом списке новый танк со своей абилкой будет добавлен в конец. То есть и там и там порядок важен и соблюдается приблизительно одинаково.

                                        Покажите тогда пример, когда в функциональном коде не важен порядок. (в виде «бизнес требование => псевдокод»), я, к сожалению, не могу вас понять.
                                          +1

                                          Ну так, если требование, что порядок важен, то он важен. :-)


                                          А пример, где порядок не важен: "Срабатывают сначала аддитивные абилки, потом мультипликативные" — порядок их добавления не важен.

                        0
                        По поводу //immutable — как это убережет от ошибки с неявными изменениями массива, указанной в статье(тот же sort/splice/delete/что-там-еще)?
                          0
                          Не убережет, если не хватает ума посмотреть текст объявления переменной и прочитать документацию методов sort/splice/… Поэтому я и написал «старшему разработчику».
                            0
                            Угу. Только пишет старший. а читает и правит — младший. Кроме того. окромя дефолтных методов есть еще куча своих функций. каждая из которых может оказаться троянским конем. Каждую в доке проверять — замучаешься. да и времени очень много уходить будет. Вот для таких случаев иммутабельность может оказаться вполне рентабельной.
                        +1
                        Я может не в те графики смотрю, но с выводами статьи не совсем согласен.

                        Поскольку state надо в сотни раз чаще читать, а не писать, вес победы Mori в тестах на запись несколько блекнет по сравнению с полным своим провалом в тестах на чтение (и безусловной победой там seamless).

                        Поэтому правильнее говорить, что самая быстрая, удобная и простая — это seamless. Самая стабильная и распространенная Immutable.js. А Mori остается быть только самой необычной.
                        +1
                        В секции про Мори
                        Отличия от Immutable.js:

                        Функциональные плюшки (ленивые коллекции и т.д.)


                        Секундочку, в Immutable.js тоже есть ленивые коллекции, например Seq, вот что говорит документация:
                        Seq is lazy — Seq does as little work as necessary to respond to any method call. Values are often created during iteration, including implicit iteration when reducing or converting to a concrete data structure such as a List or JavaScript Array.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое