
Sündinud ja parandatud kaheteistkümne minutiga
GeekBye v1.8.10 muudatuste logi ütleb, et see parandas krahhi klaviatuuri otseteede redigeerimisel. Nii oligi — aga krahh tutvustati ja parandati samas pull requestis, kaheteistkümne minuti vahega, ega jõudnud ühegi kasutajani. Tegelik lugu on töökindluse kaskaad, mis selle tekitas: väike, õige muudatus selles, millal otseteed peatatakse, ja React-i lammutamise viga, mis kukkus välja ühest reast, mis pidi redaktorit turvalisemaks tegema.
Siin on kõige ausam lause, mille ma GeekBye v1.8.10 kohta kirjutada saan: krahh, mille parandamise poolest see kuulus on, ei juhtunud kunagi kellelegi. Muudatuste logi ütleb "Fixed a crash while editing keyboard shortcuts and switching windows mid-edit," mis on tõsi ja loeb nagu viga, mis jõudis välja, hammustas kasutajaid ja sai järelparanduses lapitud. Nii see ei olnud. Krahhi tutvustas üks commit ja kustutas teine samas pull requestis, kaheteistkümne minuti vahega, samal reede hommikul. See elas täielikult ühe haru sees. Väljalaskemärkused kirjeldavad armi, mis tekkis ja paranes enne, kui keegi väljaspool repot seda näha sai.
See pole muudatuste logi ebaõnnestumine; see on iteratiivse töö aus kuju, ja seda tasub rääkida, sest need kaksteist minutit sisaldavad tõeliselt õpetlikku React-i viga. Aga selleks, et krahhi mõista, pead sa mõistma, milleks väljalase tegelikult oli — sest krahh oli kaasnev kahju millegi muu parandamisest.
Milleks väljalase tegelikult oli
GeekBye lubab sul oma klaviatuuri otseteid seadetes ümber siduda. Sel ajal, kui sa uut kombinatsiooni salvestad, peab rakendus peatama oma globaalsete otseteede vallandumise — muidu Cmd+B vajutamine selle määramiseks lihtsalt lülitaks akent, selle asemel et olla jäädvustatud. Nii et redaktor peatab globaalsed otseteed üle IPC: renderdaja kutsub window.electronAPI.setShortcutsSuspended(true), ja peaprotsessi ShortcutsHelper vastab, kutsudes unregisterAll(), visates maha iga globalShortcut registreeringu, nii et klahvivajutus jõuab salvestussisendini, selle asemel et vallandada tegevus. Jätkamisel kutsub see registerGlobalShortcuts(), et need tagasi panna.
Viga, mis selle kõige alustas (probleem #233), puudutas selle peatamise ulatust. Vana kood peatas otseteed kogu selleks ajaks, mil otseteede seadete leht oli avatud — mitte ainult sel ajal, kui sa aktiivselt klahvi salvestasid. Enamasti on see nähtamatu. Aga kui sa avasid lehe, hakkasid midagi muud tegema ja navigeerisid eemale, redigeerimist kunagi lõpetamata, võis rakendus jääda kõigi oma globaalsete otseteedega registreerimata — vaikselt surnud — kuni sa tagasi läksid. Commit, mis selle parandab, on sümptomi suhtes avameelne: liiga laialt peatamine riskis "making the app appear broken if the user forgot they had the page open."
Parandus on ühe rea kitsendus: peata ainult siis, kui rida tegelikult redigeeritakse — võtmestatud !!editingShortcut-ile, hook-i olekule, mis hoiab salvestatava rea id-d — mitte kogu külastuseks. Hea muudatus. Aga "peata ainult redigeerimise ajal" tõstatab kohese küsimuse: mis loeb redigeerimise lõpetamiseks? Klahvi vajutamine lõpetab selle. Escape vajutamine tühistab selle. Aga mis siis, kui sa lihtsalt… klõpsad eemale või vahetad keset salvestust hoopis teise rakendusse? Kui miski redigeerimist ei tühista, jääd sa salvestama otseteed aknasse, mis pole fookuses, ja otseteed jäävad peatatuks. Nii et sama PR lisas kaitseklapi: tühista redigeerimine, kui fookus lahkub. Ja see on rida, mis kokku kukkus.
Kaheteistkümne minuti viga
Kaitseklapp oli useEffect failis ShortcutsSettings.tsx, mis, sel ajal kui rida redigeeriti, kuulas blur-i:
const handleBlur = () => cancelEditing()
window.addEventListener('blur', handleBlur)
inputRef.current?.addEventListener('blur', handleBlur) // the landmine
Kaks kuulajat. Üks window-il — vallandub, kui vahetad teise rakendusse. Üks kinnitatud otse, toore DOM-kuulajana, salvestus-<input>-ile — mõeldud vallanduma, kui klõpsad eemale teisele elemendile. Nad näevad välja üleliigsed ja kahjutud. Nad ei ole.
Käi läbi jada "switching windows mid-edit," mis on täpselt muudatuste logi fraas:
- Sa salvestad otseteed;
<input>on fookuses. Sa vajutad Cmd+Tab teise rakendusse. - Akna blur vallandub.
handleBlurjooksutabcancelEditing(), mis seabeditingShortcuttagasinull-iks. - Selle oleku seadmine ühendab
<input>-i lahti — rida lahkub redigeerimisrežiimist, nii et sisend eemaldatakse DOM-ist. - Fookuses elemendi eemaldamine DOM-ist saadab sünkroonselt sellele elemendile
blursündmuse. TooresinputRefkuulaja püüab selle ja kutsubcancelEditing()uuesti — seekord keset React-i commit-faasi, sel ajal kui komponendipuud lammutatakse. - Olekusetteri kutsumine fiber-il, mis on keset lammutamist, komistab ühe React-i sisemise invariandi otsa, ja see viskab:
"Should have a queue". Seadete vaade kukub kokku.
Viga pole akna kuulaja ja see pole tegelikult "tühista blur-il" kontseptsioon. See on see, et üks kahest kuulajast oli toores addEventListener React-i hallataval elemendil. React ei tea sellest kuulajast, ei puhasta seda lahtiühendamisel ja — otsustavalt — brauser vallandab blur-i just selle DOM-eemaldamise ajal, mida React teostab, nii et käitleja siseneb uuesti sinu olekuloogikasse kõige halvemal võimalikul hetkel. Parandus-commiti sõnum ütleb seda täpsemini, kui mina suudan: "when cancelEditing unmounts the input, the blur fires and calls cancelEditing again during React's render cycle."
Parandus
bf28a50, kaksteist minutit pärast krahhi sündi, teeb kaks väikest asja.
Esiteks, see kustutab toore sisendkuulaja ja hoiab ainult akna oma, mis on ainuke, mis üldse vajaski manuaalset DOM-kuulajat (pole React-i onBlur-i selleks, et "the whole window lost focus"):
const handleWindowBlur = () => cancelEditing()
window.addEventListener('blur', handleWindowBlur)
return () => {
window.removeEventListener('blur', handleWindowBlur)
}
Teiseks, see liigutab klõps-eemale juhtumi — "user clicked another element" — React-i enda onBlur propile sisendil, ja lükkab selle ühe tiksu edasi:
onBlur={() => {
// Deferred to avoid state updates during React's render cycle.
setTimeout(() => cancelEditing(), 0)
}}
Mõlemad osad on olulised. React-i onBlur propi kasutamine toore kuulaja asemel tähendab, et React omab käitleja elutsüklit ega jäta seda rippuma. Ja setTimeout(…, 0) tagab, et isegi kui selle blur-i vallandab lahtiühendamine, langeb cancelEditing() kutse järgmisele tiksule — pärast praeguse renderduse ja commiti lõppu — nii et see ei saa kunagi olekut uuendada komponendil, mis alles lammutub. Kui komponent on selleks ajaks juba läinud, on edasi lükatud kutse kahjutu no-op.
See on kogu parandus. Kustuta kuulaja, mida React ei näinud; lase React-il ajastada see, mis üle jäi.
Miks see on hea sorti igav
On kiusatus liigitada see "triviaalseks" — setTimeout, kustutatud rida — ja edasi minna. Aga selle kuju on üks levinumaid viise, kuidas React-i töölauarakendused kokku kukuvad, ja seda tasub sisemuseks teha:
- Krahh ilmub ainult elutsükli piiril (lahtiühendamine), vallandatuna sündmusest, mida raamistik ei ajasta (natiivne blur DOM-eemaldamise ajal). See ei ilmu klõpsutestis; see vajab konkreetset "fookus lahkub redigeerimise ajal" rada.
- Algpõhjus on kahe omandimudeli segamine — toored DOM-sündmused ja React-i sünteetilised, ajastatud sündmused — samal elemendil. Hetkel, mil su käitleja muudab olekut, mis võib selle elemendi lahti ühendada, muutub toores rada uuestisisenevaks.
- Parandus ei ole "lisa valveslipp" (kuigi
isMountedref oleks selle varjanud); see on toore kuulaja täielik eemaldamine, mis on väiksem muudatus ja õige. Jällegi lahutamine.
Ja metaõppetund, see, mille ümber kogu see seeria ringleb: loe diffi, mitte väljalaskemärkust. "Fixed a crash while editing keyboard shortcuts" on täpne, aga see varjab, et krahh oli iseendale tekitatud, sama hommiku ja ei jõudnud kunagi välja — ja et tegelik väljalase on vaikne korrektsusevõit selle üle, millal globaalne otsetee peatada.
Mis veel välja jõudis
Veel kaks asja sõitsid v1.8.10-s kaasa, mõlemad rea väärt. Otseteesüsteem sai ka eneseparanemise une/ärkamise vältel: macOS võib vaikselt maha visata rakenduse globaalsete otseteede registreeringud, kui masin magab, nii et jätkamise käitleja nüüd kontrollib ja registreerib need uuesti, mis tähendab, et su otseteed töötavad ikka pärast sülearvuti ärkamist ilma taaskäivituseta. Ja AI poolel sai klient nutikamaks kiiruspiirangute osas — voogesituspäringud, mis tabavad 429, taganevad nüüd eksponentsiaalselt ja lähtestavad oma loenduri, kui sa salvestamise peatad, selle asemel et fikseeritud viivitust välja oodata. Kolm töökindluse parandust erinevatele alamsüsteemidele, millest üks on krahh, mis polnud kunagi krahh.
Eelmise peatüki jaoks v1 loos — koosoleku printimine PDF-i ilma PDF-teegita (v1.8.9); ja kogu kaare jaoks tarkvara täiuslikkuseni tarnimise anatoomia.