Heim > Artikel > Web-Frontend > Prozesssteuerung in js: Analyse von Callbacks&Promises&Async/Awai
Der Inhalt dieses Artikels befasst sich mit der Prozesssteuerung in js: Analyse von Callbacks&Promises&Awai. Ich hoffe, dass er für Sie hilfreich ist.
JavaScript behauptet oft, _asynchron_ zu sein. Was bedeutet das? Wie wirkt es sich auf die Entwicklung aus? Wie hat sich dieser Ansatz in den letzten Jahren verändert?
Bedenken Sie den folgenden Code:
result1 = doSomething1(); result2 = doSomething2(result1);
Die meisten Sprachen verarbeiten jede Zeile synchronisiert. Die erste Zeile wird ausgeführt und gibt das Ergebnis zurück. Die zweite Zeile wird ausgeführt, nachdem die erste Zeile abgeschlossen ist, egal wie lange es dauert.
JavaScript wird in einem einzelnen Verarbeitungsthread ausgeführt. Während der Ausführung in einem Browser-Tab stoppt alles andere, da in parallelen Threads keine Änderungen am DOM der Seite vorgenommen werden. Es ist gefährlich, einen Thread zu einer anderen URL umzuleiten, während ein anderer Thread versucht, untergeordnete Knoten anzuhängen.
Dies ist für den Benutzer offensichtlich. JavaScript erkennt beispielsweise Schaltflächenklicks, führt Berechnungen durch und aktualisiert das DOM. Sobald der Vorgang abgeschlossen ist, kann der Browser das nächste Element in der Warteschlange verarbeiten.
(Randbemerkung: Andere Sprachen wie PHP verwenden ebenfalls einen einzelnen Thread, können aber von einem Multithread-Server wie Apache verwaltet werden. Zwei gleichzeitige Anforderungen an dieselbe PHP-Laufzeitseite können zwei isolierte Threads starten von Instanzen. )
Einzelthreading wirft ein Problem auf. Was passiert, wenn JavaScript einen „langsamen“ Prozess aufruft (z. B. eine Ajax-Anfrage im Browser oder eine Datenbankoperation auf dem Server)? Dieser Vorgang kann mehrere Sekunden oder sogar Minuten dauern. Der Browser ist gesperrt, während er auf eine Antwort wartet. Auf dem Server kann die Node.js-Anwendung die Benutzeranfrage nicht weiter verarbeiten.
Die Lösung ist die asynchrone Verarbeitung. Anstatt auf den Abschluss zu warten, wird ein Prozess angewiesen, eine andere Funktion aufzurufen, sobald das Ergebnis vorliegt. Dies wird als Rückruf bezeichnet und als Parameter an jede asynchrone Funktion übergeben. Zum Beispiel:
doSomethingAsync(callback1); console.log('finished'); // call when doSomethingAsync completes function callback1(error) { if (!error) console.log('doSomethingAsync complete'); }
doSomethingAsync() akzeptiert eine Callback-Funktion als Argument (es wird nur ein Verweis auf die Funktion übergeben, sodass fast kein Overhead entsteht). Es spielt keine Rolle, wie lange doSomethingAsync() dauert; wir wissen nur, dass callback1() irgendwann in der Zukunft ausgeführt wird. Die Konsole zeigt:
finished doSomethingAsync complete
Normalerweise können Rückrufe nur von einer asynchronen Funktion aufgerufen werden. So können Sie prägnante anonyme Inline-Funktionen verwenden:
doSomethingAsync(error => { if (!error) console.log('doSomethingAsync complete'); });
Durch die Verschachtelung von Rückruffunktionen kann eine Reihe von zwei oder mehr asynchronen Aufrufen seriell abgeschlossen werden. Zum Beispiel:
async1((err, res) => { if (!err) async2(res, (err, res) => { if (!err) async3(res, (err, res) => { console.log('async1, async2, async3 complete.'); }); }); });
Leider führt dies zu Callback Hell – einem berüchtigten Konzept (http://callbackhell.com/)! Der Code ist schwer zu lesen und wird schlechter, wenn Fehlerbehandlungslogik hinzugefügt wird.
Die Callback-Hölle kommt in der clientseitigen Codierung relativ selten vor. Wenn Sie einen Ajax-Aufruf durchführen, das DOM aktualisieren und darauf warten, dass die Animation abgeschlossen ist, kann sie zwei oder drei Ebenen tief gehen, ist aber normalerweise immer noch beherrschbar.
Anders ist die Situation bei Betriebssystemen oder Serverprozessen. Node.js-API-Aufrufe können Datei-Uploads empfangen, mehrere Datenbanktabellen aktualisieren, in Protokolle schreiben und weitere API-Aufrufe durchführen, bevor eine Antwort gesendet wird.
ES2015 (ES6) führte Promises ein. Callbacks können weiterhin verwendet werden, Promises bieten jedoch eine sauberere Syntax für die Verkettung asynchroner Befehle, sodass sie seriell ausgeführt werden können (mehr dazu).
Um die Promise-basierte Ausführung zu ermöglichen, müssen asynchrone Callback-basierte Funktionen so geändert werden, dass sie sofort ein Promise-Objekt zurückgeben. Dieses verspricht, dass das Objekt irgendwann in der Zukunft eine von zwei Funktionen (als Argumente übergeben) ausführt:
resolve: Eine Rückruffunktion, die ausgeführt wird, wenn der Handler erfolgreich abgeschlossen wird
reject: Optionale Rückruffunktion, die ausgeführt wird, wenn ein Fehler auftritt.
Im folgenden Beispiel stellt die Datenbank-API eine connect()-Methode bereit, die eine Rückruffunktion akzeptiert. Die externe asyncDBconnect()-Funktion gibt sofort ein neues Promise zurück und führt auflösen() oder ablehnen() aus, nachdem die Verbindung hergestellt wurde oder fehlschlägt:
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+ bietet das Dienstprogramm util.promisify(), Convert Callback-basierte Funktionen zu Promise-basierten Alternativen. Es gibt mehrere Bedingungen:
Übergeben Sie den Callback als letzten Parameter an die asynchrone Funktion
Die Callback-Funktion muss auf einen Fehler zeigen und befolgt werden durch einen Wertparameter.
Beispiel:
// Node.js: promisify fs.readFile const util = require('util'), fs = require('fs'), readFileAsync = util.promisify(fs.readFile); readFileAsync('file.txt');
Verschiedene Client-Bibliotheken bieten ebenfalls Promisify-Optionen, aber Sie können einige eigene erstellen:
// 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);
Alles, was ein Versprechen zurückgibt, kann eine Reihe asynchroner Funktionsaufrufe initiieren, die in der Methode .then() definiert sind. Jedem wird das Ergebnis der vorherigen Lösung übergeben:
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); });
Synchrone Funktionen können auch in einem .then()-Block ausgeführt werden. Der zurückgegebene Wert wird gegebenenfalls an die nächste .then()-Anweisung übergeben. Die Methode
.catch() definiert eine Funktion, die aufgerufen wird, wenn eine vorherige Ablehnung ausgelöst wird. Zu diesem Zeitpunkt wird die Methode .then() nicht mehr ausgeführt. Sie können in der gesamten Kette mehrere .catch()-Methoden verwenden, um verschiedene Fehler abzufangen.
ES2018 hat eine .finally()-Methode eingeführt, um jede endgültige Logik unabhängig vom Ergebnis auszuführen – z. B. Bereinigung, Schließen der Datenbankverbindung usw. Wird derzeit nur auf Chrome und Firefox unterstützt, aber das Tech-Komitee 39 hat die Polyfüllung .finally() veröffentlicht.
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技巧
Das obige ist der detaillierte Inhalt vonProzesssteuerung in js: Analyse von Callbacks&Promises&Async/Awai. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!