Verbessern Sie Ihre JavaScript-Kenntnisse, indem Sie den Quellcode lesen
Veröffentlicht: 2022-03-10Erinnern 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.
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.
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ährendutils-merge
nur über die aufzählbaren Eigenschaften eines Objekts sowie die in seiner Prototypkette gefundenen Eigenschaften iteriert.merge-descriptors
verwendetObject.getOwnPropertyNames()
undObject.getOwnPropertyDescriptor()
währendutils-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.
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