За да принудите изчислените стойности да останат живи, можете да използвате опцията keepAlive: true, но имайте предвид, че това може потенциално да създаде изтичане на памет.

Горният цитат е от документацията на MobX. Ако сте като мен, когато прочетох това изявление, вероятно се чудите: Какво точно ще изтече? Ако това е само потенциално изтичане, в какви ситуации трябва да се тревожим за това? И как може да се избегне?

Постът е опит да се отговори на тези въпроси. Но първо, малко предистория.

Защо да използвате keepAlive?

Изчислените свойства на MobX са точно като нормалните свойства на JavaScript, с някои допълнителни предимства. Едно от предимствата е, че изчислената стойност се кешира автоматично. Това е чудесно, когато изчислителната функция, която MobX нарича деривация, е скъпа. Когато нито една от наблюдаемите, от които зависи извличането, не се е променила, многократният достъп до изчисленото свойство просто връща същата, предварително изчислена стойност. Когато една от наблюдаемите сепромени, MobX магически знае, че трябва да обезсили кеша и да изпълни отново извличането.

Хората, които са нови за MobX, често са изненадани да научат, че това автоматично кеширане работи само когато собствеността е достъпна в реактивен контекст. Ако осъществим достъп до свойството извън autorun,observer или подобен, тогава свойството действа точно като нормално свойство. Без кеширане за нас.

Ако нашето извеждане е скъпо, това е проблем. Може да се изкушим да внедрим собствено кеширане, ако MobX ще бъде придирчив. Просто изтеглете изчислената стойност някъде и след това я върнете следващия път, когато бъдем помолени. Но изчакайте, ако нашата изчислена стойност е извлечена от други наблюдаеми величини и някоя от тях се промени, трябва да изпълним повторно извличането. Може би хващаме всички входни наблюдаеми, премахваме и тях, а след това следващия път, когато има достъп до свойството, сравняваме с предишните наблюдаеми стойности, за да решим дали да изпълним отново извличането? Това става сложно и не трябва ли MobX да се справя автоматично с подобни неща?

Аха, има опцията keepAlive: true! Ако нашето изчислено свойство е създадено с този набор от опции, MobX ще кешира изчислената стойност дори когато е достъпна извън реактивен контекст. И автоматично ще го обезсили, когато наблюдаваните наблюдаеми се променят. Перфектно!

Но изчакайте, „потенциално може да създаде изтичане на памет?“ Това звучи лошо.

Какъв вид изтичане на памет?

Предупреждение за спойлер: Животът на изчислено свойство с keepAlive: true ще бъде удължен до най-дългия живот на който и да е от наблюдаемите, до които има достъп.

Нека разбием това малко. Помислете за този доста измислен пример:

Така че имаме един обект, екземпляр от клас LongLived, който остава наоколо за цялата продължителност на цикъла. И след това във всяка итерация на цикъла създаваме чисто нов екземпляр от клас ShortLived, оценяваме неговото свойство и след това го изхвърляме.

Както е написано в момента, всяко копие на ShortLived става подходящо за събиране на боклук след всяка итерация на цикъла. Няма изтичане на памет; всичко е наред!

Но след това променяме свойството bar на ShortLived, за да използваме keepAlive:

С тази малка промяна всеки екземпляр на ShortLived не отговаря на условията за събиране на боклук, докато не завърши целият цикъл и екземплярът на LongLived излезе извън обхвата. Животът на екземплярите ShortLived е удължен, за да съответства на живота на LongLived.

Може би това не звучи многолошо, но си представете, ако LongLived беше някакъв вид обект на ниво приложение, който оставаше наоколо през целия живот на нашето приложение. Всяко keepAlive изчислено, което има достъп до него, никогане може да бъде събрано за боклук през цялото време, докато вашето приложение работи. Това очевидно е изтичане на памет.

Но защо?

В MobX, когато деривацията за изчислената стойност C има достъп до наблюдаем O, препратка към C се добавя към списъка observers на O. MobX използва тази препратка, за да обезсили кешираната стойност на C, когато O се промени. Но докато C остава в списъка с наблюдатели на O, C не може да бъде събиран боклук, защото има жива препратка към него.

Това е вярно, дори ако няма други препратки към C никъде в приложението. В този случай може да изглежда безсмислено да се анулира и преизчислява C, защото как може някой дори да види новата стойност, ако няма препратки към C никъде в приложението? За съжаление, MobX няма начин да определи дали това е така. Може би би могло да се направи, ако JavaScript имаше нещо като WeakReference на Java. Но WeakMap и WeakSet не са достатъчни, защото нито едно не позволява съдържанието им да бъде изброено.

Но, което е важно, MobX само добавя C към списъка observers на O в тези две ситуации:

  1. C се оценява в реактивен контекст, като autorun или mobx-reactobserver, или
  2. C е отбелязано с keepAlive: true.

Така че на теория активен autorun може да причини изтичане на памет точно като keepAlive: true. На практика това е много малко вероятно. За изчислена стойност, оценена в autorun, за да причини изтичане на памет, изчислената стойност ще трябва да има достъп до някои дълготрайни наблюдаеми. Тогава всички препратки към този наблюдаем (извън MobX) ще трябва да бъдат премахнати (напр. зададени на undefined), без да се задейства autorun да се изпълнява отново. В този сценарий autorun наистина би причинил изтичане на памет, предпазвайки обект от събиране на боклук, който е недостъпен за приложението.

Въпреки че е малко вероятно това някога да се случи, то може лесно да бъде поправено. Просто направете самите препратки към наблюдаемите наблюдаеми, така че задаването им на undefined да задейства autorun. Или можем просто да изхвърлим autorun!

Изчислена стойност с keepAlive: true, от друга страна, прави многолесно неволното предизвикване на изтичане на памет. И не можем лесно да се разпореждаме с изчислена стойност.

Безопасно ли е да използвате keepAlive?

Безопасно е да се създаде изчислена стойност с keepAlive: true, ако извличането самодостъпва до наблюдаеми на същия обект. При този сценарий няма удължаване на живота.

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

Разбира се, може да е трудно да се гарантира, че тези изисквания остават верни, особено когато кодът се развива с течение на времето. Със сигурност всяка изчислена стойност с keepAlive: true заслужава коментар, в който се споменават проблемите, и изисква допълнителна проверка всеки път, когато се промени.

Ако вашата изчислена стойност нетрябва да има достъп до дълготрайни наблюдаеми, единственият начин, за който знам, за да избегнете изтичане на памет, е да използвате изричен dispose шаблон. Изчислените стойности нямат вграден механизъм за изхвърляне, но е доста лесно да се създаде такъв. Изглежда така:

Сега, когато сте готови с обект със свойство keepAlive: true, извикайте неговия dispose метод, за да се уверите, че той се премахва от всички наблюдаеми, до които е осъществявал достъп преди това. Като този:

Край на изтичането на памет! Освен ако не забравите да се обадите на dispose, разбира се. Така че, ако можете, по-добре е да избягвате изцяло keepAlive: true.