Îmbunătățiți-vă cunoștințele JavaScript citind codul sursă
Publicat: 2022-03-10Îți amintești prima dată când ai săpat adânc în codul sursă al unei biblioteci sau al unui cadru pe care îl folosești frecvent? Pentru mine, acel moment a venit în timpul primului meu job ca dezvoltator frontend în urmă cu trei ani.
Tocmai terminasem de rescrierea unui cadru moștenit intern pe care l-am folosit pentru a crea cursuri de e-learning. La începutul rescrierii, am petrecut timp investigând o serie de soluții diferite, inclusiv Mithril, Inferno, Angular, React, Aurelia, Vue și Polymer. Deoarece eram foarte începător (tocmai treceam de la jurnalism la dezvoltarea web), îmi amintesc că m-am simțit intimidat de complexitatea fiecărui cadru și nu am înțeles cum funcționează fiecare.
Înțelegerea mea a crescut când am început să investighez mai aprofundat cadrul ales de noi, Mithril. De atunci, cunoștințele mele despre JavaScript – și despre programare în general – au fost foarte mult ajutate de orele pe care le-am petrecut să sapă adânc în intestinele bibliotecilor pe care le folosesc zilnic, fie la serviciu, fie în propriile proiecte. În această postare, voi împărtăși câteva dintre modalitățile prin care puteți lua biblioteca sau cadrul preferat și o puteți folosi ca instrument educațional.
Beneficiile citirii codului sursă
Unul dintre avantajele majore ale citirii codului sursă este numărul de lucruri pe care le poți învăța. Când m-am uitat prima oară în baza de cod a lui Mithril, am avut o idee vagă despre ce este DOM-ul virtual. Când am terminat, am știut că DOM-ul virtual este o tehnică care implică crearea unui arbore de obiecte care descrie cum ar trebui să arate interfața ta cu utilizatorul. Arborele respectiv este apoi transformat în elemente DOM folosind API-uri DOM, cum ar fi document.createElement
. Actualizările sunt efectuate prin crearea unui nou arbore care descrie starea viitoare a interfeței cu utilizatorul și apoi comparându-l cu obiectele din arborele vechi.
Citisem despre toate acestea în diverse articole și tutoriale și, deși a fost util, posibilitatea de a le observa la locul de muncă în contextul unei aplicații pe care am trimis-o a fost foarte iluminator pentru mine. De asemenea, m-a învățat ce întrebări să pun atunci când compar diferite cadre. În loc să mă uit la stelele GitHub, de exemplu, acum știam să pun întrebări precum: „Cum afectează modul în care fiecare cadru realizează actualizările performanța și experiența utilizatorului?”
Un alt beneficiu este o creștere a aprecierii și înțelegerii unei arhitecturi bune a aplicațiilor. În timp ce majoritatea proiectelor open-source urmează în general aceeași structură cu depozitele lor, fiecare dintre ele conține diferențe. Structura lui Mithril este destul de plată și, dacă sunteți familiarizat cu API-ul său, puteți face ipoteze educate despre cod în foldere precum render
, router
și request
. Pe de altă parte, structura lui React reflectă noua sa arhitectură. Menținătorii au separat modulul responsabil pentru actualizările UI ( react-reconciler
) de modulul responsabil pentru redarea elementelor DOM ( react-dom
).
Unul dintre avantajele acestui lucru este că acum este mai ușor pentru dezvoltatori să-și scrie propriile randări personalizate prin conectarea la pachetul react-reconciler
. Parcel, un bundler de module pe care l-am studiat recent, are și un folder de packages
precum React. Modulul cheie se numește parcel-bundler
și conține codul responsabil pentru crearea pachetelor, pornirea serverului de modul cald și instrumentul de linie de comandă.
Un alt beneficiu - care a fost o surpriză binevenită pentru mine - este că deveniți mai confortabil citind specificația oficială JavaScript care definește modul în care funcționează limbajul. Prima dată când am citit specificațiile a fost când investigam diferența dintre throw Error
și throw new Error
(alertă spoiler - nu există). M-am uitat în acest lucru pentru că am observat că Mithril a folosit throw Error
în implementarea funcției sale m
și m-am întrebat dacă există un beneficiu în folosirea lui peste throw new Error
. De atunci, am mai învățat că operatorii logici &&
și ||
nu returnează neapărat booleeni, s-au găsit regulile care guvernează modul în care operatorul de egalitate ==
constrânge valorile și motivul pentru care Object.prototype.toString.call({})
returnează '[object Object]'
.
Tehnici de citire a codului sursă
Există multe moduri de a aborda codul sursă. Am găsit că cea mai ușoară modalitate de a începe este să selectez o metodă din biblioteca aleasă și să documentez ce se întâmplă când o apelați. Nu documentați fiecare pas, ci încercați să identificați fluxul și structura generală a acestuia.
Am făcut asta recent cu ReactDOM.render
și, prin urmare, am învățat multe despre React Fiber și despre unele dintre motivele din spatele implementării sale. Din fericire, deoarece React este un cadru popular, am dat peste o mulțime de articole scrise de alți dezvoltatori pe aceeași problemă și acest lucru a accelerat procesul.
Această scufundare profundă m-a introdus și în conceptele de programare cooperativă, metoda window.requestIdleCallback
și un exemplu real de liste legate (React gestionează actualizările punându-le într-o coadă care este o listă legată de actualizări prioritizate). Când faceți acest lucru, este recomandabil să creați o aplicație foarte simplă folosind biblioteca. Acest lucru facilitează depanarea deoarece nu trebuie să vă ocupați de urmele stivei cauzate de alte biblioteci.
Dacă nu fac o revizuire aprofundată, voi deschide folderul /node_modules
dintr-un proiect la care lucrez sau voi merge la depozitul GitHub. Acest lucru se întâmplă de obicei când dau peste o eroare sau o caracteristică interesantă. Când citiți codul pe GitHub, asigurați-vă că citiți din cea mai recentă versiune. Puteți vizualiza codul de la commit-uri cu eticheta de cea mai recentă versiune făcând clic pe butonul folosit pentru a schimba ramurile și selectați „etichete”. Bibliotecile și cadrele sunt mereu supuse modificărilor, așa că nu doriți să aflați despre ceva ce ar putea fi abandonat în versiunea următoare.
Un alt mod mai puțin implicat de a citi codul sursă este ceea ce îmi place să numesc metoda „privirii cursive”. La început, când am început să citesc codul, am instalat express.js , am deschis folderul /node_modules
și am trecut prin dependențele sale. Dacă README
-ul nu mi-a oferit o explicație satisfăcătoare, am citit sursa. Făcând acest lucru m-a condus la aceste descoperiri interesante:
- Express depinde de două module care îmbină ambele obiecte, dar fac acest lucru în moduri foarte diferite.
merge-descriptors
adaugă numai proprietăți găsite direct pe obiectul sursă și, de asemenea, îmbină proprietăți nenumerabile, în timp ceutils-merge
doar iterează asupra proprietăților enumerabile ale unui obiect, precum și pe cele găsite în lanțul său de prototipuri.merge-descriptors
foloseșteObject.getOwnPropertyNames()
șiObject.getOwnPropertyDescriptor()
în timp ceutils-merge
foloseștefor..in
; - Modulul
setprototypeof
oferă o modalitate transversală de setare a prototipului unui obiect instanțiat; -
escape-html
este un modul de 78 de linii pentru evadarea unui șir de conținut, astfel încât să poată fi interpolat în conținut HTML.
În timp ce rezultatele nu vor fi utile imediat, este util să aveți o înțelegere generală a dependențelor utilizate de biblioteca sau cadrul dvs.
Când vine vorba de depanarea codului front-end, instrumentele de depanare ale browserului sunt cele mai bune prietene. Printre altele, vă permit să opriți programul în orice moment și să inspectați starea acestuia, să omiteți execuția unei funcții sau să intrați sau să ieșiți din el. Uneori, acest lucru nu va fi imediat posibil deoarece codul a fost redus. Am tendința de a-l deminifica și de a copia codul neminificat în fișierul relevant din folderul /node_modules
.
Studiu de caz: Funcția Redux Connect
React-Redux este o bibliotecă folosită pentru a gestiona starea aplicațiilor React. Când am de-a face cu biblioteci populare precum acestea, încep prin a căuta articole care au fost scrise despre implementarea acesteia. Făcând acest lucru pentru acest studiu de caz, am dat peste acest articol. Acesta este un alt lucru bun despre citirea codului sursă. Faza de cercetare te duce de obicei către articole informative ca acesta, care doar îți îmbunătățesc propria gândire și înțelegere.
connect
este o funcție React-Redux care conectează componentele React la magazinul Redux al unei aplicații. Cum? Ei bine, conform documentelor, face următoarele:
„... returnează o clasă de componente nouă, conectată, care include componenta pe care ați transmis-o.”
După ce am citit asta, aș pune următoarele întrebări:
- Cunosc modele sau concepte în care funcțiile preiau o intrare și apoi returnează aceeași intrare învelită cu funcționalitate suplimentară?
- Dacă cunosc astfel de modele, cum l-aș implementa pe baza explicației oferite în documente?
De obicei, următorul pas ar fi crearea unui exemplu de aplicație de bază care utilizează connect
. Cu toate acestea, cu această ocazie am optat să folosesc noua aplicație React pe care o construim la Limejump pentru că am vrut să înțeleg connect
în contextul unei aplicații care va intra în cele din urmă într-un mediu de producție.
Componenta pe care mă concentrez arată astfel:
class MarketContainer extends Component { // code omitted for brevity } const mapDispatchToProps = dispatch => { return { updateSummary: (summary, start, today) => dispatch(updateSummary(summary, start, today)) } } export default connect(null, mapDispatchToProps)(MarketContainer);
Este o componentă de container care înfășoară patru componente mai mici conectate. Unul dintre primele lucruri pe care le întâlniți în fișierul care exportă metoda de connect
este acest comentariu: conectarea este o fațadă peste connectAdvanced . Fără să mergem departe, avem primul nostru moment de învățare: o oportunitate de a observa modelul de design al fațadei în acțiune . La sfârșitul fișierului vedem că connect
exportă o invocare a unei funcții numită createConnect
. Parametrii săi sunt o grămadă de valori implicite care au fost destructurate astfel:
export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory } = {})
Din nou, întâlnim un alt moment de învățare: exportarea funcțiilor invocate și destructurarea argumentelor funcției implicite . Partea de destructurare este un moment de învățare pentru că dacă codul ar fi fost scris astfel:
export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory })
Ar fi dus la această eroare Uncaught TypeError: Cannot destructure property 'connectHOC' of 'undefined' or 'null'.
Acest lucru se datorează faptului că funcția nu are niciun argument implicit la care să recurgă.
Notă : Pentru mai multe despre aceasta, puteți citi articolul lui David Walsh. Unele momente de învățare pot părea banale, în funcție de cunoștințele tale de limbă, așa că ar fi mai bine să te concentrezi asupra lucrurilor pe care nu le-ai văzut până acum sau despre care trebuie să înveți mai multe.
createConnect
în sine nu face nimic în corpul său de funcție. Returnează o funcție numită connect
, cea pe care am folosit-o aici:
export default connect(null, mapDispatchToProps)(MarketContainer)
Este nevoie de patru argumente, toate opționale, iar primele trei argumente trec fiecare printr-o funcție de match
care ajută la definirea comportamentului lor în funcție de faptul dacă argumentele sunt prezente și tipul lor de valoare. Acum, deoarece al doilea argument furnizat pentru a se match
este una dintre cele trei funcții importate în connect
, trebuie să decid ce fir să urmez.
Există momente de învățare cu funcția proxy folosită pentru a încheia primul argument pentru a connect
dacă acele argumente sunt funcții, utilitarul isPlainObject
folosit pentru a verifica obiectele simple sau modulul de warning
care dezvăluie cum puteți seta depanatorul să întrerupă toate excepțiile. După funcțiile de potrivire, ajungem la connectHOC
, funcția care ia componenta noastră React și o conectează la Redux. Este o altă invocare a funcției care returnează wrapWithConnect
, funcția care se ocupă de fapt de conectarea componentei la magazin.
Privind implementarea connectHOC
, pot aprecia de ce trebuie să connect
pentru a-și ascunde detaliile de implementare. Este inima lui React-Redux și conține o logică care nu trebuie să fie expusă prin connect
. Chiar dacă voi încheia scufundarea aici, dacă aș fi continuat, acesta ar fi fost momentul perfect pentru a consulta materialul de referință pe care l-am găsit mai devreme, deoarece conține o explicație incredibil de detaliată a bazei de cod.
rezumat
Citirea codului sursă este dificilă la început, dar, ca în orice, devine mai ușoară cu timpul. Scopul nu este de a înțelege totul, ci de a ieși cu o perspectivă diferită și cunoștințe noi. Cheia este să fii deliberat cu privire la întregul proces și să fii intens curios pentru tot.
De exemplu, mi s-a părut interesantă funcția isPlainObject
, deoarece folosește acest lucru if (typeof obj !== 'object' || obj === null) return false
pentru a ne asigura că argumentul dat este un obiect simplu. Când am citit prima dată implementarea sa, m-am întrebat de ce nu a folosit Object.prototype.toString.call(opts) !== '[object Object]'
, care este mai puțin cod și face distincție între obiecte și subtipuri de obiecte, cum ar fi Data obiect. Cu toate acestea, citirea următoarei rânduri a arătat că, în cazul extrem de puțin probabil în care un dezvoltator care folosește connect
returnează un obiect Date, de exemplu, acesta va fi gestionat de verificarea Object.getPrototypeOf(obj) === null
.
Un alt pic de intrigă în isPlainObject
este acest cod:
while (Object.getPrototypeOf(baseProto) !== null) { baseProto = Object.getPrototypeOf(baseProto) }
Unele căutări Google m-au condus la acest fir StackOverflow și la problema Redux care explică modul în care codul tratează cazuri, cum ar fi verificarea față de obiecte care provin dintr-un iFrame.
Linkuri utile despre citirea codului sursă
- „Cum să inginerii inverse a cadrelor”, Max Koretskyi, Medium
- „Cum să citești codul”, Aria Stewart, GitHub