Migliora la tua conoscenza di JavaScript leggendo il codice sorgente

Pubblicato: 2022-03-10
Riassunto rapido ↬ Quando sei ancora agli inizi della tua carriera di programmatore, scavare nel codice sorgente di librerie e framework open source può essere un'impresa scoraggiante. In questo articolo, Carl Mungazi racconta come ha superato la sua paura e ha iniziato a utilizzare il codice sorgente per migliorare le sue conoscenze e abilità. Usa anche Redux per dimostrare come si avvicina alla demolizione di una libreria.

Ricordi la prima volta che hai approfondito il codice sorgente di una libreria o di un framework che usi frequentemente? Per me, quel momento è arrivato durante il mio primo lavoro come sviluppatore frontend tre anni fa.

Avevamo appena finito di riscrivere un framework legacy interno che abbiamo utilizzato per creare corsi e-learning. All'inizio della riscrittura, abbiamo passato del tempo a studiare diverse soluzioni tra cui Mithril, Inferno, Angular, React, Aurelia, Vue e Polymer. Dato che ero un principiante (ero appena passato dal giornalismo allo sviluppo web), ricordo di essermi sentito intimidito dalla complessità di ogni framework e di non aver capito come funzionassero.

La mia comprensione è cresciuta quando ho iniziato a indagare in modo più approfondito sulla nostra struttura prescelta, Mithril. Da allora, la mia conoscenza di JavaScript - e della programmazione in generale - è stata notevolmente aiutata dalle ore che ho passato a scavare a fondo nelle viscere delle librerie che uso quotidianamente al lavoro o nei miei progetti. In questo post, condividerò alcuni dei modi in cui puoi prendere la tua libreria o framework preferito e usarla come strumento educativo.

Il codice sorgente per la funzione hyperscript di Mithril
La mia prima introduzione alla lettura del codice è stata tramite la funzione hyperscript di Mithril. (Grande anteprima)
Altro dopo il salto! Continua a leggere sotto ↓

I vantaggi della lettura del codice sorgente

Uno dei maggiori vantaggi della lettura del codice sorgente è il numero di cose che puoi imparare. Quando ho esaminato per la prima volta la base di codice di Mithril, avevo una vaga idea di cosa fosse il DOM virtuale. Quando ho finito, mi sono reso conto che il DOM virtuale è una tecnica che prevede la creazione di un albero di oggetti che descrivono come dovrebbe essere la tua interfaccia utente. Quell'albero viene quindi trasformato in elementi DOM utilizzando API DOM come document.createElement . Gli aggiornamenti vengono eseguiti creando un nuovo albero che descrive lo stato futuro dell'interfaccia utente e quindi confrontandolo con gli oggetti del vecchio albero.

Avevo letto tutto questo in vari articoli e tutorial e, sebbene sia stato utile, poterlo osservare al lavoro nel contesto di un'applicazione che avevamo spedito è stato molto illuminante per me. Mi ha anche insegnato quali domande porre quando si confrontano diversi framework. Invece di guardare le stelle di GitHub, ad esempio, ora sapevo di porre domande come "In che modo il modo in cui ogni framework esegue gli aggiornamenti influisce sulle prestazioni e sull'esperienza dell'utente?"

Un altro vantaggio è un aumento dell'apprezzamento e della comprensione della buona architettura dell'applicazione. Sebbene la maggior parte dei progetti open source generalmente segua la stessa struttura con i propri repository, ognuno di essi contiene differenze. La struttura di Mithril è piuttosto piatta e se hai familiarità con la sua API, puoi fare ipotesi plausibili sul codice in cartelle come render , router e request . D'altra parte, la struttura di React riflette la sua nuova architettura. I manutentori hanno separato il modulo responsabile degli aggiornamenti dell'interfaccia utente ( react-reconciler ) dal modulo responsabile del rendering degli elementi DOM ( react-dom ).

Uno dei vantaggi di questo è che ora è più facile per gli sviluppatori scrivere i propri renderer personalizzati collegandosi al pacchetto react-reconciler . Parcel, un bundler di moduli che ho studiato di recente, ha anche una cartella di packages come React. Il modulo chiave è denominato parcel-bundler e contiene il codice responsabile della creazione di bundle, della rotazione del server del modulo caldo e dello strumento della riga di comando.

La sezione della specifica JavaScript che spiega come funziona Object.prototype.toString
Non passerà molto tempo prima che il codice sorgente che stai leggendo ti porti alla specifica JavaScript. (Grande anteprima)

Ancora un altro vantaggio, che è stata una gradita sorpresa per me, è che ti senti più a tuo agio leggendo le specifiche JavaScript ufficiali che definiscono come funziona il linguaggio. La prima volta che ho letto le specifiche è stato quando stavo studiando la differenza tra throw Error e throw new Error (avviso spoiler - non ce n'è nessuno). Ho esaminato questo perché ho notato che Mithril usava throw Error nell'implementazione della sua funzione m e mi chiedevo se ci fosse un vantaggio nell'usarlo oltre al throw new Error . Da allora, ho anche imparato che gli operatori logici && e || non restituiscono necessariamente booleani, ha trovato le regole che regolano il modo in cui l'operatore di uguaglianza == forza i valori e il motivo Object.prototype.toString.call({}) restituisce '[object Object]' .

Tecniche per la lettura del codice sorgente

Ci sono molti modi per avvicinarsi al codice sorgente. Ho trovato il modo più semplice per iniziare è selezionare un metodo dalla libreria scelta e documentare cosa succede quando lo chiami. Non documentare ogni singolo passaggio, ma cercare di identificarne il flusso e la struttura complessivi.

L'ho fatto di recente con ReactDOM.render e di conseguenza ho imparato molto su React Fiber e alcuni dei motivi alla base della sua implementazione. Per fortuna, poiché React è un framework popolare, mi sono imbattuto in molti articoli scritti da altri sviluppatori sullo stesso problema e questo ha accelerato il processo.

Questa immersione profonda mi ha anche introdotto i concetti di pianificazione cooperativa, il metodo window.requestIdleCallback e un esempio reale di elenchi collegati (React gestisce gli aggiornamenti mettendoli in una coda che è un elenco collegato di aggiornamenti prioritari). Quando si esegue questa operazione, è consigliabile creare un'applicazione molto semplice utilizzando la libreria. Ciò semplifica il debug poiché non è necessario gestire le tracce dello stack causate da altre librerie.

Se non sto facendo una revisione approfondita, aprirò la cartella /node_modules in un progetto su cui sto lavorando o andrò al repository GitHub. Questo di solito accade quando mi imbatto in un bug o in una funzionalità interessante. Quando leggi il codice su GitHub, assicurati di leggere dall'ultima versione. Puoi visualizzare il codice dai commit con il tag dell'ultima versione facendo clic sul pulsante utilizzato per modificare i rami e selezionando "tag". Le librerie e i framework sono continuamente soggetti a modifiche, quindi non vuoi saperne di più su qualcosa che potrebbe essere abbandonato nella prossima versione.

Un altro modo meno complicato di leggere il codice sorgente è quello che mi piace chiamare il metodo "sguardo a cursore". All'inizio, quando ho iniziato a leggere il codice, ho installato express.js , ho aperto la sua cartella /node_modules e ho esaminato le sue dipendenze. Se il README non mi ha fornito una spiegazione soddisfacente, ho letto la fonte. Questo mi ha portato a questi interessanti risultati:

  • Express dipende da due moduli che uniscono entrambi gli oggetti ma lo fanno in modi molto diversi. merge-descriptors aggiunge solo proprietà che si trovano direttamente sull'oggetto sorgente e unisce anche proprietà non enumerabili mentre utils-merge scorre solo sulle proprietà enumerabili di un oggetto e su quelle trovate nella sua catena di prototipi. merge-descriptors usa Object.getOwnPropertyNames() e Object.getOwnPropertyDescriptor() mentre utils-merge usa for..in ;
  • Il modulo setprototypeof fornisce un modo multipiattaforma per impostare il prototipo di un oggetto istanziato;
  • escape-html è un modulo di 78 righe per l'escape di una stringa di contenuto in modo che possa essere interpolato nel contenuto HTML.

Sebbene sia improbabile che i risultati siano utili immediatamente, è utile avere una comprensione generale delle dipendenze utilizzate dalla libreria o dal framework.

Quando si tratta di eseguire il debug del codice front-end, gli strumenti di debug del browser sono i tuoi migliori amici. Tra le altre cose, consentono di interrompere il programma in qualsiasi momento e di ispezionarne lo stato, saltare l'esecuzione di una funzione o entrarvi o uscirne. A volte questo non sarà immediatamente possibile perché il codice è stato minimizzato. Tendo a annullarlo e copiare il codice non minimizzato nel file pertinente nella cartella /node_modules .

Il codice sorgente per la funzione ReactDOM.render
Avvicinati al debug come faresti con qualsiasi altra applicazione. Forma un'ipotesi e poi verificala. (Grande anteprima)

Caso di studio: funzione Connect di Redux

React-Redux è una libreria utilizzata per gestire lo stato delle applicazioni React. Quando ho a che fare con biblioteche popolari come queste, inizio cercando articoli che sono stati scritti sulla sua implementazione. Nel farlo per questo case study, mi sono imbattuto in questo articolo. Questa è un'altra cosa buona della lettura del codice sorgente. La fase di ricerca di solito ti porta ad articoli informativi come questo che migliorano solo il tuo pensiero e la tua comprensione.

connect è una funzione di React-Redux che collega i componenti di React all'archivio Redux di un'applicazione. Come? Bene, secondo i documenti, fa quanto segue:

"...restituisce una nuova classe di componenti connessa che racchiude il componente che hai passato."

Dopo aver letto questo, vorrei porre le seguenti domande:

  • Conosco modelli o concetti in cui le funzioni accettano un input e quindi restituiscono lo stesso input avvolto con funzionalità aggiuntive?
  • Se conosco tali schemi, come potrei implementarli in base alla spiegazione fornita nei documenti?

Di solito, il passaggio successivo consiste nel creare un'app di esempio molto semplice che utilizzi connect . Tuttavia, in questa occasione ho scelto di utilizzare la nuova app React che stiamo costruendo a Limejump perché volevo capire la connect nel contesto di un'applicazione che alla fine andrà in un ambiente di produzione.

Il componente su cui mi sto concentrando è simile a questo:

 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);

È un componente contenitore che avvolge quattro componenti collegati più piccoli. Una delle prime cose che incontri nel file che esporta il metodo connect è questo commento: connect è una facciata su connectAdvanced . Senza andare lontano abbiamo il nostro primo momento di apprendimento: un'opportunità per osservare il modello di progettazione della facciata in azione . Alla fine del file vediamo che connect esporta un'invocazione di una funzione chiamata createConnect . I suoi parametri sono un mucchio di valori predefiniti che sono stati destrutturati in questo modo:

 export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory } = {})

Ancora una volta, ci imbattiamo in un altro momento di apprendimento: l'esportazione delle funzioni invocate e la destrutturazione degli argomenti delle funzioni predefinite . La parte di destrutturazione è un momento di apprendimento perché se il codice fosse stato scritto in questo modo:

 export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory })

Avrebbe provocato questo errore Uncaught TypeError: Cannot destructure property 'connectHOC' of 'undefined' or 'null'. Questo perché la funzione non ha argomenti predefiniti su cui ripiegare.

Nota : per ulteriori informazioni su questo, puoi leggere l'articolo di David Walsh. Alcuni momenti di apprendimento possono sembrare banali, a seconda della tua conoscenza della lingua, quindi potrebbe essere meglio concentrarsi su cose che non hai visto prima o su cui devi saperne di più.

createConnect stesso non fa nulla nel corpo della sua funzione. Restituisce una funzione chiamata connect , quella che ho usato qui:

 export default connect(null, mapDispatchToProps)(MarketContainer)

Occorrono quattro argomenti, tutti facoltativi, e ciascuno dei primi tre argomenti passa attraverso una funzione di match che aiuta a definire il loro comportamento in base alla presenza degli argomenti e al tipo di valore. Ora, poiché il secondo argomento fornito per la match è una delle tre funzioni importate in connect , devo decidere quale thread seguire.

Ci sono momenti di apprendimento con la funzione proxy utilizzata per avvolgere il primo argomento per connect se quegli argomenti sono funzioni, l'utilità isPlainObject utilizzata per verificare la presenza di oggetti semplici o il modulo di warning che rivela come impostare il debugger per interrompere tutte le eccezioni. Dopo le funzioni di corrispondenza, arriviamo a connectHOC , la funzione che prende il nostro componente React e lo collega a Redux. È un'altra chiamata di funzione che restituisce wrapWithConnect , la funzione che gestisce effettivamente la connessione del componente al negozio.

Osservando l'implementazione di connectHOC , posso capire perché ha bisogno di connect per nascondere i dettagli di implementazione. È il cuore di React-Redux e contiene la logica che non ha bisogno di essere esposta tramite connect . Anche se concluderò qui l'immersione profonda, se avessi continuato, questo sarebbe stato il momento perfetto per consultare il materiale di riferimento che ho trovato in precedenza poiché contiene una spiegazione incredibilmente dettagliata della base di codice.

Sommario

Leggere il codice sorgente è difficile all'inizio, ma come con qualsiasi cosa, diventa più facile con il tempo. L'obiettivo non è capire tutto, ma arrivare con una prospettiva diversa e nuove conoscenze. La chiave è essere deliberati sull'intero processo e intensamente curiosi di tutto.

Ad esempio, ho trovato interessante la funzione isPlainObject perché la usa if (typeof obj !== 'object' || obj === null) return false per assicurarsi che l'argomento fornito sia un oggetto semplice. Quando ho letto per la prima volta la sua implementazione, mi sono chiesto perché non utilizzasse Object.prototype.toString.call(opts) !== '[object Object]' , che è meno codice e distingue tra oggetti e sottotipi di oggetti come Date oggetto. Tuttavia, la lettura della riga successiva ha rivelato che nell'eventualità estremamente improbabile che uno sviluppatore che utilizza connect restituisca un oggetto Date, ad esempio, questo verrà gestito dal controllo Object.getPrototypeOf(obj) === null .

Un altro po' di intrighi in isPlainObject è questo codice:

 while (Object.getPrototypeOf(baseProto) !== null) { baseProto = Object.getPrototypeOf(baseProto) }

Alcune ricerche su Google mi hanno portato a questo thread StackOverflow e al problema Redux che spiega come quel codice gestisce casi come il controllo di oggetti che provengono da un iFrame.

Link utili sulla lettura del codice sorgente

  • "Come invertire i framework di ingegnerizzazione", Max Koretskyi, Medium
  • "Come leggere il codice", Aria Stewart, GitHub