Моментальны знятак чы актуальнае значэнне? Выбір таго, што бачаюць затрыманыя калебэкі JavaScript.
Дазвольце дазнаць, чаму клаудзы зберагаюць доступ да прыкрепленых значэнняў, а не да ўсіх іх копій, і як выбіраць межу моментальнага знятка і актуальных дадзеных у таймерах, эфектах React, асэнблерах і асінхронным кодзе.
Кнапкі работае з дадзеннямі з пярэднейга атрыбутавання. Таймер фіксуе значэнне, якога не было на момент задання. Кожны працоўнік, створанный у цыкле, здаецца належаць да пярэднега элемента. Медленная адпаведзь перазапісвае экран рэзультатамі сторанкі, яку корыстнік вялікі час тады ўжо пакінуў. Гэтыя багі зазвычай класифікуюцца як «проблемы з закрыццем», але такія абазаванні рэдка калі дапамагаюць хто-небудзь іх вылечыць. У гэтым артыкуле абазаванне заменяецца точным модэлем: закрыцце падтрымлівае доступ да прыязначэнняях зменных, а не да копіях значэнняў, і кожны элемент з адкладзеным выкарыстаннем патрэбуе чыстае рашэнне пра тое, чы гэты элемент павінен бачыць момантальную копію чы ж актуальнае значэнне.
Правілу, якое стоіць за кожным багам з закрыццем
Закрыцце функцыі не ўсё час яе неперакладзумелае. Функцыя JavaScript зберагае апуск да лексычнай среды, у якой была створана, і кожны раз, калі ў её тэле згадваецца зовнішняя зменная, гэта імя шукаецца ў средзе ў момент выканання коду. Ключовым словам тут є доступ. Функцыя не отрымае зашчытнай копіі всьго, што было видна ў момент яе адзначэння; яна застаецца з тымі ж прыяўленнямі, і якщо адна з гэтых прыяўленняў пазней будзе перазначана, функцыя прачытае новую значэнне.
Існуе два протыякожныя спосабы падцься на глупую памылку. Першы — калі спакаваецца, што будзе створана копія дадзеных, хоць на самай працэ код з’яўляе жывыя лінкі да зменных, якія ўсё час перабіваюцца. Другі, які часта трапляецца ў фрэймворках для атрыбутавання, — калі спакаваецца, што будзе атрыбутавана найновейшая значэнне, хоць функцыя-вызов была створана ў старэйшам контексте, дзе её зв’язкі больш ніколи не будуць зменяцца. Обе памылкі вынікаюць з адной і той жа прычыны: ніхто не выбраў, да канкрэтнага момента павінна належаць задача, якая выканана з адзінаго кроку.
Калі гэта рашэнне становіцца ясным, паведамлення перестае здавацца таінственным. Функцыя-вызов выканана абсалютна так, як програма ёй запаведала. Проста програма сказала не тое, што мяркаваў разработчык.
Даступ, а не фота
У прыкладзе з закрыццем кнігі паўчыкавання є внутраняя функцыя, якая продавальвае выкорыстоўваць зменную зовнішняй функцыі пасля таго, як зовнішняя функцыя вяртаеся. Ён паказвае, што среда выжывае пасля вызову, але тыхамо стварае некоректную інтуіцыю: што внутраняя функцыя зберагае значэнне, якое бачыла час стварэння. Невялікая варіяцыя паказвае разніцу. Хутчэй логгер ствараецца, калі message ў значэнні "Starting", а зменная перазначаецца прытаму, як фабрыка вяртаеся.
function createLogger() {
let message = "Starting";
const logMessage = () => {
console.log(message);
};
message = "Finished";
return logMessage;
}
const logger = createLogger();
logger();
Выклік logger() выдрукуе Finished. Функцыя-стрэлка ніколі не зберагала строку "Starting"; яна зберагала прыяўленне message, і калі яна выканалася, гэта прыяўленне вядала на іншую строку.
Это ўласнасць, а не дзеянне. Прыватны зменны стан у лічыльнях, функціях-фабрыках, кэшах для мемаізацыі і патэрне модуляў усе залежаць ад таго, каб закрыцце магла бачыць змены ў своіх сабецялявых. Проблемы пачынаюцца толькі тады, калі хтось супакоўваецца, што функцыя-калбэк прыўязана да пачатковага значэння, а не да самай змянной.
Прімітывы спрыяюць такаму неправильнаму разумеўню, адтолькі што строкі і цалыя здаюцца самодостатнімі, і здаецца прыродным, што функцыя проста будзе зберагаць „строку“. Але тэла функцыі мячыць імя, а не значэнне, і імены вырашаюцца пад час выканання коду. Якщо вы хочаце глыбэй разумець, як такі пошук праходзіць через вялізныя скопы, як на самай працэ ў JavaScript вырашаюцься змянныя раскрывае гэты механізм.
Практычным прынцыпам, якія следуюць, ўоснованыя на адной простай запытке, яку трэба ставіць кожны раз, калі пазней будзе выканана функцыя-вызов і ў яй будуць викорыстоўваны данні зза меж яе тэрыторіі: чы хаця бы ўжываць значэнне такім, якім яго засталося час стварэння функцыі-вызову, чы такім, яким яно є час ее выканання? Адказ на гэтую запытку падкажае, чы трэба зафіксаваць текущае значэнне, прачытаць данні з моменту выканання функцыі-вызову, чы проста перадаць значэнне як аргумент. Без гэтай запытки код застаецца правільным JavaScript, але його працэўнасць будзе випадковай.
Баг у цыкле стосуецца прыяжэння ідэнтычнасці
Найвядомейшы баг, связаны з клозурамі, выклікаецца цыклам for, адзначаным словам var, які запланавае калькуляцыю колькасці таймаў. Кожная функцыя-вызов здаецца прыяжаная да сваей сабэтной ітераціі.
for (var index = 0; index < 3; index++) {
setTimeout(() => {
console.log(index);
}, 100);
}
Ён выводзіць 3 тры разы. Заява var мае дыапазон функціі, таму весь цикл выкарыстоўвае роўна адну прыязь index. Усі тры калбэкі апылваюцца той самай прыяззю, і калі запускаюцца таймеры, цикл вяршыўся і застаўся на значэнні 3.
Якщо заменіць заяву на let, рэзультат змініцца, таму што мова стварае новую прыязь для кожнай ітераціі циклу for з атрыбутам let і копіюе ў яе текущае значэнне.
for (let index = 0; index < 3; index++) {
setTimeout(() => {
console.log(index);
}, 100);
}
У гэтым варыянце выводзіцца 0, 1 і 2, таму што кожны калбэк тепер апылваецца розным index.
Звычный підсумак — «let выправляе проблему з клозурамі», — але гэта приховвае справжнюй урок. Клозуры паводзіліся аднакова ў обох циклах. У першым трое калебэкаў былі навмесна падключаны да аднай спяльной, змінюемайся зменны; у другім кожны отрымаў сваю сабственную. Змянілася толькі ідэнтычнасць прыязначэння, а не паводзінне клозуры.
Гэта разлік мае значэнне, таму што тая ж самая проблема застаецца без var. Якщо заявіце змінную з let за межами цикла, будзеце яе адкорэгаваць у кожной ітерацыі і чытаць унутры калебэкаў, то кожны калебэк знову будзе бачыць толькі фінальную значэнне. Змена ключавага слова не дапамагае, якщо код усё ўсё падключае калькі калебэкаў да аднаго змінюемага джерела.
Лепшая прычка — спытвацца з’ясаваць, чы рэкаліі павінны дзеліцца станам. Якщо ўсе яны павінны стежыць за адной зменяючайся значэннем, тады правільны ўжоў один біндынг. Якщы кожны з яных павінен запам’ятваць даны, спецыфічныя для сваей ітерацыі, кожнаму патрэбны свой біндынг або явны аргумент. Калі ў коде є калькі рэкаліі, бывае спакуса прыпускаць, што кожны з яных володзіе мэнеджамі, якія ён называе; лексычны дыяпазон паведамляе толькі, дзе шукаюць мэнеджы, а не хто іх володзіе.
Калі час разлучае стварэнне і выконанне
Прыгожданні, якія выступаюць у результате клозуры, становяцца ўсё горшымі, калі існуе перашчэпка между стварэнням функцыі і ўжоў яе: таймер, сеанс з’язку па сеті, дзеянне корыстувальца, задача, якая чакае ў черзі. Любы даны, якія функцыя чытае зза меж свайго коду, можа зменіцца за гэтую перашчэпку. Разглянем рутыну зявлення, якая выкорыстоўвае ідэнтыфікатор проекта на рэвэлі модуля.
let activeProjectId = 42;
async function saveChanges(changes) {
await saveProject(activeProjectId, changes);
}
activeProjectId = 84;
Тое, чы гэта правільна, залежыць ад часу выканання. Аргументы ацэнююцца калі вызываецца функцыя, таму якщо saveChanges выканана раней за перазначэнне, activeProjectId чытаецца сінхронна, і самэў 42 пасылаецца да saveProject, нават як той вызыв чакае. Але будзь-якая функцыя-вызыв, якая чытае activeProjectId пасля певнага затрымкі, пакажае тое, што на тым моментті ўтримвае ця зменная, можа 84, і запішае данні ў абсолютна іншы проект.
Самэльга таму фраза "закрыціі захопляюць значэнні" ёсць небяпечным скрацанням. У дзеякам коде значэнне копіюецца ў аргументаў раней за паузу; у іншам коде чытаецца спяльнае прыязначэнне пасля яе. Две функцыі можу выглядаць практычна ідэнтычнымі, пры тым як следуюць разным правілам па часу.
Інтерфейсы корыстніка сповненыя такімі патэрамі. Уявіце дыалогавое вікно паўтарэння, яке ачынаецца для адного запису. Корыстнік пераходзіць да іншага запису, а потым нажымае «Паўтарыць». Якшо працоўнік чытае зменную «тэкучы запис», ён вычыраюе той запис, які зараз паказваецца на экране, а не той, для якога было ачынена вікно. Такой падход працюе ідеальна; аднак продукт ў такім разе несправны, таму што дзеянне павінна была застацца ў тым жа контэксце, у якім яна была запускалася.
Рашэнне не ў тым, каб аўтаматычна «скопіюваць зменную». Спачатку трэба выявіць, у які момент належыць даная дзеянне:
- Руйнівнае дзеянне зазвычай належыць да моменту, калі яно было запускалася, і павінна выкорыстоўваць ID, зафіксаваны тады.
- Індыкатор статусу павінен бачыць найновейшую значэнне ў кожны момент апдэйта.
- Паўтарэнне, запланаванае на пазнейшы час, можа аб’еднаваць оба варіанты: первысны ID дзеяння і той токен автанацыі, які ў той момент є чынным.
Закрыцце вымагае адпрацоўнікаў JavaScript ясна размышляць пра час. Важныя пытанні — не толькі якая зменная чытаецца праз калбэк, але і якая ў версіі гэтай зменной плануецца викорыстаць у процесе роботы.
Об’екты падтрымліваюць стабільнасць канкэрента, нават калі змянюецца ўсё іхнё содержыш
Калі зафіксаваныя канекціі вядуць да об’екта, з’яўляецца новы слой плутання. Зробленне канекціі const не заморажвае нічога, кроме самай канекціі. Якщо об’ект є зменным, калбэк, які выканаецца пазней, все равно можа спазірваць кожную змяну, якая была адбута чераз спакульнае канкэрента.
const settings = {
retries: 2,
};
setTimeout(() => {
console.log(settings.retries);
}, 100);
settings.retries = 5;
Таймер выводзіць 5. Канстанта settings ніколі не зменяла об’ект, да якога вядуць, але атрыбут retries гэтага об’екта быў апдэйтаваны ўсё раней, чым выканаўся калбэк.
Канцэлера браузера можа яшчэ больш адгубіць. Вы фіксуеце об’екта пры пачатку якога-небудзь асінхроннага задання, пазней розгледзеце яго ў інструментах разработчыка і бачыце поля, якія былі зменены пасля выканання запісу. Некалькі канцэлера паказваюць жывы відбітак об’екта, калі вы яго розгледзеце, а не запис його стану у момент запісу, што прычыняецца таму, што запіс выглядае так, нібы ён адбуўся праз час. Запіс JSON.stringify(obj) або структурованага клону — гэта прыемны спосаб з’ясавіць правдзівы стан об’екта пад час дэбаггавання.
Стварэнне справжняй копіі кодам трэбуе больш, чым проста новая зменная, а глыбіна копіювання залежыць ад структуры таго, што вы прагнуце захаваць. Паверхневая копія, за дапамою метода spread або Object.assign, ад’ёеднае атрыбуты верхньага рэвэлю, але заставае спільным вкладзеныя об’екты і масівы. Глыбокая копія ад’ёеднае больш элементаў, але можа быць даскладной у выкарыстоўванні, можа адмахнуцца протатыпаў класоў і методаў, а таксама може стварыць дублікаты канстант, якія мелі б застацца спільнымі.
Часта лепшы варыянт — зовсім не ствараць копію, а выяўіць толькі тые маленькія, незменныя значэння, якія неабходны для аперацыі.
const retryLimit = settings.retries;
setTimeout(() => {
console.log(retryLimit);
}, 100);
Калебэк стаў чытаць значэнне, якое ніколі не будзе зменяцца, і, што таксама важна, код чыста адзначае, на які історычны контэкст павязаны таймер.
Ставіцеся з падозрою даўжэйшым выкананню коду, які працуе з вялікімі зменнымі об’ектамі. Контексты, контейнеры налаштавання, об’екты стану компонентаў і спяльныя кэшы є типовымі прыкладамі. Калі калбэк пазней звертаецца да адного з іх, ён тылу незалежна будзе спаглядаць кожную змяну, якая адбылася ў міжчасі. Перадача скуранага пакета дадзеных у даўжэйшы код є набагато яснейшым угодам.
У React бачыцца працоўныя недагодзінні на працоўнае
У React багі, вызваныя закрыццем функцый, зазвычай вказуюць на іншы направлення. Калбэк не чытае занадто новую значэння; ён продовжае чытаць ту, якая вже застарела.
Кожны раз запускаець функцыйнальнага компонента ўтварае новы вызов функціі з савоймі локальнымі прыяўленнямі для прапсаў, стану і выведзеных значэнняў. Калебэк, створаны пад час запуска, атрымвае прыяўленні таго самога запуска. Калі стан змяніцца, React зноў вызывае компонент і стварае новыя прыяўленні, але будь-які калебэк з паканальнага запуска, який ўсё ўжо існуе, продовжае абсылуватыся да старых прыяўленняў.
Сімптамы знаёмы:
- Інтэрвал, створаны пад час адного запуска, вечна фіксуе застарэлае значэнне.
- Прыёмач запуску, зарэгістраваны ўсьмо толькі раз, продовжае выкарыстоўваць прапс з першага запуска.
- Эфект з непашыроўнай лістой залежнасцяў продовжае вызываць функцыю, якая атрымвае прыяўленні з застарэлага стану.
Гэта называецца застарэлым калебэкам, але сам калебэк не зламаны. Ён вярны своему паканальнаму запуску, і нічто ў мове не прымушвае яго адразу паследаваць новым даннем, калі React зноў запускае компонент.
Гэта можа здавацца суперактным даўняйшым прыкладам, дзе клозуры з успехам фіксавалі абнавлення. Разлік, знову ж таки, заключаецца ў ідэнтычнасці прыяўлення. У прыкладзе логгера была адна прыяўлення, якая была перазначана, і клозура бачыла новую значэнне. React не перазначае старыя прыяўлення; ён стварае абсалютна новыя среды пад час кожнага рэндару. Стары калбэк застаецца прычастным да старой среды, чыяя значэння ніколі не зменяюцца.
Якща адносіцца да гэтага падходу, рашэнні вылучваюцца з таго, што калбэк павінен рабіць:
- Абнавленне стану, якое залежыць ад текущага значэння, можа выкарыстоўваць функцыональную форму, такую як
setCount(c => c + 1), тады React падае найновейшы стан. - Падпіска, якой патрэбна найновейшая значэнне, можа быць створана занова, калі змянююцыся яе залежнасці, або можа чытаць з намерна падтрымванага ref.
Пытанне застаецца тым самым, як і раней: чы гэта запазджанае паведамленне должна выкарыстоўваць історычны стан чы тэкучы стан? Багі ў React часта выныкаюць калі на яго адпаведзяюць випадкова, змінюючы масэвую прызначання, а не размышляючы над тым, калі функцыя должна выконваць свою роботу. Новэйшыя версіі React таксама праслугваюць useEffectEvent для чытання найновейшых значэнняў унутрь эфекту без ўскладненага яго запуску; выключэнне рефа latest-value з useEffectEvent поясняе гэты патэрн.
Масэвыя прызначання описваюць рэальнасць, а не прыяжненасці
Звычны спосаб борьбы з сустарэлымі клаузурамі — это налагоджанне массива залежнасцяў эфекту, пакуль яго працэю не стане належным. Значэнне падае ў массив таму, што інструменты для перагляду коду выдаюць заўважэння, яго выкарыстоўваецца тады, калі эфект выконваецца занадта часта, а ў канечнай праграме массив стае порожнім, таму што эфект «дыяўаць должен толькі раз».
Это спрыяе спрыйманню массива як регулятора частоты выконвання, але на самай працэ ён являе сабою адзначэнне тых значэнняў, якія чытае клаузура эфекту.
Якщо эфект выкарыстоўвае зменную з адпаведнага фрагмента коду, тая зменная є часткаю яго клаузуры, незалежна ад таго, чы прынятая ёй у массиве. Яе выключэнне не пазбавляе эфект залежнасці; гэта толькі гарантуе, што эфект будзе ісхадзіць з версіі, якую ўпэўніў апошні фрагмент коду.
Уключэнне ўсіх залежнасцяў можа выклікать разныя проблемы: такі эфект можа знішчаць і ствараць падпіс, перазапускати таймер або занова надсілать запиты значна частае, чым планавалося. Гэта легка спрыяе асцензіі, што лінтар занадта строгі. Частае гэта сягненне ўскладнень свідчыць пра тое, што эфект выканае больш адной задачы, або што об’ект чы функцыя, якія ён выкарыстоўвае, ствараюцца занова ў кожны раз пад час адрасавання без прычыны.
Тыповыя способы рашэння включаюць:
- стабілізаванне калбэка так, каб ён змяняўся толькі тады, калі змянююцыся яго сабеўхі вводныя даны
- раздзелэнне адного эфекта на калькі з болей вузькімі задачамі
- перыявленне дапаможнай функцыі ўнутрь эфекта, так што яна больш не будзе зовнішняй залежнасцю
- выкарыстоўванне функцыйнае аднавленне стану замест прымітання стану безпосередна
- задумванне над тым, чы рэальна логіка патрэбуе быць эфектам
Мэтаспраба не ў тым, каб прымусіць React запускать эфект па жаданай частоте. Ёй трэба даць эфекту закрытую функцыю, чыя трымкі паспаўляюцься з поведэнням, за якое ён адпавядае. Калі эфекту патрэбны новыя значэння, але яго не трэба занова ствараць ў кожны раз, калі значэння зміняюцца, трэба даць яму спосаб прачытаць іх, напрыклад, за дапамою ref або эфект-з’явы. Якщо ён должен пачаць працаваць занова, калі значэння змінюецца, гэта значэнне павінна быць у масе. А якщо эфект насправды не выкарыстоўвае данае значэнне, трэба адмахнуцца ад чытання, а не ад залежнасці.
Проблемы з залежнасцямі — это сигналы пра недастаткі дизайну. Такое паганае поведэння пачынаецца, калі код прызначае адну трымкі для закрытай функцыі, тады як функцыя патрэбна іншая.
Слухачы з’яв выжываюць дольш чым контэкст, які іх стварыў
Слухачы спецыяльна ствараюць перерыв між рэгістрацыяй і выкананнем. Вы прыўязуеце обробнік раз, і ён запускаецца кожны раз, калі вызначаецца запускаваная подыя, можа, часоў пазней, чым зменіліся суседзяючыя зменныя.
У стандартнам JavaScript-е слухач, які чытае зменную на рэвэлі модуля, бачыць яе найсяроднейшую значэнне. У фрамворку компанентаў слухач, прыўязанный падчас раннейга атрыбутавання, застаецца з тымі ж атрыбутамі таго атрыбутавання. У будзь-якам случае гэтыя зв’язкі лёгка працягнуць, таму што обробнік запускаецца толькі тады, калі корыстнік пазнейша здзейсніў які-небудзь заход.
Чыстка дадае ўтолькі што новую вялічыну. Якщо кожны раз пад час адрасавання дадзеныя прыўязуецца новы слухальнік без адключэння пярэйшага, канечна выходзіць так, што кальцыяцыі рэагуюць на адно і тое ж з’явленне, пры чым кожны з яных трывае розную версію стану. Тады аднальны клік можа прывести да калькольных наследкаў, якія выйшлі з разных момантамі історыі прыкладнення. Видавальны рэзультат можа быть дуплікаваным апдэйтом, старым значэннем, якое з’являецца па-пераднему, або обробнікам, які працюе пасля таго, як його компонент быў ад’язваны. Основная прычына ў кожным случае заключаецца у тым, што трымачка жыццёвага циклу слухальніка ніколі не была супараднаўаная з трымачкай жыццёвага циклу стану, ад каторога ён залежыць.
Надзеяны код слухальніка чыні власнасць явной:
- Зберагаюце кансэкт да самэй функцыі, яку вы зарэгістравалі, таму што
removeEventListenerпатрэбны той самы кансэкт.
Усё гэта не ўскрынка формальнасцяў. Рэгістрацыя абшыранніка-выкліку стварае зв’язак межу будучымі запускамі і сяродавай средой. Якщо гэты зв’язак павінен завершыцца, код павінен гэта зробіць.
Асінхронныя адпаведзі носаць стары намер у новы экран
Запросы да сеті ствараюць адныя з найболей дорогіх багоў, вызваных застарэлым контекстам. Запрос пачынаецца, калі корыстувальнік адглядае певны запит, проект чы маршрут. Перш чым ён завершыцца, корыстувальнік пераходзіць куды-інша. Калі прыходзіць адпаведзь, яе абшыраннік-выкліку запісвае данні ў спяльны стан, выкарыстоўваючы контекст, які быў зафіксаваны на пачатку.
Інодзе такі історычныя контэксты ўсё правільны. Запрос, паданы для проекта 42, должен застацца прыяўленым да проекта 42 нават пасля таго, як актыўны проект стане 84. Апасцярожнасць трэба падкрэсты, калі такі результат можа апдэйтаваць экран, які ўжо перайшоў на проект 84. Памяць пра первісны запрос — гэта тое, што дапамагае правільна завершыць задачу; але як толькі вы прыпускаете, што завершаная адпаведь яшчэ трэба, тады логіка паспяшаецца.
Самэльга таму логіка завершання задач і асінхронна власнасць выкананыя разам. Калебэк можа зберагаць абсолютна правільныя історычныя значэння, але ўсё рав не мае права апдэйтаваць той об’ект, які ён нарабатывае. Частыя меры захоплення включаюць:
- ID запросу або номер парады, які паўтараюць перад тым, як застосаваць результат
- сігнал
AbortController, які абрывае роботу, калі корыстнік пераходзіць да іншага - перакананне, што текущая маршрутка або выбор усё ўсё ўзмоўлены з запросам
Проста перапісваўка функцыі-вярнення, каб чытаць найновейшы актыўны проект, можа пагоршыць ситуацыю: адпаведны рэзультат для проекта 42 будзе зберагнуты пад проектам 84. Чытанне тэкучага стану не є універсальным рашэнням. Сахавайце ідэнтычнасць запиту та пераканайцеся, што пункт прызначэння все ўсё ж хочаць гэты рэзультат, прычым яго зберагачыце.
Сахавайце два контексты околачынны. Контекст аперацыі належыць да дадзейнасці, за якую быў зданы запит. Контекст інтэрфейсу належыць да таго, што зараз пераглядае корыстнік. Экран должен апдэйтавацца толькі тады, калі гэтыя два элементы яшчэ збігаюцца. Без такога раздзелення функцыі-вярнення ствараюць вражанне „паўтарэння часу“, калі вярнюецца правядзёмая інформацыя з болей раннего моменту ў відобразэнне, якое вже змянілася.
Выбір между кэшаваным знімкам і жывым доступам
Большая частка гэтых багоў становіцца простымі, калі команда называе жаданую зв’язку межу элементамі.
Снімак означае, што запізнена робота выкарыстоўвае даныя такімі, якімі яны былі на момент стварэння роботы. Гэта пасводзіць да ідэнтыфікатораў транзакцый, ідэнтыфікатора выбранага запису, значэнняў заполненай формы, контекста аудыту і команд, якія павінны застацца ў своем первасным значэнні. Яго можна реалізаваць праз перадачу значэнняў як аргументаў, стварэнне незменных пакетаў дадзенаў або копіюванне толькі тых полей, якія неабходны для аперацыі.
Рэальны доступ означае, што запізнена робота выкарыстоўвае найновейшае значэнне на момент ўжывання. Гэта пасводзіць да статусу з’язку, найсвежэйшай настройкі, дзеякога потычнага стану інтэрфейсу ў працоўніках падзеяў і зменных значэнняў коордынацыі. Яго можна реалізаваць праз спяльную прыязу, рэферэнс, адказчык хранілні або іншы джерэл, якое ўсунуто чыніцца „потычным“.
У загальным сэнсе ні адна з іх не ўважаецца болей безпечной. Бягі з’яўляюцца, калі код реалізуе адну з варыянтных стратэгій, тады калі разработчык прыпускае іншую.
Две прычыны робяць выбор зрозумілым у коде. Першая — утрымайцеся ад клаусураў, якія без разбору выкарыстоўваюць усе зместы ў межах свайго дасягнення; шырокі клаусура масківае свае залежнасці, таму чытальнік не можа зрозумець, калкі з іх павінны застацца незменнымі, а калкі — актуальнымі. Другая — вярніцеся да маленькіх функцый з частым наборам параметраў, ўсё гэта зменшае колькість зменных, пры якіх трэба врачуваць семантыку часу. Обе стратэгіі павышаюць надзеянасць асінхронных задач з той жа прычыны: менша колькасць неявных зв’язкаў з часам.
Короткі список пераконтраць для будзь-яга калебэка, які выкананы пазней:
- Калкі зовнішнія зменныя ён чытае?
- Для кожной з іх, чы хаця бы ён меў бачыць яе значэнне пад час стварэння чы пад час выканання?
- Чы є сярод іх якісь зменныя об’екты, чыё содержанне магчыма зменіцца за гэты час?
Узагальненне
Клозуры ў JavaScript являюцься дэтерміністычнымі. Яны следуюць лексычнаму дыяпазону, зберагаюць связі і падтрымваюць сяродавышча столькі, сколькі ім патрабуе функцыя, якая можа даўно ўжо вызваць іх. Тое, што стварае вражанне, што яны «перашкоджаюць», — гэта калі зменную спрыяваць так, нібы яна мела адну значныю значэнне працоўна часу.
Кожны з вышэйзгаданых сценарыяў ёсць варіяцыяю адной запитанні: календарны момент, да каго павінен належаць гэты калебак? Цикл можа даць усім своім калебакам адну спяльную звязь. Таймер можа чытаць об’ект пасля таго, як ён быў зменены. Калебак у React можа застацца прычастным да пярэдньага рендару. Аслухальнік можа прыжыць стану, для якога ён быў створаны. Асінхронны адпаведзь можа перадаць правильную історычную мету ў відварот, які ўжо перайшаў да наступнага кроку.
Аптэкты разработчыкі все ўсё сталкаюцца з гэтымі багамі, таму што сучасныя прыкладнікі постаўляюць выкананне задач на пазнейшы час. Таймауты, ланцюгі обяв, адбуванні DOM, падпісы, перзапускі, адрагі задач і адпаведныя HTTP-адказы ўсе ствараюць перашчэпку між заданнем функцыі і яе выкананнем, і чым большая гэта перашчэпка, тым болей зменяецца сурאунд. Захопленне не ў тым, каб запам’ятаваць яшчо адна версію клаусураў. Це ў тым, каб чыткая адзначыць час і власніку: свядома выбіраць копію стану або рэальны доступ, зберагаць толькі тыя історычныя значэнні, якія патрэбны для выканання задачы, надаць текущам стану адзін цаленаправлены источнік, прыяўзаць трымачак час жыцця кожнага калбэка да таго, хто яго володзіць, і не дазволяць застарэлым задачам запісваць данні там, дзе яны больш не кантролююць. Калі гэтыя выборы ўсё яшчэ видны ў кодзе, клаусуры больш не становяць ся прыкметам, таму што ўжо нарэшце стаюць часткай таго моменту, які вы планавалі.