Comment démarrer rapidement avec VUE3.0 : se lancer dans l'apprentissage
En ce qui concerne Node.js, je pense que la plupart des ingénieurs front-end penseront à développer des serveurs basés sur celui-ci. Il suffit de maîtriser JavaScript pour devenir un full-stack. ingénieur, mais en fait, le sens de Node.js Et ce n'est pas tout.
Pour de nombreux langages de haut niveau, les autorisations d'exécution peuvent atteindre le système d'exploitation, mais JavaScript exécuté du côté du navigateur est une exception. L'environnement sandbox créé par le navigateur enferme les ingénieurs front-end dans une tour d'ivoire dans le monde de la programmation. Cependant, l'émergence de Node.js a comblé cette lacune et les ingénieurs front-end peuvent également atteindre le fond du monde informatique.
Par conséquent, l’importance de Nodejs pour les ingénieurs front-end n’est pas seulement de fournir des capacités de développement full-stack, mais, plus important encore, d’ouvrir une porte au monde sous-jacent des ordinateurs pour les ingénieurs front-end. Cet article ouvre cette porte en analysant les principes d'implémentation de Node.js.
Il existe plus d'une douzaine de dépendances dans le répertoire /deps de l'entrepôt de code source de Node.js, y compris des modules écrits en langage C (tels que libuv, V8) et des modules écrits en langage JavaScript (tels qu'acorn , acorn-plugins). Comme indiqué ci-dessous.
Les plus importants d'entre eux sont les modules correspondant aux répertoires v8 et uv. V8 lui-même n'a pas la capacité de s'exécuter de manière asynchrone, mais il est implémenté à l'aide d'autres threads dans le navigateur. C'est pourquoi nous disons souvent que js est monothread, car son moteur d'analyse ne prend en charge que le code d'analyse synchrone. Mais dans Node.js, l'implémentation asynchrone repose principalement sur libuv. Concentrons-nous sur l'analyse du principe d'implémentation de libuv.
libuv est une bibliothèque d'E/S asynchrone écrite en C qui prend en charge plusieurs plates-formes. Elle résout principalement le problème des opérations d'E/S qui provoquent facilement des blocages. Il a été initialement développé spécifiquement pour être utilisé avec Node.js, mais a depuis été utilisé par d'autres modules tels que Luvit, Julia et pyuv. La figure ci-dessous est le diagramme de structure de libuv.
libuv a deux méthodes d'implémentation asynchrones, qui sont les deux parties sélectionnées par la case jaune à gauche et à droite de l'image ci-dessus.
La partie gauche est le module d'E/S réseau, qui a différents mécanismes d'implémentation sous différentes plates-formes. Les systèmes Linux utilisent epoll pour l'implémenter, OSX et d'autres systèmes BSD utilisent KQueue, les systèmes SunOS utilisent les ports d'événement et les systèmes Windows utilisent IOCP. Puisqu’il s’agit de l’API sous-jacente du système d’exploitation, c’est plus compliqué à comprendre, je ne le présenterai donc pas ici.
La partie droite comprend le module d'E/S de fichier, le module DNS et le code utilisateur, qui implémente les opérations asynchrones via le pool de threads. Les E/S de fichiers sont différentes des E/S réseau. Libuv ne s'appuie pas sur l'API sous-jacente du système, mais effectue des opérations d'E/S de fichiers bloquées dans le pool de threads global.
La figure suivante est le diagramme de workflow d'interrogation d'événements donné par le site officiel de libuv. Analysons-le avec le code.
Le code principal de la boucle d'événements libuv est implémenté dans la fonction uv_run(). Ce qui suit fait partie du code principal sous le système Unix. Bien qu’il soit écrit en langage C, il s’agit d’un langage de haut niveau comme JavaScript, il n’est donc pas trop difficile à comprendre. La plus grande différence réside peut-être dans les astérisques et les flèches. Nous pouvons simplement ignorer les astérisques. Par exemple, la boucle uv_loop_t* dans le paramètre de fonction peut être comprise comme une boucle variable de type uv_loop_t. La flèche "→" peut être comprise comme le point ".", par exemple, loop→stop_flag peut être compris comme loop.stop_flag.
int uv_run (boucle uv_loop_t*, mode uv_run_mode) { ... r = uv__loop_alive(boucle); if (!r) uv__update_time(boucle); while (r != 0 && boucle ->stop_flag == 0) { uv__update_time(boucle); uv__run_timers(boucle); ran_ending = uv__run_ending(boucle); uv__run_idle(boucle); uv__run_prepare(loop);...uv__io_poll(loop, timeout); uv__run_check(boucle); uv__run_closing_handles(boucle);... }... }
uv__loop_alive
Cette fonction est utilisée pour déterminer si l'interrogation des événements doit continuer. S'il n'y a aucune tâche active dans l'objet boucle, elle renverra 0 et quittera la boucle.
En langage C, cette « tâche » a un nom professionnel, c’est-à-dire « handle », qui peut être compris comme une variable pointant vers la tâche. Les handles peuvent être divisés en deux catégories : request et handle, qui représentent respectivement les handles à cycle de vie court et les handles à cycle de vie longue. Le code spécifique est le suivant :
static int uv__loop_alive(const uv_loop_t * loop) { return uv__has_active_handles(loop) || uv__has_active_reqs(loop) || boucle - >closing_handles != NULL; }
uv__update_time
Afin de réduire le nombre d'appels système liés à l'heure, cette fonction est utilisée pour mettre en cache l'heure système actuelle. La précision est très élevée et peut atteindre le niveau de la nanoseconde, mais l'unité est toujours la milliseconde.
Le code source spécifique est le suivant :
UV_UNUSED(static void uv__update_time(uv_loop_t * loop)) { boucle ->time = uv__hrtime(UV_CLOCK_FAST) / 1000000; }
uv__run_timers
exécute les fonctions de rappel qui atteignent le seuil de temps dans setTimeout() et setInterval(). Ce processus d'exécution est implémenté via un parcours de boucle for. Comme vous pouvez le voir dans le code ci-dessous, le rappel du minuteur est stocké dans les données d'une structure de tas minimum. Il se termine lorsque le tas minimum est vide ou n'a pas atteint le cycle de seuil. .
Supprimez le minuteur avant d'exécuter la fonction de rappel du minuteur. Si la répétition est définie, elle doit être à nouveau ajoutée au tas minimum, puis le rappel du minuteur est exécuté.
Le code spécifique est le suivant :
void uv__run_timers(uv_loop_t * loop) { struct heap_node * tas_node ; uv_timer_t * poignée ; pour (;;) { tas_node = tas_min(timer_heap(loop)); if (heap_node == NULL) break ; handle = conteneur_of(heap_node, uv_timer_t, heap_node); if (poignée -> timeout > boucle -> time) break; uv_timer_stop(poignée); uv_timer_again(poignée); poignée ->timer_cb(poignée); } }
uv__run_ending
parcourt toutes les fonctions de rappel d'E/S stockées dans ending_queue et renvoie 0 lorsque ending_queue est vide ; sinon, renvoie 1 après l'exécution de la fonction de rappel dans ending_queue.
Le code est le suivant :
static int uv__run_ending(uv_loop_t * loop) { FILE D'ATTENTE * q ; FILE d'attente pq ; uv__io_t * w; if (QUEUE_EMPTY( & loop -> ending_queue)) renvoie 0 ; QUEUE_MOVE( & boucle - >attente_queue, &pq); tandis que (!QUEUE_EMPTY( & pq)) { q = QUEUE_HEAD( &pq); QUEUE_REMOVE(q); QUEUE_INIT(q); w = QUEUE_DATA(q, uv__io_t, ending_queue); w ->cb(boucle, w, POLLOUT); } renvoyer 1 ; }Les trois fonctions
uvrun_idle / uvrun_prepare / uv__run_check
sont toutes définies via une fonction macro UV_LOOP_WATCHER_DEFINE. La fonction macro peut être comprise comme un modèle de code ou une fonction utilisée pour définir des fonctions. La fonction macro est appelée trois fois et les valeurs des paramètres de nom Prepare, Check et Idle sont respectivement transmises. En même temps, trois fonctions, uvrun_idle, uvrun_prepare et uv__run_check, sont définies.
Par conséquent, leur logique d'exécution est cohérente. Ils parcourent et retirent tous les objets de la file d'attente loop->name##_handles selon le principe du premier entré, premier sorti, puis exécutent la fonction de rappel correspondante.
#define UV_LOOP_WATCHER_DEFINE(nom, type) void uv__run_##name(uv_loop_t* boucle) { uv_##nom##_t* h; File d'attente ; FILE D'ATTENTE* q ; QUEUE_MOVE(&loop->name##_handles, &queue); while (!QUEUE_EMPTY(&file d'attente)) { q = QUEUE_HEAD(&file d'attente); h = QUEUE_DATA(q, uv_##name##_t, file d'attente); QUEUE_REMOVE(q); QUEUE_INSERT_TAIL(&loop->name##_handles, q); h->nom##_cb(h); } } UV_LOOP_WATCHER_DEFINE(préparer, PRÉPARER) UV_LOOP_WATCHER_DEFINE(vérifier, CHECK) UV_LOOP_WATCHER_DEFINE(idle, IDLE)
uv__io_poll
uv__io_poll est principalement utilisé pour interroger les opérations d'E/S. L'implémentation spécifique variera en fonction du système d'exploitation. Nous prenons le système Linux comme exemple pour l'analyse.
La fonction uv__io_poll a beaucoup de code source. Le noyau est constitué de deux morceaux de code de boucle. Une partie du code est la suivante :
void uv__io_poll(uv_loop_t * loop, int timeout) { while (!QUEUE_EMPTY( & boucle ->watcher_queue)) { q = QUEUE_HEAD( & boucle ->watcher_queue); QUEUE_REMOVE(q); QUEUE_INIT(q); w = QUEUE_DATA(q, uv__io_t, watcher_queue); e.events = w ->pevents; e.data.fd = w ->fd; si (w -> événements == 0) op = EPOLL_CTL_ADD ; sinon op = EPOLL_CTL_MOD ; if (epoll_ctl(loop ->backend_fd, op, w ->fd, &e)) { if (errno != EEXIST) abort(); if (epoll_ctl(loop - >backend_fd, EPOLL_CTL_MOD, w ->fd, &e)) abort(); } w ->événements = w ->peévénements; } pour (;;) { pour (i = 0; i < nfds; i++) { pe = événements + i ; fd = pe ->data.fd; w = boucle ->observateurs[fd]; pe - >événements &= w ->pevents | if (pe - >events == POLLERR || pe - >events == POLLHUP) pe - >events |= w ->pevents & (POLLIN | POLLOUT | UV__POLLRDHUP | UV__POLLPRI); if (pe ->événements != 0) { si (w == &loop ->signal_io_watcher) have_signals = 1; sinon w ->cb(boucle, w, pe ->événements); événements++; } } if (have_signals != 0) loop - >signal_io_watcher.cb(loop, &loop - >signal_io_watcher, POLLIN); }... }
Dans la boucle while, parcourez la file d'attente des observateurs watcher_queue, supprimez l'événement et le descripteur de fichier et affectez-les à l'objet d'événement e, puis appelez la fonction epoll_ctl pour enregistrer ou modifier l'événement epoll.
Dans la boucle for, le descripteur de fichier en attente dans epoll sera retiré et attribué à nfds, puis nfds sera parcouru pour exécuter la fonction de rappel.
uv__run_closing_handles
parcourt la file d'attente en attente de fermeture, ferme les handles tels que stream, tcp, udp, etc., puis appelle le close_cb correspondant au handle. Le code est le suivant :
static void uv__run_closing_handles(uv_loop_t * loop) { uv_handle_t *p; uv_handle_t *q; p = boucle ->closing_handles; boucle ->closing_handles = NULL; tandis que (p) { q = p ->next_closing; uv__finish_close(p); p = q; } }
Bien que process.nextTick et Promise soient tous deux des API asynchrones, ils ne font pas partie de l'interrogation d'événements. Ils disposent de leurs propres files d'attente de tâches, qui sont exécutées une fois chaque étape de l'interrogation d'événements terminée. Ainsi, lorsque nous utilisons ces deux API asynchrones, nous devons faire attention. Si de longues tâches ou récursions sont effectuées dans la fonction de rappel entrant, l'interrogation des événements sera bloquée, « affamant » ainsi les opérations d'E/S.
Le code suivant est un exemple dans lequel la fonction de rappel de fs.readFile ne peut pas être exécutée en appelant de manière récursive prcoess.nextTick.
fs.readFile('config.json', (err, data) = >{... }) const traversée = () = >{ process.nextTick (traverse) }
Pour résoudre ce problème, vous pouvez utiliser setImmediate à la place, car setImmediate exécutera la file d'attente des fonctions de rappel dans la boucle d'événements. La file d'attente des tâches process.nextTick a une priorité plus élevée que la file d'attente des tâches Promise. Pour les raisons spécifiques, veuillez vous référer au code suivant :
function processTicksAndRejections() {. laisser couler ; faire { while (tock = queue.shift()) { const asyncId = tock[async_id_symbol]; submitBefore(asyncId, tock[trigger_async_id_symbol], tock); essayer { const callback = tock.callback; if (tock.args === non défini) { rappel(); } autre { const args = tock.args; commutateur (longueur des arguments) { cas 1 : rappel(args[0]); casser; cas 2 : rappel(args[0], args[1]); casser; cas 3 : rappel(args[0], args[1], args[2]); casser; cas 4 : rappel(args[0], args[1], args[2], args[3]); casser; défaut: rappel(...arguments); } } } enfin { if (destroyHooksExist()) émetDestroy(asyncId); } émettreAprès(asyncId); } runMicrotasks(); } while (! queue . isEmpty () || processPromiseRejections()); setHasTickScheduled(faux); setHasRejectionToWarn(false); }
Comme le montre la fonction processTicksAndRejections(), la fonction de rappel de la file d'attente est d'abord supprimée via la boucle while, et la fonction de rappel dans la file d'attente est ajoutée via process.nextTick. Lorsque la boucle while se termine, la fonction runMicrotasks() est appelée pour exécuter la fonction de rappel Promise.
la structure de base de Node.js qui s'appuie sur libuv peut être divisée en deux parties. Une partie est les E/S réseau. L'implémentation sous-jacente s'appuiera sur différentes API système selon les différents systèmes d'exploitation. /O, DNS et code utilisateur Cette partie utilise le pool de threads pour le traitement.
Le mécanisme de base de libuv pour gérer les opérations asynchrones est l'interrogation d'événements. L'interrogation d'événements est divisée en plusieurs étapes. L'opération générale consiste à parcourir et à exécuter la fonction de rappel dans la file d'attente.
Enfin, il est mentionné que les processus API asynchrones.nextTick et Promise n'appartiennent pas à l'interrogation d'événements. Une utilisation inappropriée entraînera le blocage de l'interrogation d'événements. Une solution consiste à utiliser setImmediate à la place.