Die grundlegendste asynchrone Aufrufmethode in JS ist der Rückruf, der den Rückruf der Rückruffunktion an die asynchrone API übergibt und der Browser oder Knoten die JS-Engine nach dem asynchronen Aufruf benachrichtigt Abschluss Rückruf anrufen. Für einfache asynchrone Vorgänge reicht es aus, einen Rückruf zu verwenden. Doch mit dem Aufkommen interaktiver Seiten und Node traten auch die Nachteile der Callback-Lösung zum Vorschein. Die Promise-Spezifikation wurde geboren und in die ES6-Spezifikation integriert. Später integrierte ES7 asynchrone Funktionen in den auf Promise basierenden Standard. Dies ist die Geschichte der asynchronen Entwicklung von JavaScript.
Normalerweise wird der Code von oben nach unten ausgeführt. Wenn mehrere Aufgaben vorhanden sind, müssen diese in die Warteschlange gestellt werden. Die vorherige Aufgabe wird abgeschlossen, bevor die nächste Aufgabe ausgeführt wird. Dieser Ausführungsmodus wird als synchron bezeichnet. Anfänger können die Synchronisierung in der Computersprache leicht mit der Synchronisierung in der Alltagssprache verwechseln. Beispielsweise bezieht sich die Synchronisierung in „Dateien mit der Cloud synchronisieren“ auf „Konsistenz beibehalten“. Bei Computern bezeichnet Synchronisation den Modus, in dem Aufgaben nacheinander von oben nach unten ausgeführt werden. Beispiel:
A(); B(); C();
In diesem Code sind A, B und C drei verschiedene Funktionen, und jede Funktion ist eine unabhängige Aufgabe. Im synchronen Modus führt der Computer Aufgabe A, dann Aufgabe B und schließlich Aufgabe C aus. In den meisten Fällen ist der Synchronisierungsmodus in Ordnung. Wenn es sich bei Aufgabe B jedoch um eine Netzwerkanforderung mit langer Laufzeit handelt und Aufgabe C zufällig eine neue Seite anzeigt, friert die Webseite ein.
Eine bessere Lösung besteht darin, Aufgabe B in zwei Teile aufzuteilen. Ein Teil führt vom Netzwerk angeforderte Aufgaben sofort aus und der andere Teil führt Aufgaben aus, nachdem die Anfrage zurückkommt. Dieses Muster, bei dem ein Teil sofort und der andere Teil in der Zukunft ausgeführt wird, wird als asynchron bezeichnet.
A(); // 在现在发送请求 ajax('url1',function B() { // 在未来某个时刻执行 }) C(); // 执行顺序 A => C => B
Eigentlich verarbeitet die JS-Engine Netzwerkanfragen nicht direkt, sondern ruft lediglich die Netzwerkanfrageschnittstelle des Browsers auf, und der Browser sendet Netzwerkanfragen und überwacht die zurückgegebenen Daten. Der Kern der asynchronen Funktionen von JavaScript sind die Multithreading-Funktionen des Browsers oder Knotens.
Funktionen, die in der Zukunft ausgeführt werden, werden normalerweise als Rückruf bezeichnet. Die Verwendung des asynchronen Rückrufmodus löst das Blockierungsproblem, bringt jedoch auch einige andere Probleme mit sich. Am Anfang wurden unsere Funktionen von oben nach unten geschrieben und von oben nach unten ausgeführt. Dieser „lineare“ Modus entspricht sehr gut unseren Denkgewohnheiten, wird aber jetzt durch Rückrufe unterbrochen! Im obigen Code wird jetzt Aufgabe B übersprungen und zuerst Aufgabe C ausgeführt! Diese Art von asynchronem „nichtlinearem“ Code ist schwieriger zu lesen als synchroner „linearer“ Code und führt daher eher zu Fehlern.
Versuchen Sie, die Ausführungsreihenfolge des folgenden Codes zu beurteilen. Sie werden ein tieferes Verständnis dafür haben, dass „nichtlinearer“ Code schwieriger zu lesen ist als „linearer“ Code.
A(); ajax('url1', function(){ B(); ajax('url2', function(){ C(); } D(); }); E(); // A => E => B => D => C
In diesem Code wird die Ausführungsreihenfolge von oben nach unten durch Callback unterbrochen. Unsere Sichtlinie beim Lesen des Codes ist A => B => C => D => E
, aber die Ausführungsreihenfolge ist A => E => B => D => C
. Das ist das Schlechte an nichtlinearem Code.
Indem Sie die nach ajax
ausgeführten Aufgaben vorzeitig verschieben, ist es einfacher, die Ausführungsreihenfolge des Codes zu verstehen. Obwohl der Code aufgrund der Verschachtelung unschön aussieht, ist die Ausführungsreihenfolge nun „linear“ von oben nach unten. Diese Technik ist sehr nützlich, wenn Sie mehrfach verschachtelten Code schreiben.
A(); E(); ajax('url1', function(){ B(); D(); ajax('url2', function(){ C(); } }); // A => E => B => D => C
Der vorherige Code behandelt nur den Erfolgsrückruf und nicht den Ausnahmerückruf. Fügen Sie als Nächstes den Rückruf zur Ausnahmebehandlung hinzu und diskutieren Sie dann das Problem der „linearen“ Ausführung des Codes. Nach dem Hinzufügen des Ausnahmebehandlungsrückrufs zu
A(); ajax('url1', function(){ B(); ajax('url2', function(){ C(); },function(){ D(); }); },function(){ E(); });
werden die Erfolgsrückruffunktion B und die Ausnahmerückruffunktion E von url1
getrennt. Diese „nichtlineare“ Situation tritt erneut auf.
Um das durch abnormale Rückrufe verursachte „nichtlineare“ Problem im Knoten zu lösen, wurde eine Fehler-zuerst-Strategie formuliert. Der erste Parameter des Rückrufs im Knoten wird speziell verwendet, um zu bestimmen, ob eine Ausnahme auftritt.
A(); get('url1', function(error){ if(error){ E(); }else { B(); get('url2', function(error){ if(error){ D(); }else{ C(); } }); } });
Zu diesem Zeitpunkt ist das durch den Rückruf verursachte „nichtlineare“ Problem grundsätzlich gelöst. Leider ist die Verwendung von Callback-Verschachtelung, Schicht für Schicht if else
und Callback-Funktionen nicht sehr bequem zu lesen, sobald die Anzahl der verschachtelten Ebenen zunimmt. Sobald im Rückruf eine Ausnahme auftritt, kann die Ausnahme außerdem nur innerhalb der aktuellen Rückruffunktion behandelt werden.
In der Geschichte der asynchronen Entwicklung von JavaScript ist eine Reihe von Bibliotheken entstanden, um die Mängel des Rückrufs zu beheben. Promise wurde der endgültige Gewinner und wurde erfolgreich in ES6 eingeführt. Dies bietet eine bessere „lineare“ Schreibweise und löst das Problem, dass asynchrone Ausnahmen nur im aktuellen Rückruf abgefangen werden können.
Versprechen ist wie ein Vermittler, der verspricht, ein vertrauenswürdiges asynchrones Ergebnis zurückzugeben. Zunächst unterzeichnet Promise eine Vereinbarung mit der asynchronen Schnittstelle. Bei Erfolg wird die Funktion resolve
aufgerufen, um Promise zu benachrichtigen. Wenn eine Ausnahme auftritt, wird reject
aufgerufen, um Promise zu benachrichtigen. Andererseits unterzeichnen Promise und Callback auch eine Vereinbarung, und Promise wird in Zukunft vertrauenswürdige Werte an die in then
und catch
registrierten Callbacks zurückgeben.
// 创建一个 Promise 实例(异步接口和 Promise 签订协议) var promise = new Promise(function (resolve,reject) { ajax('url',resolve,reject); }); // 调用实例的 then catch 方法 (成功回调、异常回调与 Promise 签订协议) promise.then(function(value) { // success }).catch(function (error) { // error })
Promise 是个非常不错的中介,它只返回可信的信息给 callback。它对第三方异步库的结果进行了一些加工,保证了 callback 一定会被异步调用,且只会被调用一次。
var promise1 = new Promise(function (resolve) { // 可能由于某些原因导致同步调用 resolve('B'); }); // promise依旧会异步执行 promise1.then(function(value){ console.log(value) }); console.log('A'); // A B (先 A 后 B) var promise2 = new Promise(function (resolve) { // 成功回调被通知了2次 setTimeout(function(){ resolve(); },0) }); // promise只会调用一次 promise2.then(function(){ console.log('A') }); // A (只有一个) var promise3 = new Promise(function (resolve,reject) { // 成功回调先被通知,又通知了失败回调 setTimeout(function(){ resolve(); reject(); },0) }); // promise只会调用成功回调 promise3.then(function(){ console.log('A') }).catch(function(){ console.log('B') }); // A(只有A)
介绍完 Promise 的特性后,来看看它如何利用链式调用,解决异步代码可读性的问题的。
var fetch = function(url){ // 返回一个新的 Promise 实例 return new Promise(function (resolve,reject) { ajax(url,resolve,reject); }); } A(); fetch('url1').then(function(){ B(); // 返回一个新的 Promise 实例 return fetch('url2'); }).catch(function(){ // 异常的时候也可以返回一个新的 Promise 实例 return fetch('url2'); // 使用链式写法调用这个新的 Promise 实例的 then 方法 }).then(function() { C(); // 继续返回一个新的 Promise 实例... }) // A B C ...
如此反复,不断返回一个 Promise 对象,再采用链式调用的方式不断地调用。使 Promise 摆脱了 callback 层层嵌套的问题和异步代码“非线性”执行的问题。
Promise 解决的另外一个难点是 callback 只能捕获当前错误异常。Promise 和 callback 不同,每个 callback 只能知道自己的报错情况,但 Promise 代理着所有的 callback,所有 callback 的报错,都可以由 Promise 统一处理。所以,可以通过catch
来捕获之前未捕获的异常。
Promise 解决了 callback 的异步调用问题,但 Promise 并没有摆脱 callback,它只是将 callback 放到一个可以信任的中间机构,这个中间机构去链接我们的代码和异步接口。
异步(async)函数是 ES7 的一个新的特性,它结合了 Promise,让我们摆脱 callback 的束缚,直接用类同步的“线性”方式,写异步函数。
声明异步函数,只需在普通函数前添加一个关键字 async
即可,如async function main(){}
。在异步函数中,可以使用await
关键字,表示等待后面表达式的执行结果,一般后面的表达式是 Promise 实例。
async function main{ // timer 是在上一个例子中定义的 var value = await timer(100); console.log(value); // done (100ms 后返回 done) } main();
异步函数和普通函数一样调用 main()
。调用后,会立即执行异步函数中的第一行代码 var value = await timer(100)
。等到异步执行完成后,才会执行下一行代码。
除此之外,异步函数和其他函数基本类似,它使用try...catch
来捕捉异常。也可以传入参数。但不要在异步函数中使用return
来返回值。
var timer = new Promise(function create(resolve,reject) { if(typeof delay !== 'number'){ reject(new Error('type error')); } setTimeout(resolve,delay,'done'); }); async function main(delay){ try{ var value1 = await timer(delay); var value2 = await timer(''); var value3 = await timer(delay); }catch(err){ console.error(err); // Error: type error // at create (<anonymous>:5:14) // at timer (<anonymous>:3:10) // at A (<anonymous>:12:10) } } main(0);
异步函数也可以被当作值,传入普通函数和异步函数中执行。但是在异步函数中,使用异步函数时要注意,如果不使用await
,异步函数会被同步执行。
async function main(delay){ var value1 = await timer(delay); console.log('A') } async function doAsync(main){ main(0); console.log('B') } doAsync(main); // B A
这个时候打印出来的值是 B A
。说明 doAsync
函数并没有等待 main
的异步执行完毕就执行了 console
。如果要让 console
在main
的异步执行完毕后才执行,我们需要在main
前添加关键字await
。
async function main(delay){ var value1 = await timer(delay); console.log('A') } async function doAsync(main){ await main(0); console.log('B') } doAsync(main); // A B
由于异步函数采用类同步的书写方法,所以在处理多个并发请求,新手可能会像下面一样书写。这样会导致url2
的请求必需等到url1
的请求回来后才会发送。
var fetch = function (url) { return new Promise(function (resolve,reject) { ajax(url,resolve,reject); }); } async function main(){ try{ var value1 = await fetch('url1'); var value2 = await fetch('url2'); conosle.log(value1,value2); }catch(err){ console.error(err) } } main();
使用Promise.all
的方法来解决这个问题。Promise.all
用于将多个Promise实例,包装成一个新的 Promis e实例,当所有的 Promise 成功后才会触发Promise.all
的resolve
函数,当有一个失败,则立即调用Promise.all
的reject
函数。
var fetch = function (url) { return new Promise(function (resolve,reject) { ajax(url,resolve,reject); }); } async function main(){ try{ var arrValue = await Promise.all[fetch('url1'),fetch('url2')]; conosle.log(arrValue[0],arrValue[1]); }catch(err){ console.error(err) } } main();
目前使用 Babel 已经支持 ES7 异步函数的转码了,大家可以在自己的项目中开始尝试。
以上就是JavaScript 异步进化史的代码实例详细介绍的内容,更多相关内容请关注PHP中文网(www.php.cn)!