
Dzimis un izlabots divpadsmit minūtēs
GeekBye v1.8.10 izmaiņu žurnālā teikts, ka tas izlaboja avāriju, rediģējot tastatūras saīsnes. Tā arī bija — bet avārija tika ieviesta un izlabota vienā un tajā pašā pull request, divpadsmit minūšu attālumā, un nekad nesasniedza nevienu lietotāju. Īstais stāsts ir uzticamības kaskāde, kas to radīja: neliela, pareiza izmaiņa tajā, kad saīsnes tiek apturētas, un React demontāžas kļūda, kas izbira no vienas rindiņas, kurai vajadzēja padarīt redaktoru drošāku.
Lūk, godīgākais teikums, ko varu uzrakstīt par GeekBye v1.8.10: avārija, ar kuras labošanu tas ir slavens, nekad nevienam nenotika. Izmaiņu žurnālā teikts "Fixed a crash while editing keyboard shortcuts and switching windows mid-edit," kas ir tiesa un lasās kā kļūda, kas tika piegādāta, sakoda lietotājus un tika ielāpota turpinājumā. Tā nebija. Avāriju ieviesa viens komits un izdzēsa cits tajā pašā pull request, divpadsmit minūšu attālumā, tajā pašā piektdienas rītā. Tā pilnībā dzīvoja zarā. Laidiena piezīmes apraksta rētu, kas izveidojās un sadzija, pirms kāds ārpus repozitorija to varēja redzēt.
Tā nav izmaiņu žurnāla neveiksme; tā ir iteratīvā darba godīgā forma, un to ir vērts pastāstīt, jo šīs divpadsmit minūtes satur patiesi pamācošu React kļūdu. Bet, lai saprastu avāriju, tev jāsaprot, kam laidiens patiesībā bija domāts — jo avārija bija blakuszaudējums no kaut kā cita labošanas.
Par ko laidiens patiesībā bija
GeekBye ļauj tev pārsaistīt tā tastatūras saīsnes iestatījumos. Kamēr tu ieraksti jaunu kombināciju, lietotnei ir jāaptur tās globālās saīsnes no iedarbināšanās — citādi Cmd+B nospiešana, lai to piešķirtu, tikai pārslēgtu logu, nevis tiktu tverta. Tāpēc redaktors aptur globālās saīsnes caur IPC: renderētājs izsauc window.electronAPI.setShortcutsSuspended(true), un galvenā procesa ShortcutsHelper atbild, izsaucot unregisterAll(), nometot katru globalShortcut reģistrāciju, lai taustiņa nospiešana sasniedz tvēruma ievadi, nevis iedarbina darbību. Atsākot tas izsauc registerGlobalShortcuts(), lai tās ieliktu atpakaļ.
Kļūda, kas visu to sāka (problēma #233), bija par šīs apturēšanas tvērumu. Vecais kods apturēja saīsnes visu laiku, kamēr saīsņu iestatījumu lapa bija atvērta — nevis tikai tad, kamēr tu aktīvi ieraksti taustiņu. Lielāko daļu laika tas ir neredzams. Bet, ja tu atvēri lapu, sāki darīt kaut ko citu un aizgāji prom, nekad nepabeidzot rediģēšanu, lietotne varēja palikt ar visām tās globālajām saīsnēm nereģistrētām — klusi mirušām — līdz tu atgriezies. Komits, kas to izlabo, ir atklāts par simptomu: pārāk plaša apturēšana riskēja "making the app appear broken if the user forgot they had the page open."
Labojums ir vienas rindiņas sašaurinājums: apturēt tikai tad, kad rinda tiešām tiek rediģēta — atkarīgs no !!editingShortcut, āķa stāvokļa, kas tur tās rindas id, kuru tu ieraksti — nevis uz visu apmeklējumu. Labas izmaiņas. Bet "apturēt tikai rediģēšanas laikā" rada tūlītēju jautājumu: kas skaitās rediģēšanas pabeigšana? Taustiņa nospiešana to pabeidz. Escape nospiešana to atceļ. Bet ko darīt, ja tu vienkārši… noklikšķini prom vai pārslēdzies uz pavisam citu lietotni tvēruma vidū? Ja nekas neatceļ rediģēšanu, tu paliec, ierakstot saīsni logā, kas nav fokusēts, un saīsnes paliek apturētas. Tāpēc tas pats PR pievienoja drošības vārstu: atcelt rediģēšanu, kad fokuss aiziet. Un tā ir rindiņa, kas avarēja.
Divpadsmit minūšu kļūda
Drošības vārsts bija useEffect failā ShortcutsSettings.tsx, kas, kamēr rinda tika rediģēta, klausījās blur:
const handleBlur = () => cancelEditing()
window.addEventListener('blur', handleBlur)
inputRef.current?.addEventListener('blur', handleBlur) // the landmine
Divi klausītāji. Viens uz window — iedarbinās, kad pārslēdzies uz citu lietotni. Viens piesaistīts tieši, kā jēls DOM klausītājs, tvēruma <input> — domāts iedarbināties, kad noklikšķini prom uz citu elementu. Tie izskatās lieki un nekaitīgi. Tie nav.
Izej cauri secībai "switching windows mid-edit," kas ir tieši izmaiņu žurnāla frāze:
- Tu ieraksti saīsni;
<input>ir fokusēts. Tu nospied Cmd+Tab uz citu lietotni. - Loga blur iedarbinās.
handleBlurpalaižcancelEditing(), kas iestataeditingShortcutatpakaļ uznull. - Šī stāvokļa iestatīšana noņem
<input>— rinda pamet rediģēšanas režīmu, tāpēc ievade tiek izņemta no DOM. - Fokusēta elementa izņemšana no DOM sinhroni nosūta
blurnotikumu uz šo elementu. JēlaisinputRefklausītājs to noķer un izsauccancelEditing()vēlreiz — šoreiz React commit fāzes vidū, kamēr komponenšu koks tiek demontēts. - Stāvokļa iestatītāja izsaukšana šķiedrai (fiber), kas ir demontāžas vidū, aizķer vienu no React iekšējiem invariantiem, un tas izmet:
"Should have a queue". Iestatījumu skats avarē.
Kļūda nav loga klausītājs, un tā patiešām nav "atcelt pie blur" jēdziens. Tā ir tas, ka viens no diviem klausītājiem bija jēls addEventListener uz React pārvaldīta elementa. React nezina par šo klausītāju, netīra to demontāžas laikā un — būtiski — pārlūks iedarbina blur tieši tās DOM izņemšanas laikā, ko React veic, tāpēc apstrādātājs atkārtoti ienāk tavā stāvokļa loģikā vissliktākajā iespējamajā mirklī. Labojuma komita ziņojums to saka precīzāk, nekā es varu: "when cancelEditing unmounts the input, the blur fires and calls cancelEditing again during React's render cycle."
Labojums
bf28a50, divpadsmit minūtes pēc tam, kad avārija piedzima, dara divas mazas lietas.
Pirmkārt, tas izdzēš jēlo ievades klausītāju un patur tikai loga, kas ir vienīgais, kuram vispār vajadzēja būt manuālam DOM klausītājam (nav React onBlur priekš "the whole window lost focus"):
const handleWindowBlur = () => cancelEditing()
window.addEventListener('blur', handleWindowBlur)
return () => {
window.removeEventListener('blur', handleWindowBlur)
}
Otrkārt, tas pārvieto klikšķa-prom gadījumu — "user clicked another element" — uz React paša onBlur īpašību uz ievades, un atliec to par vienu soli:
onBlur={() => {
// Deferred to avoid state updates during React's render cycle.
setTimeout(() => cancelEditing(), 0)
}}
Abas daļas ir svarīgas. React onBlur īpašības izmantošana jēla klausītāja vietā nozīmē, ka React pārvalda apstrādātāja dzīves ciklu un neatstās to karājamies. Un setTimeout(…, 0) garantē, ka pat tad, kad šo blur iedarbina demontāža, cancelEditing() izsaukums nonāk nākamajā solī — pēc tam, kad pašreizējā renderēšana un commit ir pabeigti — tāpēc tas nekad nevar atjaunināt stāvokli komponentei, kas joprojām demontējas. Ja komponente līdz tam jau ir prom, atliktais izsaukums ir nekaitīgs no-op.
Tas ir viss labojums. Izdzēs klausītāju, kuru React nevarēja redzēt; ļauj React ieplānot to, kas palicis.
Kāpēc šī ir tā labā garlaicība
Ir kārdinājums to ierindot zem "triviāls" — setTimeout, izdzēsta rindiņa — un iet tālāk. Bet tā forma ir viens no visbiežākajiem veidiem, kā React darbvirsmas lietotnes avarē, un to ir vērts internalizēt:
- Avārija parādās tikai pie dzīves cikla robežas (demontāža), ko iedarbina notikums, kuru ietvars neieplāno (dabisks blur DOM izņemšanas laikā). Tā neparādīsies klikšķu testā; tai vajadzīgs konkrētais "fokuss aiziet rediģēšanas laikā" ceļš.
- Pamatcēlonis ir divu īpašumtiesību modeļu sajaukšana — jēli DOM notikumi un React sintētiskie, ieplānotie notikumi — uz viena un tā paša elementa. Brīdī, kad tavs apstrādātājs maina stāvokli, kas var noņemt šo elementu, jēlais ceļš kļūst atkārtoti ienākošs.
- Labojums nav "pievienot sarga karogu" (lai gan
isMountedatsauce to būtu maskējusi); tas ir jēla klausītāja pilnīga noņemšana, kas ir mazākā izmaiņa un pareizā. Atkal atņemšana.
Un meta-mācība, tā, ap kuru visa šī sērija riņķo: lasi diff, nevis laidiena piezīmi. "Fixed a crash while editing keyboard shortcuts" ir precīzs, bet tas slēpj, ka avārija bija pašnodarīta, tajā pašā rītā un nekad netika piegādāta — un ka īstais laidiens ir kluss pareizības ieguvums par to, kad apturēt globālo saīsni.
Kas vēl tika piegādāts
Vēl divas lietas brauca līdzi v1.8.10, abas vērtas rindiņas. Saīsņu sistēma arī ieguva pašdziedēšanos miega/pamošanās laikā: macOS var klusi nomest lietotnes globālo saīsņu reģistrācijas, kad mašīna iet gulēt, tāpēc atsākšanas apstrādātājs tagad tās atkārtoti pārbauda un reģistrē, kas nozīmē, ka tavas saīsnes joprojām darbojas pēc tam, kad klēpjdators pamostas, bez atkārtotas palaišanas. Un AI pusē klients kļuva gudrāks par ātruma ierobežojumiem — straumēšanas pieprasījumi, kas trāpa 429, tagad atkāpjas eksponenciāli un atiestata savu skaitītāju, kad tu apstādini ierakstīšanu, nevis nogaida fiksētu aizkavi. Trīs uzticamības labojumi dažādām apakšsistēmām, no kuriem viens ir avārija, kas nekad nebija avārija.
Iepriekšējai nodaļai v1 stāstā — sanāksmes drukāšana PDF formātā bez PDF bibliotēkas (v1.8.9); un visai lokai — programmatūras piegādes līdz pilnībai anatomiju.