
Gimęs ir ištaisytas per dvylika minučių
GeekBye v1.8.10 changelog sako, kad ištaisė strigtį redaguojant klaviatūros sparčiuosius klavišus. Ištaisė — bet strigtis buvo įvesta ir ištaisyta tame pačiame pull request, dvylikos minučių skirtumu, ir nepasiekė nė vieno naudotojo. Tikroji istorija — patikimumo kaskada, kuri ją pagimdė: mažas, teisingas pakeitimas to, kada spartieji klavišai pristabdomi, ir React išardymo bagas, iškritęs iš vienos eilutės, skirtos padaryti redaktorių saugesnį.
Štai sąžiningiausias sakinys, kokį galiu parašyti apie GeekBye v1.8.10: strigtis, kurios pataisymu jis garsėja, niekam niekada neįvyko. Changelog sako „Ištaisyta strigtis redaguojant klaviatūros sparčiuosius klavišus ir perjungiant langus vidury redagavimo”, o tai tiesa ir skaitosi kaip bagas, kuris pateko į leidimą, įkando naudotojams ir buvo užlopytas paskesniame leidime. Taip nebuvo. Strigtį įvedė vienas komitas ir ištrynė kitas tame pačiame pull request, dvylikos minučių skirtumu, tą patį penktadienio rytą. Ji gyveno visiškai šakos viduje. Leidimo pastabos aprašo randą, kuris susidarė ir užgijo dar prieš tai, kol kas nors už saugyklos ribų galėjo jį pamatyti.
Tai nėra changelog nesėkmė; tai sąžininga iteratyvaus darbo forma, ir verta ją papasakoti, nes tos dvylika minučių talpina tikrai pamokantį React bagą. Bet kad suprastum strigtį, turi suprasti, kam leidimas iš tikrųjų buvo skirtas — nes strigtis buvo šalutinė žala taisant kažką kita.
Apie ką leidimas iš tikrųjų buvo
GeekBye leidžia tau iš naujo priskirti savo klaviatūros sparčiuosius klavišus Nustatymuose. Kol įrašinėji naują kombinaciją, programa privalo neleisti savo globaliems spartiesiems klavišams suveikti — kitaip Cmd+B paspaudimas jam priskirti tiesiog perjungtų langą, užuot būdamas užfiksuotas. Tad redaktorius pristabdo globalius sparčiuosius klavišus per IPC: rendereris iškviečia window.electronAPI.setShortcutsSuspended(true), o pagrindinio proceso ShortcutsHelper atsako iškviesdamas unregisterAll(), numesdamas kiekvieną globalShortcut registraciją, tad klavišo paspaudimas pasiekia fiksavimo input, o ne paleidžia veiksmą. Atnaujinus jis iškviečia registerGlobalShortcuts(), kad juos grąžintų.
Bagas, nuo kurio viskas prasidėjo (issue #233), buvo apie to pristabdymo apimtį. Senas kodas pristabdydavo sparčiuosius klavišus visam laikui, kol buvo atverta sparčiųjų klavišų nustatymų puslapis — ne tik tada, kai aktyviai įrašinėjai klavišą. Didžiąją laiko dalį tai nematoma. Bet jei atvėrei puslapį, pradėjai daryti kažką kita ir nuėjai šalin taip ir nebaigęs redagavimo, programa galėjo likti su visais globaliais sparčiaisiais klavišais neregistruotais — tyliai negyvais — kol negrįžai. Komitas, kuris tai taiso, atviras dėl simptomo: per platus pristabdymas rizikavo „priversti programą atrodyti sugedusią, jei naudotojas pamiršo, kad puslapis atvertas”.
Pataisymas — vienos eilutės susiaurinimas: pristabdyti tik tada, kai eilutė iš tikrųjų redaguojama — raktuojant ties !!editingShortcut, hook būsena, laikančia įrašinėjamos eilutės id — vietoj viso apsilankymo. Geras pakeitimas. Bet „pristabdyti tik redaguojant” kelia neatidėliotiną klausimą: kas laikoma redagavimo pabaiga? Klavišo paspaudimas jį baigia. Escape paspaudimas jį atšaukia. Bet kas, jei tu tiesiog… spusteli šalin arba perjungi į kitą programą visiškai, vidury fiksavimo? Jei niekas neatšaukia redagavimo, lieki įrašinėjantis spartųjį klavišą į langą, kuris neturi fokuso, o spartieji klavišai lieka pristabdyti. Tad tas pats PR pridėjo apsauginį vožtuvą: atšaukti redagavimą, kai fokusas išeina. Ir tai ta eilutė, kuri strigo.
Dvylikos minučių bagas
Apsauginis vožtuvas buvo useEffect faile ShortcutsSettings.tsx, kuris, kol eilutė buvo redaguojama, klausėsi blur:
const handleBlur = () => cancelEditing()
window.addEventListener('blur', handleBlur)
inputRef.current?.addEventListener('blur', handleBlur) // the landmine
Du klausytojai. Vienas ant window — suveikia, kai perjungi į kitą programą. Vienas prikabintas tiesiai, kaip žalias DOM klausytojas, prie fiksavimo <input> — skirtas suveikti, kai spusteli šalin ant kito elemento. Jie atrodo pertekliniai ir nekenksmingi. Jie tokie nėra.
Pereik seką „langų perjungimui vidury redagavimo”, kuri yra būtent changelog frazė:
- Įrašinėji spartųjį klavišą;
<input>turi fokusą. Padarai Cmd+Tab į kitą programą. - Suveikia lango blur.
handleBlurpaleidžiacancelEditing(), kuris nustatoeditingShortcutatgal įnull. - Tos būsenos nustatymas atjungia
<input>— eilutė palieka redagavimo režimą, tad input pašalinamas iš DOM. - Fokusuoto elemento pašalinimas iš DOM sinchroniškai išsiunčia
blurįvykį ant to elemento. ŽaliasinputRefklausytojas jį pagauna ir iškviečiacancelEditing()vėl — šįkart vidury React commit fazės, kol komponentų medis ardomas. - Būsenos setterio iškvietimas ant fiberio, esančio vidury išardymo, užkabina vieną iš vidinių React invariantų, ir jis išmeta:
"Should have a queue". Nustatymų vaizdas strigsta.
Bagas nėra lango klausytojas ir tai iš tikrųjų nėra „atšaukti ties blur” koncepcija. Esmė ta, kad vienas iš dviejų klausytojų buvo žalias addEventListener ant React valdomo elemento. React apie tą klausytoją nežino, neišvalo jo atjungiant ir — kas svarbiausia — naršyklė iššauna blur būtent to pašalinimo iš DOM metu, kurį React vykdo, tad tvarkyklė pakartotinai įeina į tavo būsenos logiką pačiu blogiausiu įmanomu momentu. Pataisymo komito žinutė sako tai tiksliau, nei galiu aš: „when cancelEditing unmounts the input, the blur fires and calls cancelEditing again during React's render cycle.”
Pataisymas
bf28a50, dvylika minučių po to, kai strigtis gimė, daro du mažus dalykus.
Pirma, jis ištrina žalią input klausytoją ir palieka tik lango, kuris vienintelis apskritai turėjo būti rankiniu DOM klausytoju (nėra React onBlur skirto „visas langas prarado fokusą”):
const handleWindowBlur = () => cancelEditing()
window.addEventListener('blur', handleWindowBlur)
return () => {
window.removeEventListener('blur', handleWindowBlur)
}
Antra, jis perkelia spustelėjimo-šalin atvejį — „naudotojas spustelėjo kitą elementą” — ant paties React onBlur propo ant input ir atideda jį per tiksą:
onBlur={() => {
// Deferred to avoid state updates during React's render cycle.
setTimeout(() => cancelEditing(), 0)
}}
Abi dalys svarbios. React onBlur propo naudojimas vietoj žalio klausytojo reiškia, kad React valdo tvarkyklės gyvavimo ciklą ir nepaliks jos kaboti. O setTimeout(…, 0) garantuoja, kad net kai šis blur iš tikrųjų sukeltas atjungimo, cancelEditing() iškvietimas nusileidžia kitą tiksą — po to, kai dabartinis renderis ir commit pasibaigė — tad jis niekada negali atnaujinti būsenos ant komponento, kuris vis dar ardomas. Jei komponentas iki tol jau dingęs, atidėtas iškvietimas yra nekenksminga tuščia operacija.
Tai visas pataisymas. Ištrink klausytoją, kurio React nematė; leisk React suplanuoti tą, kuris liko.
Kodėl tai geras nuobodulio tipas
Yra pagunda priskirti tai „trivialiam” — setTimeout, ištrinta eilutė — ir eiti toliau. Bet jo forma yra vienas dažniausių būdų, kuriais strigsta React darbalaukio programos, ir verta jį įsisąmoninti:
- Strigtis pasirodo tik ties gyvavimo ciklo riba (atjungimas), sukelta įvykio, kurio karkasas neplanuoja (natyvus blur DOM pašalinimo metu). Ji nepasirodys perspaudinėjimo teste; jai reikia konkretaus „fokusas išeina redaguojant” kelio.
- Šakninė priežastis yra dviejų nuosavybės modelių maišymas — žalių DOM įvykių ir sintetinių, planuojamų React įvykių — ant to paties elemento. Tą akimirką, kai tavo tvarkyklė keičia būseną, galinčią atjungti tą elementą, žalias kelias tampa pakartotinai įeinamas.
- Pataisymas nėra „pridėti apsauginę vėliavėlę” (nors
isMountedrefas būtų tai užmaskavęs); tai visiškas žalio klausytojo pašalinimas, kuris yra mažesnis pakeitimas ir teisingas. Vėl atimtis.
Ir meta-pamoka, ta, apie kurią visa ši serija vis sukasi: skaityk diff, ne leidimo pastabą. „Ištaisyta strigtis redaguojant klaviatūros sparčiuosius klavišus” yra tikslu, bet slepia, kad strigtis buvo susižalota pačiam, tą patį rytą, ir niekada nepateko į leidimą — ir kad tikrasis leidimas yra tyli teisingumo pergalė apie tai, kada pristabdyti globalų spartųjį klavišą.
Kas dar išėjo
Dar du dalykai atvažiavo kartu v1.8.10, abu verti eilutės. Sparčiųjų klavišų sistema taip pat gavo savaiminį gijimą per miegą/pabudimą: macOS gali tyliai numesti programos globalių sparčiųjų klavišų registracijas, kai mašina užmiega, tad atnaujinimo tvarkyklė dabar iš naujo patikrina ir iš naujo registruoja juos, o tai reiškia, kad tavo spartieji klavišai vis dar veikia po nešiojamojo kompiuterio pabudimo be perkrovimo. O DI pusėje klientas pasidarė protingesnis dėl greičio limitų — srautinės užklausos, kurios atsitrenkia į 429, dabar eksponentiškai atsitraukia ir iš naujo nustato savo skaitiklį, kai sustabdai įrašinėjimą, užuot laukusios fiksuoto vėlavimo. Trys patikimumo pataisymai skirtingose posistemėse, iš kurių viena — strigtis, kuri niekada nebuvo strigtis.
Ankstesniam v1 istorijos skyriui — susitikimo spausdinimas į PDF be PDF bibliotekos (v1.8.9); o visai istorijos linijai — programinės įrangos tobulybės pristatymo anatomija.