Améliorez vos connaissances JavaScript en lisant le code source
Publié: 2022-03-10Vous souvenez-vous de la première fois où vous avez fouillé profondément dans le code source d'une bibliothèque ou d'un framework que vous utilisez fréquemment ? Pour moi, ce moment est venu lors de mon premier emploi en tant que développeur frontend il y a trois ans.
Nous venions de terminer la réécriture d'un cadre hérité interne que nous utilisions pour créer des cours d'apprentissage en ligne. Au début de la réécriture, nous avions passé du temps à étudier un certain nombre de solutions différentes, notamment Mithril, Inferno, Angular, React, Aurelia, Vue et Polymer. Comme j'étais très débutant (je venais de passer du journalisme au développement web), je me souviens m'être senti intimidé par la complexité de chaque framework et ne pas comprendre comment chacun fonctionnait.
Ma compréhension a grandi lorsque j'ai commencé à étudier plus en profondeur notre cadre choisi, Mithril. Depuis lors, ma connaissance de JavaScript - et de la programmation en général - a été grandement aidée par les heures que j'ai passées à creuser profondément dans les entrailles des bibliothèques que j'utilise quotidiennement, que ce soit au travail ou dans mes propres projets. Dans cet article, je vais partager certaines des façons dont vous pouvez utiliser votre bibliothèque ou votre framework préféré et l'utiliser comme outil pédagogique.
Les avantages de la lecture du code source
L'un des principaux avantages de la lecture du code source est le nombre de choses que vous pouvez apprendre. Lorsque j'ai examiné pour la première fois la base de code de Mithril, j'avais une vague idée de ce qu'était le DOM virtuel. Lorsque j'ai terminé, je suis reparti avec la connaissance que le DOM virtuel est une technique qui consiste à créer une arborescence d'objets décrivant à quoi devrait ressembler votre interface utilisateur. Cet arbre est ensuite transformé en éléments DOM à l'aide d'API DOM telles que document.createElement
. Les mises à jour sont effectuées en créant une nouvelle arborescence décrivant l'état futur de l'interface utilisateur, puis en la comparant aux objets de l'ancienne arborescence.
J'avais lu tout cela dans divers articles et tutoriels, et bien que cela ait été utile, pouvoir l'observer au travail dans le contexte d'une application que nous avions livrée était très éclairant pour moi. Cela m'a également appris quelles questions poser lors de la comparaison de différents cadres. Au lieu de regarder les stars de GitHub, par exemple, je savais maintenant poser des questions telles que "Comment la façon dont chaque framework effectue les mises à jour affecte-t-elle les performances et l'expérience utilisateur ?"
Un autre avantage est une augmentation de votre appréciation et de votre compréhension d'une bonne architecture d'application. Alors que la plupart des projets open source suivent généralement la même structure avec leurs référentiels, chacun d'eux contient des différences. La structure de Mithril est assez plate et si vous connaissez son API, vous pouvez faire des suppositions éclairées sur le code dans des dossiers tels que render
, router
et request
. D'autre part, la structure de React reflète sa nouvelle architecture. Les responsables ont séparé le module responsable des mises à jour de l'interface utilisateur ( react-reconciler
) du module responsable du rendu des éléments DOM ( react-dom
).
L'un des avantages de cela est qu'il est désormais plus facile pour les développeurs d'écrire leurs propres moteurs de rendu personnalisés en se connectant au package react-reconciler
. Parcel, un groupeur de modules que j'ai étudié récemment, a également un dossier de packages
comme React. Le module clé est nommé parcel-bundler
et contient le code responsable de la création des bundles, de la mise en marche du serveur de modules chauds et de l'outil de ligne de commande.
Un autre avantage - qui m'a été agréablement surpris - est que vous devenez plus à l'aise pour lire la spécification JavaScript officielle qui définit le fonctionnement du langage. La première fois que j'ai lu la spécification, c'était lorsque j'étudiais la différence entre throw Error
et throw new Error
(alerte spoiler - il n'y en a pas). J'ai examiné cela parce que j'ai remarqué que Mithril utilisait throw Error
dans l'implémentation de sa fonction m
et je me suis demandé s'il y avait un avantage à l'utiliser plutôt que throw new Error
. Depuis, j'ai aussi appris que les opérateurs logiques &&
et ||
ne renvoient pas nécessairement des booléens, ont trouvé les règles qui régissent la manière dont l'opérateur d'égalité ==
contraint les valeurs et la raison pour laquelle Object.prototype.toString.call({})
renvoie '[object Object]'
.
Techniques de lecture du code source
Il existe de nombreuses façons d'aborder le code source. J'ai trouvé que le moyen le plus simple de commencer est de sélectionner une méthode dans la bibliothèque de votre choix et de documenter ce qui se passe lorsque vous l'appelez. Ne documentez pas chaque étape, mais essayez d'identifier son déroulement et sa structure globale.
Je l'ai fait récemment avec ReactDOM.render
et j'ai par conséquent beaucoup appris sur React Fiber et certaines des raisons de sa mise en œuvre. Heureusement, comme React est un framework populaire, je suis tombé sur de nombreux articles écrits par d'autres développeurs sur le même problème, ce qui a accéléré le processus.
Cette plongée approfondie m'a également présenté les concepts de planification coopérative, la méthode window.requestIdleCallback
et un exemple concret de listes liées (React gère les mises à jour en les plaçant dans une file d'attente qui est une liste liée de mises à jour prioritaires). Pour ce faire, il est conseillé de créer une application très basique à l'aide de la bibliothèque. Cela facilite le débogage car vous n'avez pas à gérer les traces de pile causées par d'autres bibliothèques.
Si je ne fais pas d'examen approfondi, j'ouvrirai le dossier /node_modules
dans un projet sur lequel je travaille ou j'irai dans le référentiel GitHub. Cela se produit généralement lorsque je rencontre un bogue ou une fonctionnalité intéressante. Lorsque vous lisez du code sur GitHub, assurez-vous de lire à partir de la dernière version. Vous pouvez afficher le code des commits avec la dernière balise de version en cliquant sur le bouton utilisé pour changer de branche et sélectionner "tags". Les bibliothèques et les frameworks subissent en permanence des changements, vous ne voulez donc pas en savoir plus sur quelque chose qui pourrait être abandonné dans la prochaine version.
Une autre façon moins compliquée de lire le code source est ce que j'aime appeler la méthode du "coup d'œil rapide". Dès le début, lorsque j'ai commencé à lire le code, j'ai installé express.js , ouvert son dossier /node_modules
et parcouru ses dépendances. Si le README
ne m'a pas fourni d'explication satisfaisante, je lis la source. Cela m'a conduit à ces découvertes intéressantes:
- Express dépend de deux modules qui fusionnent tous les deux des objets mais le font de manière très différente.
merge-descriptors
n'ajoute que des propriétés trouvées directement sur l'objet source et il fusionne également des propriétés non énumérables tandis queutils-merge
ne fait qu'itérer sur les propriétés énumérables d'un objet ainsi que sur celles trouvées dans sa chaîne de prototypes.merge-descriptors
utiliseObject.getOwnPropertyNames()
etObject.getOwnPropertyDescriptor()
tandis queutils-merge
utilisefor..in
; - Le module
setprototypeof
fournit un moyen multiplateforme de définir le prototype d'un objet instancié ; -
escape-html
est un module de 78 lignes permettant d'échapper une chaîne de contenu afin qu'elle puisse être interpolée dans le contenu HTML.
Bien que les résultats ne soient pas susceptibles d'être utiles immédiatement, il est utile d'avoir une compréhension générale des dépendances utilisées par votre bibliothèque ou votre framework.
Lorsqu'il s'agit de déboguer du code frontal, les outils de débogage de votre navigateur sont votre meilleur ami. Entre autres choses, ils vous permettent d'arrêter le programme à tout moment et d'inspecter son état, de sauter l'exécution d'une fonction ou d'entrer ou de sortir de celle-ci. Parfois, cela ne sera pas immédiatement possible car le code a été minifié. J'ai tendance à le déminifier et à copier le code non minifié dans le fichier correspondant du dossier /node_modules
.
Étude de cas : fonction de connexion de Redux
React-Redux est une bibliothèque utilisée pour gérer l'état des applications React. Lorsque je traite avec des bibliothèques populaires telles que celles-ci, je commence par rechercher des articles qui ont été écrits sur sa mise en œuvre. Ce faisant, pour cette étude de cas, je suis tombé sur cet article. C'est une autre bonne chose à propos de la lecture du code source. La phase de recherche vous mène généralement à des articles informatifs comme celui-ci qui ne font qu'améliorer votre propre réflexion et votre compréhension.
connect
est une fonction React-Redux qui connecte les composants React au magasin Redux d'une application. Comment? Eh bien, selon les docs, il fait ce qui suit:
"... renvoie une nouvelle classe de composants connectés qui encapsule le composant que vous avez transmis."
Après avoir lu ceci, je me poserais les questions suivantes :
- Est-ce que je connais des modèles ou des concepts dans lesquels les fonctions prennent une entrée, puis renvoient cette même entrée enveloppée de fonctionnalités supplémentaires ?
- Si je connais de tels modèles, comment pourrais-je les implémenter sur la base de l'explication donnée dans la documentation ?
Habituellement, l'étape suivante consiste à créer un exemple d'application très basique qui utilise connect
. Cependant, à cette occasion, j'ai choisi d'utiliser la nouvelle application React que nous construisons chez Limejump parce que je voulais comprendre la connect
dans le contexte d'une application qui finira par entrer dans un environnement de production.
Le composant sur lequel je me concentre ressemble à ceci:
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);
Il s'agit d'un composant de conteneur qui encapsule quatre composants connectés plus petits. L'une des premières choses que vous rencontrez dans le fichier qui exporte la méthode connect
est ce commentaire : connect est une façade sur connectAdvanced . Sans aller bien loin, nous avons notre premier moment d'apprentissage : une occasion d'observer le modèle de conception de façade en action . À la fin du fichier, nous voyons que connect
exporte une invocation d'une fonction appelée createConnect
. Ses paramètres sont un tas de valeurs par défaut qui ont été déstructurées comme ceci :
export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory } = {})
Encore une fois, nous rencontrons un autre moment d'apprentissage : exporter les fonctions invoquées et déstructurer les arguments de fonction par défaut . La partie déstructuration est un moment d'apprentissage car si le code avait été écrit comme ceci :
export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory })
Cela aurait entraîné cette erreur Uncaught TypeError: Cannot destructure property 'connectHOC' of 'undefined' or 'null'.
C'est parce que la fonction n'a pas d'argument par défaut sur lequel se rabattre.
Note : Pour en savoir plus, vous pouvez lire l'article de David Walsh. Certains moments d'apprentissage peuvent sembler triviaux, selon votre connaissance de la langue, et il peut donc être préférable de vous concentrer sur des choses que vous n'avez jamais vues auparavant ou sur lesquelles vous devez en savoir plus.
createConnect
lui-même ne fait rien dans son corps de fonction. Elle renvoie une fonction appelée connect
, celle que j'ai utilisée ici :
export default connect(null, mapDispatchToProps)(MarketContainer)
Il prend quatre arguments, tous facultatifs, et les trois premiers arguments passent chacun par une fonction de match
qui aide à définir leur comportement en fonction de la présence ou non des arguments et de leur type de valeur. Maintenant, comme le deuxième argument fourni pour match
est l'une des trois fonctions importées dans connect
, je dois décider quel thread suivre.
Il y a des moments d'apprentissage avec la fonction proxy utilisée pour envelopper le premier argument à connect
si ces arguments sont des fonctions, l'utilitaire isPlainObject
utilisé pour vérifier les objets simples ou le module warning
qui révèle comment vous pouvez configurer votre débogueur pour qu'il s'arrête sur toutes les exceptions. Après les fonctions de correspondance, nous arrivons à connectHOC
, la fonction qui prend notre composant React et le connecte à Redux. C'est une autre invocation de fonction qui renvoie wrapWithConnect
, la fonction qui gère réellement la connexion du composant au magasin.
En regardant l'implémentation de connectHOC
, je peux comprendre pourquoi il a besoin de connect
pour masquer ses détails d'implémentation. C'est le cœur de React-Redux et contient une logique qui n'a pas besoin d'être exposée via connect
. Même si je terminerai la plongée en profondeur ici, si j'avais continué, cela aurait été le moment idéal pour consulter le matériel de référence que j'ai trouvé plus tôt car il contient une explication incroyablement détaillée de la base de code.
Sommaire
La lecture du code source est difficile au début, mais comme pour tout, cela devient plus facile avec le temps. Le but n'est pas de tout comprendre mais de repartir avec un regard différent et de nouvelles connaissances. La clé est d'être délibéré sur l'ensemble du processus et intensément curieux de tout.
Par exemple, j'ai trouvé la fonction isPlainObject
intéressante car elle utilise this if (typeof obj !== 'object' || obj === null) return false
pour s'assurer que l'argument donné est un objet simple. Quand j'ai lu son implémentation pour la première fois, je me suis demandé pourquoi il n'utilisait pas Object.prototype.toString.call(opts) !== '[object Object]'
, qui est moins de code et fait la distinction entre les objets et les sous-types d'objets tels que la Date objet. Cependant, la lecture de la ligne suivante a révélé que dans le cas extrêmement improbable où un développeur utilisant connect
renvoie un objet Date, par exemple, cela sera géré par la Object.getPrototypeOf(obj) === null
.
Une autre intrigue dans isPlainObject
est ce code :
while (Object.getPrototypeOf(baseProto) !== null) { baseProto = Object.getPrototypeOf(baseProto) }
Certaines recherches sur Google m'ont conduit à ce fil StackOverflow et au problème Redux expliquant comment ce code gère des cas tels que la vérification d'objets provenant d'un iFrame.
Liens utiles sur la lecture du code source
- "Comment inverser les cadres d'ingénierie", Max Koretskyi, Medium
- "Comment lire le code", Aria Stewart, GitHub