Verbessern Sie Ihre JavaScript-Kenntnisse, indem Sie den Quellcode lesen

Veröffentlicht: 2022-03-10
Kurze Zusammenfassung ↬ Wenn Sie noch am Anfang Ihrer Programmierkarriere stehen, kann das Graben im Quellcode von Open-Source-Bibliotheken und -Frameworks ein entmutigendes Unterfangen sein. In diesem Artikel erzählt Carl Mungazi, wie er seine Angst überwunden und begonnen hat, Quellcode zu verwenden, um sein Wissen und seine Fähigkeiten zu verbessern. Er verwendet Redux auch, um zu demonstrieren, wie er an die Auflösung einer Bibliothek herangeht.

Erinnern Sie sich an das erste Mal, als Sie sich tief in den Quellcode einer Bibliothek oder eines Frameworks vertieft haben, das Sie häufig verwenden? Für mich kam dieser Moment während meines ersten Jobs als Frontend-Entwickler vor drei Jahren.

Wir hatten gerade die Überarbeitung eines internen Legacy-Frameworks abgeschlossen, das wir zur Erstellung von E-Learning-Kursen verwendet haben. Zu Beginn der Neufassung hatten wir Zeit damit verbracht, eine Reihe verschiedener Lösungen zu untersuchen, darunter Mithril, Inferno, Angular, React, Aurelia, Vue und Polymer. Da ich ein absoluter Anfänger war (ich war gerade vom Journalismus zur Webentwicklung gewechselt), erinnere ich mich, dass ich mich von der Komplexität jedes Frameworks eingeschüchtert fühlte und nicht verstand, wie jedes einzelne funktionierte.

Mein Verständnis wuchs, als ich anfing, unser gewähltes Framework, Mithril, eingehender zu untersuchen. Seitdem wurde mein Wissen über JavaScript – und Programmierung im Allgemeinen – durch die Stunden, die ich damit verbracht habe, tief in die Eingeweide der Bibliotheken zu graben, die ich täglich entweder bei der Arbeit oder in meinen eigenen Projekten verwende, sehr unterstützt. In diesem Beitrag werde ich einige Möglichkeiten aufzeigen, wie Sie Ihre Lieblingsbibliothek oder Ihr bevorzugtes Framework als Lehrmittel verwenden können.

Der Quellcode für die Hyperscript-Funktion von Mithril
Meine erste Einführung in das Lesen von Code erfolgte über die Hyperscript-Funktion von Mithril. (Große Vorschau)
Mehr nach dem Sprung! Lesen Sie unten weiter ↓

Die Vorteile des Lesens von Quellcode

Einer der Hauptvorteile des Lesens von Quellcode ist die Anzahl der Dinge, die Sie lernen können. Als ich mir zum ersten Mal die Codebasis von Mithril ansah, hatte ich eine vage Vorstellung davon, was das virtuelle DOM war. Als ich fertig war, kam ich zu dem Wissen, dass das virtuelle DOM eine Technik ist, bei der ein Baum von Objekten erstellt wird, die beschreiben, wie Ihre Benutzeroberfläche aussehen soll. Dieser Baum wird dann mithilfe von DOM-APIs wie document.createElement in DOM-Elemente umgewandelt. Aktualisierungen werden durchgeführt, indem ein neuer Baum erstellt wird, der den zukünftigen Zustand der Benutzeroberfläche beschreibt, und dieser dann mit Objekten aus dem alten Baum verglichen wird.

Ich hatte über all dies in verschiedenen Artikeln und Tutorials gelesen, und obwohl es hilfreich war, war es für mich sehr aufschlussreich, es im Kontext einer von uns ausgelieferten Anwendung bei der Arbeit beobachten zu können. Außerdem habe ich gelernt, welche Fragen ich stellen muss, wenn ich verschiedene Frameworks vergleiche. Anstatt beispielsweise auf GitHub-Stars zu schauen, wusste ich jetzt, Fragen zu stellen wie: „Wie wirkt sich die Art und Weise, wie jedes Framework Updates durchführt, auf die Leistung und die Benutzererfahrung aus?“

Ein weiterer Vorteil ist die Steigerung Ihrer Wertschätzung und Ihres Verständnisses für eine gute Anwendungsarchitektur. Während die meisten Open-Source-Projekte mit ihren Repositories im Allgemeinen der gleichen Struktur folgen, enthält jedes von ihnen Unterschiede. Die Struktur von Mithril ist ziemlich flach und wenn Sie mit seiner API vertraut sind, können Sie fundierte Vermutungen über den Code in Ordnern wie render , router und request . Andererseits spiegelt die Struktur von React seine neue Architektur wider. Die Betreuer haben das für UI-Updates zuständige Modul ( react-reconciler ) von dem für das Rendern von DOM-Elementen zuständigen Modul ( react-dom ) getrennt.

Einer der Vorteile davon ist, dass es für Entwickler jetzt einfacher ist, ihre eigenen benutzerdefinierten Renderer zu schreiben, indem sie sich in das react-reconciler Paket einklinken. Parcel, ein Modul-Bundler, den ich kürzlich studiert habe, hat auch einen packages wie React. Das Schlüsselmodul heißt parcel-bundler und enthält den Code, der für das Erstellen von Bundles, das Hochfahren des Hot-Modul-Servers und das Befehlszeilentool verantwortlich ist.

Der Abschnitt der JavaScript-Spezifikation, der erklärt, wie Object.prototype.toString funktioniert
Es wird nicht lange dauern, bis Sie der Quellcode, den Sie gerade lesen, zur JavaScript-Spezifikation führt. (Große Vorschau)

Ein weiterer Vorteil – der für mich eine willkommene Überraschung war – ist, dass Sie sich beim Lesen der offiziellen JavaScript-Spezifikation, die definiert, wie die Sprache funktioniert, wohler fühlen. Das erste Mal, dass ich die Spezifikation las, war, als ich den Unterschied zwischen throw Error und throw new Error untersuchte (Spoiler-Alarm – es gibt keinen). Ich habe mir das angesehen, weil mir aufgefallen ist, dass Mithril throw Error bei der Implementierung seiner m -Funktion verwendet hat, und ich mich gefragt habe, ob es einen Vorteil hat, es gegenüber throw new Error zu verwenden. Seitdem habe ich auch gelernt, dass die logischen Operatoren && und || geben nicht unbedingt boolesche Werte zurück, haben die Regeln gefunden, die bestimmen, wie der Gleichheitsoperator == Werte erzwingt, und den Grund, warum Object.prototype.toString.call({}) '[object Object]' zurückgibt.

Techniken zum Lesen von Quellcode

Es gibt viele Möglichkeiten, sich Quellcode zu nähern. Ich habe festgestellt, dass der einfachste Einstieg darin besteht, eine Methode aus der von Ihnen gewählten Bibliothek auszuwählen und zu dokumentieren, was passiert, wenn Sie sie aufrufen. Dokumentieren Sie nicht jeden einzelnen Schritt, sondern versuchen Sie, den gesamten Ablauf und die Struktur zu identifizieren.

Ich habe dies kürzlich mit ReactDOM.render und dadurch viel über React Fiber und einige der Gründe für seine Implementierung gelernt. Da React ein beliebtes Framework ist, bin ich zum Glück auf viele Artikel gestoßen, die von anderen Entwicklern zum gleichen Thema geschrieben wurden, und das hat den Prozess beschleunigt.

Dieser Deep Dive führte mich auch in die Konzepte der kooperativen Planung, der window.requestIdleCallback -Methode und einem realen Beispiel für verknüpfte Listen ein (React behandelt Updates, indem es sie in eine Warteschlange stellt, die eine verknüpfte Liste priorisierter Updates ist). Dabei ist es ratsam, eine sehr einfache Anwendung mit Hilfe der Bibliothek zu erstellen. Das erleichtert das Debuggen, weil man sich nicht mit Stack-Traces anderer Bibliotheken herumschlagen muss.

Wenn ich keine eingehende Überprüfung durchführe, öffne ich den Ordner /node_modules in einem Projekt, an dem ich arbeite, oder gehe zum GitHub-Repository. Dies geschieht normalerweise, wenn ich auf einen Fehler oder eine interessante Funktion stoße. Stellen Sie beim Lesen von Code auf GitHub sicher, dass Sie von der neuesten Version lesen. Sie können den Code von Commits mit dem Tag der neuesten Version anzeigen, indem Sie auf die Schaltfläche klicken, die zum Ändern von Zweigen verwendet wird, und „Tags“ auswählen. Bibliotheken und Frameworks unterliegen ständig Änderungen, sodass Sie nichts über etwas erfahren möchten, das in der nächsten Version möglicherweise wegfällt.

Eine andere, weniger umständliche Art, Quellcode zu lesen, ist das, was ich gerne die Methode des „flüchtigen Blicks“ nenne. Als ich früh anfing, Code zu lesen, installierte ich express.js , öffnete den Ordner /node_modules und ging seine Abhängigkeiten durch. Wenn mir die README keine zufriedenstellende Erklärung lieferte, las ich die Quelle. Dies führte mich zu diesen interessanten Erkenntnissen:

  • Express hängt von zwei Modulen ab, die beide Objekte zusammenführen, dies jedoch auf sehr unterschiedliche Weise tun. merge-descriptors fügt nur direkt auf dem Quellobjekt gefundene Eigenschaften hinzu und führt auch nicht aufzählbare Eigenschaften zusammen, während utils-merge nur über die aufzählbaren Eigenschaften eines Objekts sowie die in seiner Prototypkette gefundenen Eigenschaften iteriert. merge-descriptors verwendet Object.getOwnPropertyNames() und Object.getOwnPropertyDescriptor() während utils-merge for..in verwendet;
  • Das Modul setprototypeof bietet eine plattformübergreifende Möglichkeit, den Prototyp eines instanziierten Objekts zu setzen;
  • escape-html ist ein 78-Zeilen-Modul zum Escapezeichen einer Inhaltszeichenfolge, damit sie in HTML-Inhalt interpoliert werden kann.

Obwohl die Ergebnisse wahrscheinlich nicht sofort nützlich sind, ist es hilfreich, ein allgemeines Verständnis der Abhängigkeiten zu haben, die von Ihrer Bibliothek oder Ihrem Framework verwendet werden.

Wenn es um das Debuggen von Front-End-Code geht, sind die Debugging-Tools Ihres Browsers Ihr bester Freund. Unter anderem erlauben sie Ihnen, das Programm jederzeit zu stoppen und seinen Status zu überprüfen, die Ausführung einer Funktion zu überspringen oder in sie hinein- oder herauszuspringen. Manchmal ist dies nicht sofort möglich, da der Code minimiert wurde. Ich neige dazu, es zu entminifizieren und den nicht minimierten Code in die entsprechende Datei im Ordner /node_modules zu kopieren.

Der Quellcode für die ReactDOM.render-Funktion
Gehen Sie beim Debuggen wie bei jeder anderen Anwendung vor. Bilde eine Hypothese und teste sie dann. (Große Vorschau)

Fallstudie: Connect-Funktion von Redux

React-Redux ist eine Bibliothek zur Verwaltung des Zustands von React-Anwendungen. Wenn ich mich mit populären Bibliotheken wie diesen befasse, suche ich zunächst nach Artikeln, die über ihre Implementierung geschrieben wurden. Dabei bin ich für diese Fallstudie auf diesen Artikel gestoßen. Dies ist eine weitere gute Sache beim Lesen von Quellcode. Die Recherchephase führt Sie normalerweise zu informativen Artikeln wie diesem, die nur Ihr eigenes Denken und Verstehen verbessern.

connect ist eine React-Redux-Funktion, die React-Komponenten mit dem Redux-Speicher einer Anwendung verbindet. Wie? Nun, laut den Dokumenten macht es Folgendes:

„... gibt eine neue, verbundene Komponentenklasse zurück, die die übergebene Komponente umschließt.“

Nachdem ich das gelesen habe, würde ich folgende Fragen stellen:

  • Kenne ich irgendwelche Muster oder Konzepte, in denen Funktionen eine Eingabe annehmen und dann dieselbe Eingabe zurückgeben, die mit zusätzlicher Funktionalität verpackt ist?
  • Wenn ich solche Muster kenne, wie würde ich dies basierend auf der Erklärung in den Dokumenten implementieren?

Normalerweise besteht der nächste Schritt darin, eine sehr einfache Beispiel-App zu erstellen, die connect verwendet. Bei dieser Gelegenheit habe ich mich jedoch für die neue React-App entschieden, die wir bei Limejump entwickeln, weil ich connect im Kontext einer Anwendung verstehen wollte, die schließlich in eine Produktionsumgebung gehen wird.

Die Komponente, auf die ich mich konzentriere, sieht so aus:

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

Es ist eine Containerkomponente, die vier kleinere verbundene Komponenten umhüllt. Eines der ersten Dinge, auf die Sie in der Datei stoßen, die die Verbindungsmethode exportiert, ist dieser Kommentar: connect ist eine Fassade über connectAdvanced . Ohne weit zu gehen, haben wir unseren ersten Lernmoment: eine Gelegenheit, das Fassadendesignmuster in Aktion zu beobachten . Am Ende der Datei sehen wir, dass connect einen Aufruf einer Funktion namens createConnect . Seine Parameter sind eine Reihe von Standardwerten, die wie folgt destrukturiert wurden:

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

Wieder stoßen wir auf einen weiteren Lernmoment: Exportieren aufgerufener Funktionen und Destrukturieren von Standardfunktionsargumenten . Der Destrukturierungsteil ist ein Lernmoment, denn wäre der Code so geschrieben worden:

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

Dies hätte zu diesem Fehler Uncaught TypeError: Cannot destructure property 'connectHOC' of 'undefined' or 'null'. Dies liegt daran, dass die Funktion kein Standardargument hat, auf das sie zurückgreifen kann.

Hinweis : Weitere Informationen hierzu finden Sie im Artikel von David Walsh. Einige Lernmomente können je nach Ihren Sprachkenntnissen trivial erscheinen, und daher ist es möglicherweise besser, sich auf Dinge zu konzentrieren, die Sie zuvor noch nicht gesehen haben oder über die Sie mehr lernen müssen.

createConnect selbst tut nichts in seinem Funktionsrumpf. Es gibt eine Funktion namens connect zurück, die ich hier verwendet habe:

 export default connect(null, mapDispatchToProps)(MarketContainer)

Es benötigt vier Argumente, alle optional, und die ersten drei Argumente durchlaufen jeweils eine match , die dabei hilft, ihr Verhalten zu definieren, je nachdem, ob die Argumente vorhanden sind, und ihren Werttyp. Da nun das zweite für den match bereitgestellte Argument eine von drei in connect importierten Funktionen ist, muss ich mich entscheiden, welchem ​​Thread ich folgen soll.

Es gibt Lernmomente mit der Proxy-Funktion, die verwendet wird, um das erste zu connect Argument zu umschließen, wenn diese Argumente Funktionen sind, das Dienstprogramm isPlainObject , das verwendet wird, um nach einfachen Objekten zu suchen, oder das warning , das zeigt, wie Sie Ihren Debugger so einstellen können, dass er bei allen Ausnahmen abbricht. Nach den Match-Funktionen kommen wir zu connectHOC , der Funktion, die unsere React-Komponente nimmt und sie mit Redux verbindet. Es ist ein weiterer Funktionsaufruf, der wrapWithConnect , die Funktion, die tatsächlich die Verbindung der Komponente mit dem Geschäft handhabt.

Wenn ich mir die Implementierung von connectHOC , kann ich verstehen, warum es connect braucht, um seine Implementierungsdetails zu verbergen. Es ist das Herzstück von React-Redux und enthält Logik, die nicht über connect offengelegt werden muss. Auch wenn ich den tiefen Tauchgang hier beenden werde, wäre dies der perfekte Zeitpunkt gewesen, um das Referenzmaterial zu konsultieren, das ich zuvor gefunden habe, da es eine unglaublich detaillierte Erklärung der Codebasis enthält, wenn ich fortgefahren wäre.

Zusammenfassung

Das Lesen des Quellcodes ist anfangs schwierig, aber wie bei allem wird es mit der Zeit einfacher. Das Ziel ist nicht, alles zu verstehen, sondern mit einer anderen Perspektive und neuem Wissen davonzukommen. Der Schlüssel ist, über den gesamten Prozess bewusst nachzudenken und auf alles sehr neugierig zu sein.

Zum Beispiel fand ich die Funktion isPlainObject interessant, weil sie dies verwendet, if (typeof obj !== 'object' || obj === null) return false , um sicherzustellen, dass das angegebene Argument ein einfaches Objekt ist. Als ich seine Implementierung zum ersten Mal las, fragte ich mich, warum es nicht Object.prototype.toString.call(opts) !== '[object Object]' verwendete, was weniger Code ist und zwischen Objekten und Objektuntertypen wie Date unterscheidet Objekt. Das Lesen der nächsten Zeile hat jedoch gezeigt, dass in dem äußerst unwahrscheinlichen Fall, dass ein Entwickler, der connect verwendet, beispielsweise ein Date-Objekt zurückgibt, dies von der Object.getPrototypeOf(obj) === null Prüfung behandelt wird.

Eine weitere Faszination in isPlainObject ist dieser Code:

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

Einige Google-Suchen führten mich zu diesem StackOverflow-Thread und dem Redux-Problem, in dem erklärt wird, wie dieser Code mit Fällen umgeht, wie z. B. der Überprüfung auf Objekte, die aus einem iFrame stammen.

Nützliche Links zum Lesen von Quellcode

  • „Wie man Frameworks umkehrt“, Max Koretskyi, Medium
  • „Wie man Code liest“, Aria Stewart, GitHub