Îmbunătățiți-vă cunoștințele JavaScript citind codul sursă

Publicat: 2022-03-10
Rezumat rapid ↬ Când sunteți încă la începutul carierei dvs. de programator, săpați în codul sursă al bibliotecilor și cadrelor open source poate fi un efort descurajan. În acest articol, Carl Mungazi povestește cum și-a depășit frica și a început să folosească codul sursă pentru a-și îmbunătăți cunoștințele și abilitățile. De asemenea, folosește Redux pentru a demonstra cum abordează distrugerea unei biblioteci.

Îț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.

Codul sursă pentru funcția de hiperscript al lui Mithril
Prima mea introducere în citirea codului a fost prin intermediul funcției hyperscript a lui Mithril. (Previzualizare mare)
Mai multe după săritură! Continuați să citiți mai jos ↓

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ă.

Secțiunea specificației JavaScript care explică cum funcționează Object.prototype.toString
Nu va trece mult până când codul sursă pe care îl citiți vă va conduce la specificația JavaScript. (Previzualizare mare)

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 ce utils-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ște Object.getOwnPropertyNames() și Object.getOwnPropertyDescriptor() în timp ce utils-merge folosește for..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 .

Codul sursă pentru funcția ReactDOM.render
Abordați depanarea ca orice altă aplicație. Formulați o ipoteză și apoi testați-o. (Previzualizare mare)

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