Le principal argument de vente de NodeJS - le mécanisme d'événements et les E/S asynchrones, ne sont pas transparents pour les développeurs. Les développeurs doivent écrire du code de manière asynchrone pour tirer parti de cet argument de vente, qui a été critiqué par certains opposants à NodeJS. Quoi qu’il en soit, la programmation asynchrone est effectivement la plus grande fonctionnalité de NodeJS. Sans maîtriser la programmation asynchrone, vous ne pouvez pas dire que vous avez vraiment appris NodeJS. Ce chapitre présentera diverses connaissances liées à la programmation asynchrone.
Dans le code, la manifestation directe de la programmation asynchrone sont les rappels. La programmation asynchrone repose sur des rappels, mais on ne peut pas dire que le programme devient asynchrone après avoir utilisé des rappels. Nous pouvons d’abord regarder le code suivant.
function heavyCompute(n, callback) { var count = 0, i, j; for (i = n; i > 0; --i) { for (j = n; j > 0; --j) { count += 1; } } callback(count); } heavyCompute(10000, function (count) { console.log(count); }); console.log('hello');
100000000 hello
Comme vous pouvez le voir, la fonction de rappel dans le code ci-dessus est toujours exécutée avant le code suivant. JS lui-même s'exécute dans un seul thread, et il est impossible d'exécuter un autre code avant la fin de l'exécution d'un morceau de code, il n'y a donc pas de concept d'exécution asynchrone.
Cependant, si une fonction crée un autre thread ou processus, fait quelque chose en parallèle avec le thread principal JS et avertit le thread principal JS lorsque la chose est terminée, alors la situation est différente. Jetons un coup d'œil au code suivant.
setTimeout(function () { console.log('world'); }, 1000); console.log('hello');
hello world
Cette fois, vous pouvez voir que la fonction de rappel est exécutée après le code suivant. Comme mentionné ci-dessus, JS lui-même est monothread et ne peut pas être exécuté de manière asynchrone. Par conséquent, nous pouvons penser que les fonctions spéciales fournies par l'environnement d'exécution en dehors des spécifications JS telles que setTimeout sont de créer un thread parallèle et de revenir immédiatement, permettant ainsi. Maître JS vers Le processus peut ensuite exécuter le code suivant et exécuter la fonction de rappel après avoir reçu une notification du processus parallèle. En plus des fonctions courantes telles que setTimeout et setInterval, ces fonctions incluent également des API asynchrones fournies par NodeJS telles que fs.readFile.
De plus, nous revenons toujours au fait que JS s'exécute dans un seul thread, ce qui détermine que JS ne peut pas exécuter d'autres codes, y compris des fonctions de rappel, avant d'exécuter un morceau de code. En d'autres termes, même si le thread parallèle termine son travail et demande au thread principal JS d'exécuter la fonction de rappel, la fonction de rappel ne démarrera pas l'exécution tant que le thread principal JS ne sera pas inactif. Ce qui suit est un tel exemple.
function heavyCompute(n) { var count = 0, i, j; for (i = n; i > 0; --i) { for (j = n; j > 0; --j) { count += 1; } } } var t = new Date(); setTimeout(function () { console.log(new Date() - t); }, 1000); heavyCompute(50000);
8520
Comme vous pouvez le constater, le temps d'exécution réel de la fonction de rappel qui était censée être appelée après 1 seconde a été considérablement retardé car le thread principal JS était occupé à exécuter un autre code.
Modèles de conception de code
La programmation asynchrone comporte de nombreux modèles de conception de code uniques. Afin d'obtenir la même fonction, le code écrit en mode synchrone et en mode asynchrone sera très différent. Certains modèles courants sont présentés ci-dessous.
Valeur de retour de la fonction
Il est très courant d'utiliser la sortie d'une fonction comme entrée d'une autre fonction. En mode synchrone, le code s'écrit généralement comme suit :
var output = fn1(fn2('input')); // Do something.
En mode asynchrone, puisque le résultat de l'exécution de la fonction n'est pas transmis via la valeur de retour, mais via la fonction de rappel, le code est généralement écrit de la manière suivante :
fn2('input', function (output2) { fn1(output2, function (output1) { // Do something. }); });
Comme vous pouvez le voir, cette méthode est une fonction de rappel imbriquée dans une seule fonction de rappel. S'il y en a trop, il est facile d'écrire du code en forme de >.
Parcourir le tableau
Lors du parcours d'un tableau, il est également courant d'utiliser une fonction pour effectuer un traitement sur les données membres en séquence. Si la fonction est exécutée de manière synchrone, le code suivant sera généralement écrit :
var len = arr.length, i = 0; for (; i < len; ++i) { arr[i] = sync(arr[i]); } // All array items have processed.
Si la fonction est exécutée de manière asynchrone, le code ci-dessus ne peut pas garantir que tous les membres du tableau ont été traités après la fin de la boucle. Si les membres du tableau doivent être traités en série les uns après les autres, le code asynchrone est généralement écrit comme suit :
(function next(i, len, callback) { if (i < len) { async(arr[i], function (value) { arr[i] = value; next(i + 1, len, callback); }); } else { callback(); } }(0, arr.length, function () { // All array items have processed. }));
Comme vous pouvez le voir, le code ci-dessus ne transmet que le membre suivant du tableau et démarre le prochain cycle d'exécution après que la fonction asynchrone soit exécutée une fois et renvoie le résultat de l'exécution jusqu'à ce que tous les membres du tableau soient traités, l'exécution du code suivant. est déclenché par des rappels.
Si les membres du tableau peuvent être traités en parallèle, mais que le code ultérieur nécessite toujours que tous les membres du tableau soient traités avant de pouvoir être exécutés, le code asynchrone sera ajusté sous la forme suivante :
(function (i, len, count, callback) { for (; i < len; ++i) { (function (i) { async(arr[i], function (value) { arr[i] = value; if (++count === len) { callback(); } }); }(i)); } }(0, arr.length, 0, function () { // All array items have processed. }));
Comme vous pouvez le voir, par rapport à la version de parcours série asynchrone, le code ci-dessus traite tous les membres du tableau en parallèle et utilise la variable counter pour déterminer quand tous les membres du tableau ont été traités.
Gestion des exceptions
Le mécanisme de capture et de gestion des exceptions fourni par JS lui-même - try..catch.., ne peut être utilisé que pour du code exécuté de manière synchrone. Ci-dessous un exemple.
function sync(fn) { return fn(); } try { sync(null); // Do something. } catch (err) { console.log('Error: %s', err.message); }
Error: object is not a function
Comme vous pouvez le voir, l'exception bouillonnera le long du chemin d'exécution du code jusqu'à ce qu'elle soit interceptée lorsqu'elle rencontrera la première instruction try. Cependant, étant donné que les fonctions asynchrones interrompent le chemin d'exécution du code, lorsque les exceptions générées pendant et après l'exécution de la fonction asynchrone remontent jusqu'à l'emplacement où le chemin d'exécution est interrompu, si aucune instruction try n'est rencontrée, elles seront levées comme une exception globale. . Ci-dessous un exemple.
function async(fn, callback) { // Code execution path breaks here. setTimeout(function () { callback(fn()); }, 0); } try { async(null, function (data) { // Do something. }); } catch (err) { console.log('Error: %s', err.message); }
/home/user/test.js:4 callback(fn()); ^ TypeError: object is not a function at null._onTimeout (/home/user/test.js:4:13) at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)
因为代码执行路径被打断了,我们就需要在异常冒泡到断点之前用 try 语句把异常捕获住,并通过回调函数传递被捕获的异常。于是我们可以像下边这样改造上边的例子。
function async(fn, callback) { // Code execution path breaks here. setTimeout(function () { try { callback(null, fn()); } catch (err) { callback(err); } }, 0); } async(null, function (err, data) { if (err) { console.log('Error: %s', err.message); } else { // Do something. } });
Error: object is not a function
可以看到,异常再次被捕获住了。在 NodeJS 中,几乎所有异步 API 都按照以上方式设计,回调函数中第一个参数都是 err。因此我们在编写自己的异步函数时,也可以按照这种方式来处理异常,与 NodeJS 的设计风格保持一致。
有了异常处理方式后,我们接着可以想一想一般我们是怎么写代码的。基本上,我们的代码都是做一些事情,然后调用一个函数,然后再做一些事情,然后再调用一个函数,如此循环。如果我们写的是同步代码,只需要在代码入口点写一个 try 语句就能捕获所有冒泡上来的异常,示例如下。
function main() { // Do something. syncA(); // Do something. syncB(); // Do something. syncC(); } try { main(); } catch (err) { // Deal with exception. }
但是,如果我们写的是异步代码,就只有呵呵了。由于每次异步函数调用都会打断代码执行路径,只能通过回调函数来传递异常,于是我们就需要在每个回调函数里判断是否有异常发生,于是只用三次异步函数调用,就会产生下边这种代码。
function main(callback) { // Do something. asyncA(function (err, data) { if (err) { callback(err); } else { // Do something asyncB(function (err, data) { if (err) { callback(err); } else { // Do something asyncC(function (err, data) { if (err) { callback(err); } else { // Do something callback(null); } }); } }); } }); } main(function (err) { if (err) { // Deal with exception. } });
可以看到,回调函数已经让代码变得复杂了,而异步方式下对异常的处理更加剧了代码的复杂度。