Le contenu de cet article concerne le contrôle des processus en js : l'analyse des rappels&Promises&Async/Awai Il a une certaine valeur de référence. Les amis dans le besoin peuvent s'y référer.
JavaScript prétend souvent être _asynchrone_. Qu'est-ce que cela signifie? Comment cela affecte-t-il le développement ? Comment cette approche a-t-elle évolué ces dernières années ?
Considérez le code suivant :
result1 = doSomething1(); result2 = doSomething2(result1);
La plupart des langages gèrent chaque ligne de manière synchrone. La première ligne s'exécute et renvoie le résultat. La deuxième ligne s'exécute après la fin de la première ligne, quel que soit le temps que cela prend.
JavaScript s'exécute sur un seul thread de traitement. Lors de l'exécution dans un onglet du navigateur, tout le reste s'arrête car aucune modification du DOM de la page ne se produit sur les threads parallèles ; il est dangereux de rediriger un thread vers une autre URL pendant qu'un autre thread tente d'ajouter des nœuds enfants.
Cela est évident pour l’utilisateur. Par exemple, JavaScript détecte les clics sur les boutons, exécute des calculs et met à jour le DOM. Une fois terminé, le navigateur est libre de traiter l'élément suivant dans la file d'attente.
(Remarque : d'autres langages comme PHP utilisent également un seul thread, mais peuvent être gérés par un serveur multi-thread comme Apache. Deux requêtes simultanées sur la même page d'exécution PHP peuvent lancer deux exécutions de Threads isolés d'instances. )
asynchrone pose un problème. Que se passe-t-il lorsque JavaScript appelle un processus « lent » (comme une requête Ajax dans le navigateur ou une opération de base de données sur le serveur) ? Cette opération peut prendre plusieurs secondes, voire quelques minutes. Le navigateur est verrouillé en attendant une réponse. Sur le serveur, l'application Node.js ne pourra plus traiter la demande de l'utilisateur.
La solution est le traitement asynchrone. Au lieu d'attendre la fin, un processus est invité à appeler une autre fonction lorsque le résultat est prêt. C'est ce qu'on appelle un rappel et est passé en paramètre à toute fonction asynchrone. Par exemple :
doSomethingAsync(callback1); console.log('finished'); // call when doSomethingAsync completes function callback1(error) { if (!error) console.log('doSomethingAsync complete'); }
doSomethingAsync() accepte une fonction de rappel comme argument (seule une référence à la fonction est passée, donc il n'y a presque pas de surcharge). Peu importe le temps que prend doSomethingAsync() ; tout ce que nous savons, c'est que callback1() s'exécutera à un moment donné dans le futur. La console affichera :
finished doSomethingAsync complete
Généralement, les rappels ne peuvent être appelés que par une fonction asynchrone. Vous pouvez donc utiliser des fonctions en ligne anonymes concises :
doSomethingAsync(error => { if (!error) console.log('doSomethingAsync complete'); });
En imbriquant les fonctions de rappel, une série de deux appels asynchrones ou plus peut être effectuée en série. Par exemple :
async1((err, res) => { if (!err) async2(res, (err, res) => { if (!err) async3(res, (err, res) => { console.log('async1, async2, async3 complete.'); }); }); });
Malheureusement, cela introduit Callback Hell - un concept notoire (http://callbackhell.com/) ! Le code est difficile à lire et s'aggrave lorsqu'une logique de gestion des erreurs est ajoutée.
L'enfer des rappels est relativement rare dans le codage côté client. Si vous effectuez un appel Ajax, mettez à jour le DOM et attendez la fin de l'animation, cela peut aller jusqu'à deux ou trois niveaux, mais cela reste généralement gérable.
La situation est différente pour les systèmes d'exploitation ou les processus serveur. Les appels d'API Node.js peuvent recevoir des téléchargements de fichiers, mettre à jour plusieurs tables de base de données, écrire dans les journaux et effectuer d'autres appels d'API avant d'envoyer une réponse.
ES2015 (ES6) a introduit les promesses. Les rappels peuvent toujours être utilisés, mais les promesses fournissent une syntaxe plus propre pour enchaîner les commandes asynchrones afin qu'elles puissent être exécutées en série (plus d'informations à ce sujet).
Pour activer l'exécution basée sur Promise, les fonctions basées sur le rappel asynchrone doivent être modifiées afin qu'elles renvoient immédiatement un objet Promise. Cet objet de promesse exécute l'une des deux fonctions (passées en arguments) à un moment donné dans le futur :
resolve : une fonction de rappel qui s'exécute lorsque le gestionnaire termine avec succès
reject : fonction de rappel facultative à exécuter en cas d'échec.
Dans l'exemple ci-dessous, l'API de base de données fournit une méthode connect() qui accepte une fonction de rappel. La fonction externe asyncDBconnect() renvoie immédiatement une nouvelle promesse et exécute résoudre() ou rejeter() une fois la connexion établie ou échouée :
const db = require('database'); // connect to database function asyncDBconnect(param) { return new Promise((resolve, reject) => { db.connect(param, (err, connection) => { if (err) reject(err); else resolve(connection); }); }); }
Node.js 8.0+ fournit l'utilitaire util.promisify() Programme qui convertit les fonctions basées sur le rappel en alternatives basées sur la promesse. Il y a plusieurs conditions :
Passer le rappel en dernier paramètre à la fonction asynchrone
La fonction de rappel doit pointer vers une erreur, suivie par un paramètre value .
Exemple :
// Node.js: promisify fs.readFile const util = require('util'), fs = require('fs'), readFileAsync = util.promisify(fs.readFile); readFileAsync('file.txt');
Diverses bibliothèques clientes fournissent également des options de promesse, mais vous pouvez en créer quelques-unes :
// promisify a callback function passed as the last parameter // the callback function must accept (err, data) parameters function promisify(fn) { return function() { return new Promise( (resolve, reject) => fn( ...Array.from(arguments), (err, data) => err ? reject(err) : resolve(data) ) ); } } // example function wait(time, callback) { setTimeout(() => { callback(null, 'done'); }, time); } const asyncWait = promisify(wait); ayscWait(1000);
Tout ce qui renvoie une promesse peut démarrer une série d'appels de fonction asynchrones définis dans la méthode .then(). Chacun reçoit le résultat de la solution précédente :
asyncDBconnect('http://localhost:1234') .then(asyncGetSession) // passed result of asyncDBconnect .then(asyncGetUser) // passed result of asyncGetSession .then(asyncLogAccess) // passed result of asyncGetUser .then(result => { // non-asynchronous function console.log('complete'); // (passed result of asyncLogAccess) return result; // (result passed to next .then()) }) .catch(err => { // called on any reject console.log('error', err); });
Les fonctions synchrones peuvent également être exécutées dans un bloc .then(). La valeur renvoyée sera transmise au prochain .then() le cas échéant. La méthode
.catch() définit une fonction qui est appelée lorsqu'un rejet précédent est déclenché. À ce stade, la méthode .then() ne sera plus exécutée. Vous pouvez utiliser plusieurs méthodes .catch() tout au long de la chaîne pour détecter différentes erreurs.
ES2018 a introduit une méthode .finally() pour exécuter n'importe quelle logique finale quel que soit le résultat - par exemple le nettoyage, la fermeture de la connexion à la base de données, etc. Actuellement uniquement pris en charge sur Chrome et Firefox, mais le comité technique 39 a publié le polyfill .finally().
function doSomething() { doSomething1() .then(doSomething2) .then(doSomething3) .catch(err => { console.log(err); }) .finally(() => { // tidy-up here! }); }
Promise .then()方法一个接一个地运行异步函数。如果顺序无关紧要 - 例如,初始化不相关的组件 - 同时启动所有异步函数并在最后(最慢)函数运行解析时结束更快。
这可以通过Promise.all()来实现。它接受一组函数并返回另一个Promise。例如:
Promise.all([ async1, async2, async3 ]) .then(values => { // array of resolved values console.log(values); // (in same order as function array) return values; }) .catch(err => { // called on any reject console.log('error', err); });
如果任何一个异步函数调用失败,则Promise.all()立即终止。
Promise.race()与Promise.all()类似,只是它会在first Promise解析或拒绝后立即解析或拒绝。只有最快的基于Promise的异步函数才能完成:
Promise.race([ async1, async2, async3 ]) .then(value => { // single value console.log(value); return value; }) .catch(err => { // called on any reject console.log('error', err); });
Promises 减少了回调地狱但引入了别的问题。
教程经常没有提到_整个Promise链是异步的。使用一系列promise的任何函数都应返回自己的Promise或在最终的.then(),. catch()或.finally()方法中运行回调函数。
学习基础知识至关重要。
Promises 可能令人生畏,因此ES2017引入了async and await。 虽然它可能只是语法糖,它使Promise更完善,你可以完全避免.then()链。 考虑下面的基于Promise的示例:
function connect() { return new Promise((resolve, reject) => { asyncDBconnect('http://localhost:1234') .then(asyncGetSession) .then(asyncGetUser) .then(asyncLogAccess) .then(result => resolve(result)) .catch(err => reject(err)) }); } // run connect (self-executing function) (() => { connect(); .then(result => console.log(result)) .catch(err => console.log(err)) })();
用这个重写一下async/await:
外部函数必须以async语句开头
对异步的基于Promise的函数的调用必须在await之前,以确保在下一个命令执行之前完成处理。
async function connect() { try { const connection = await asyncDBconnect('http://localhost:1234'), session = await asyncGetSession(connection), user = await asyncGetUser(session), log = await asyncLogAccess(user); return log; } catch (e) { console.log('error', err); return null; } } // run connect (self-executing async function) (async () => { await connect(); })();
await有效地使每个调用看起来好像是同步的,而不是阻止JavaScript的单个处理线程。 此外,异步函数总是返回一个Promise,因此它们可以被其他异步函数调用。
async/await 代码可能不会更短,但有相当大的好处:
1、语法更清晰。括号更少,错误更少。
2、调试更容易。可以在任何await语句上设置断点。
3、错误处理更好。try / catch块可以与同步代码一样使用。
4、支持很好。它在所有浏览器(IE和Opera Mini除外)和Node 7.6+中都得到了支持。
但是并非所有都是完美的......
async / await仍然依赖于Promises,它最终依赖于回调。你需要了解Promises是如何工作的,并且没有Promise.all()和Promise.race()的直接等价物。并且不要忘记Promise.all(),它比使用一系列不相关的await命令更有效。
在某些时候,您将尝试调用异步函数中的同步循环。例如:
async function process(array) { for (let i of array) { await doSomething(i); } }
它不会起作用。这也不会:
async function process(array) { array.forEach(async i => { await doSomething(i); }); }
循环本身保持同步,并且总是在它们的内部异步操作之前完成。
ES2018引入了异步迭代器,它与常规迭代器一样,但next()方法返回Promise。因此,await关键字可以与for循环一起用于串行运行异步操作。例如:
async function process(array) { for await (let i of array) { doSomething(i); } }
但是,在实现异步迭代器之前,最好将数组项映射到异步函数并使用Promise.all()运行它们。例如:
const todo = ['a', 'b', 'c'], alltodo = todo.map(async (v, i) => { console.log('iteration', i); await processSomething(v); }); await Promise.all(alltodo);
这具有并行运行任务的好处,但是不可能将一次迭代的结果传递给另一次迭代,并且映射大型数组可能在性能消耗上是很昂贵。
如果省略任何await失败的try / catch,async函数将以静默方式退出。如果您有一组很长的异步await命令,则可能需要多个try / catch块。
一种替代方案是高阶函数,它捕获错误,因此try / catch块变得不必要(thanks to @wesbos for the suggestion):
async function connect() { const connection = await asyncDBconnect('http://localhost:1234'), session = await asyncGetSession(connection), user = await asyncGetUser(session), log = await asyncLogAccess(user); return true; } // higher-order function to catch errors function catchErrors(fn) { return function (...args) { return fn(...args).catch(err => { console.log('ERROR', err); }); } } (async () => { await catchErrors(connect)(); })();
但是,在应用程序必须以与其他错误不同的方式对某些错误做出反应的情况下,此选项可能不实用。
尽管有一些陷阱,async / await是JavaScript的一个优雅补充。
异步编程是一项在JavaScript中无法避免的挑战。回调在大多数应用程序中都是必不可少的,但它很容易陷入深层嵌套的函数中。
Promises 抽象回调,但有许多语法陷阱。 转换现有函数可能是一件苦差事,而.then()链仍然看起来很混乱。
幸运的是,async / await提供了清晰度。代码看起来是同步的,但它不能独占单个处理线程。它将改变你编写JavaScript的方式!
相关推荐:
整理Javascript流程控制语句学习笔记_javascript技巧
JavaScript中使用Callback控制流程介绍_javascript技巧
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!