Компромисно сравнение на Immutable.js, Seamless-immutable и Timm

TL; DR С популярността на Immutable.js и Seamless-immutable, която преминава през покрива, кой се нуждае от още една библиотека за неизменност? Може би вие, ако не искате да заключите достъпа за четене до вашите данни чрез чужд API, се нуждаете от разумна производителност при запис и лесен за използване API. Точно това е балансът, който Timm се опитва да постигне.

Прочетохме публикациите, изгледахме видеоклиповете, разбрахме концепциите. Неизменността може да бъде наистина полезна в много сценарии; например:

  • Извършваме някои дълги изчисления върху данни и искаме да запаметим резултатите, като ги използваме повторно, докато входовете се променят.
  • Искаме да внедрим функционалност Undo/Redo по елегантен начин.
  • Искаме да разплитаме състоянието на нашето приложение и да използваме нещо като Redux с пътуване във времето, сериализация, рехидратация, каквото и да е.
  • Закъсали сме с производителността на нашето приложение React и чухме за чудесата на shouldComponentUpdate.

Във всички тези случаи използването на неизменни данни вероятно е правилният начин. Може да направи кода по-предвидим, позволява тривиални сравнения на обекти и се чувства като у дома си с функционално програмиране. Какво друго бихме могли да поискаме?

Както се оказва, неизменните операции не идват без собствените си недостатъци. Те никога няма да бъдат толкова бързи, колкото мутациите на място, колкото и да се опитвате. Ще трябва да се ориентирате около някои алгоритми, особено рекурсивните. И в някои случаи може да стане трудно да се работи с него, в зависимост от инструментите/библиотеките, с които работите.

Но какво ще стане, ако намерим начин да облекчим когнитивното бреме върху разработчика, да поддържаме производителността разумна и да ни позволи да извлечем предимствата на неизменността? Влезте в Timm, малка библиотека, която се опитва да реши някои от тези компромиси.

Компромис №1: безпроблемност срещу защита

Immutable.js е едно от големите имена в арената на неизменността, солидна библиотека, която ви предлага всичко, от което някога ще се нуждаете, а след това и още. Производителността също е отлична, като се имат предвид многото неща, които прави, и неговите обекти (Карти, Списъци и много други) са непрозрачни, така че са защитени срещу вашите случайни мутации.

Въпреки това, след като го използвах в няколко проекта, винаги съм усещал напрежението от смесването на карти и списъци на Immutable.js с естествени JS обекти и масиви. Би било невъзможно да преброя колко пъти получих странни грешки по време на изпълнение, защото забравих да използвам инструмента за получаване на Map (image.get('url')), вместо обикновена стара нотация с точки ( image.url), или предупреждение, защото взех познатото свойство length на масива вместо size на List.

И така: безпроблемно или защитено? Timm избира безпроблемно. Би било възможно да има собствени и защитени обекти чрез Object.freeze(), но изглежда има някои затруднения в производителността (или алтернативно „различни поведения“ в разработката и производството режим, който изглежда също толкова лош). В заключение: вие можете случайно да промените своите обекти (внимавайте!), но:

  • Ще имате достъп до вашите данни по лесен начин; и
  • Няма да бъдете заключени в определена библиотека за нещо толкова повсеместно като управление на данни.

Компромис №2: простота/размер срещу производителност при запис

Immutable.js цели да бъде едновременно изчерпателен и ефективен. Оптимизирането на скоростта на четене/запис в неизменни операции, като същевременно се запази привлекателен API, не е тривиално и Immutable.js използва усъвършенствани техники („постоянни структури от данни“ и „структурно споделяне“), за да постигне точно това.

Въпреки че не предоставя всички предимства, Timm е 10 пъти по-малък (~ 1 kB минимизиран и компресиран) и използва много по-основни инструменти, въпреки че се фокусира и върху производителността. Четенията са толкова бързи, колкото и оригиналните (дори нямате нужда от библиотеката за това), а записите също са сравнително бързи („бенчмаркове“):

  • На малки масиви/обекти: същата производителност като Immutable.js за плитки записи, става все по-бърза от Immutable.js за по-дълбоки записи.
  • На големи масиви или обекти (вижте Масив: запис на фигурата по-долу, където масивите имат 1000 елемента): все по-бавно от Imutable.js, тъй като масивите стават по-дълги или обектите имат повече Имоти. Това е мястото, където Immutable.js наистина блести!
  • По-бърз (дори изключително по-бърз) от Seamless-immutable, който също използва собствени обекти.

...и какво да кажем за JS оператора за разпространение?

И накрая, защо да не използваме лъскавия нов „оператор за разпространение на масиви“ на ES6 и неговия предстоящ „брат за обекти“? В крайна сметка родният синтаксис не може да бъде победен! Защо не направите следното?

const obj = { foo: 'foo', bar: 'bar' }; 
// Why not do this... 
const obj2 = { ...obj, bar: 'boo' }; 
// ...instead of this? 
const obj2 = timm.merge(obj, { bar: 'boo' });

Операторът за разпространение на обекти определено изглежда по-хубав. Внимавайте обаче:

const obj = { foo: 'foo', bar: 'bar' };
// Future spread operator without changes
const obj2 = { ...obj, bar: 'bar' };
console.log(obj2 === obj);
// -> false
// Timm without changes
const obj2 = timm.merge(obj, { bar: 'bar' });
console.log(obj2 === obj);
// -> true!

Операторът за разпръскване винаги създава нов обект, без значение дали всъщност модифицираме оригиналния. За разлика от тях, Timm и Immutable.js само инстанцират нов (мързеливо), когато открият, че операцията всъщност мутира обекта; по този начин ще създадете по-малко обекти, облекчавайки изискванията за памет и събитията за събиране на боклук. И нещо повече: ще получите по-малко фалшиви положителни резултати, когато проверявате дали даден обект е променен.

Заключение

След всичко това, кой е най-подходящият вариант?

  • Не забравяйте очевидното: наистина ли имате нужда от неизменност? Вероятно да, но използвайте най-добрата си преценка. Ако производителността е от решаващо значение и можете да пожертвате елегантността на кода и другите хубави предимства, които носи неизменността, добре... просто мутирайте на спокойствие! Искам да кажа, на място.
  • Ако имате нужда от пълна, изпитана в битка библиотека и нямате нищо против заключването, свързано с чужд API дори за четения: използвайте Immutable.js.
  • Ако предпочитате обикновени масиви/обекти, използвайте Timm.
  • Ако типичните ви случаи на употреба включват повече четене, отколкото писане, използвайте и Timm.
  • Ако правите много записи върху много дълги масиви или дебели обекти, използвайте Immutable.js.

Първоначално публикувано в Math.random() на 16 юни 2016 г.