La méthode d'appel asynchrone la plus basique dans JS est le rappel, qui transmet la fonction de rappel à l'API asynchrone. Après l'achèvement asynchrone, le navigateur ou le nœud. Avertissez le moteur JS d’appeler le rappel. Pour les opérations asynchrones simples, il suffit d'utiliser le rappel. Mais avec l’émergence des pages interactives et de Node, les inconvénients de la solution de rappel ont commencé à apparaître. La spécification Promise est née et intégrée à la spécification ES6. Plus tard, ES7 a incorporé des fonctions asynchrones dans la norme basée sur Promise. C'est l'histoire de l'évolution asynchrone de JavaScript.
Habituellement, le code est exécuté de haut en bas. S'il y a plusieurs tâches, elles doivent être mises en file d'attente. La tâche précédente sera terminée avant que la tâche suivante ne soit exécutée. Ce mode d'exécution est dit synchrone. Les novices peuvent facilement confondre synchronisation en langage informatique et synchronisation en langage courant. Par exemple, la synchronisation dans « synchronisation des fichiers avec le cloud » fait référence à « maintenir... la cohérence ». Sur les ordinateurs, la synchronisation fait référence au mode dans lequel les tâches sont exécutées séquentiellement de haut en bas. Par exemple :
A(); B(); C();
Dans ce code, A, B et C sont trois fonctions différentes, et chaque fonction est une tâche indépendante. En mode synchrone, l'ordinateur effectuera la tâche A, puis la tâche B et enfin la tâche C. Dans la plupart des cas, le mode synchronisation convient. Mais si la tâche B est une requête réseau de longue durée et que la tâche C affiche une nouvelle page, la page Web se bloquera.
Une meilleure solution consiste à diviser la tâche B en deux parties. Une partie exécute immédiatement les tâches demandées par le réseau et l'autre partie exécute les tâches après le retour de la demande. Ce modèle dans lequel une partie est exécutée immédiatement et l’autre partie est exécutée dans le futur est appelé asynchrone.
A(); // 在现在发送请求 ajax('url1',function B() { // 在未来某个时刻执行 }) C(); // 执行顺序 A => C => B
En fait, le moteur JS ne gère pas directement les requêtes réseau. Il appelle simplement l'interface de requête réseau du navigateur, et le navigateur envoie des requêtes réseau et surveille les données renvoyées. L'essence des capacités asynchrones de JavaScript réside dans les capacités multithread du navigateur ou du nœud.
La fonction exécutée dans le futur est généralement appelée rappel. L’utilisation du mode de rappel asynchrone résout le problème de blocage, mais entraîne également d’autres problèmes. Au début, nos fonctions étaient écrites de haut en bas et exécutées de haut en bas. Ce mode "linéaire" est très cohérent avec nos habitudes de pensée, mais maintenant il est interrompu par le rappel ! Dans le morceau de code ci-dessus, il ignore désormais la tâche B et exécute la tâche C en premier ! Ce type de code « non linéaire » asynchrone sera plus difficile à lire que le code « linéaire » synchrone, et donc plus susceptible de générer des bugs.
Essayez de juger l'ordre d'exécution du code suivant. Vous comprendrez mieux que le code « non linéaire » est plus difficile à lire que le code « linéaire ».
A(); ajax('url1', function(){ B(); ajax('url2', function(){ C(); } D(); }); E(); // A => E => B => D => C
Dans ce code, l'ordre d'exécution de haut en bas est perturbé par le Callback. Notre ligne de mire lors de la lecture du code est A => C => Ce ne sont pas les inconvénients du code linéaire.
En avançant les tâches exécutées après ajax, il est plus facile de comprendre l'ordre d'exécution du code. Bien que le code semble moche en raison de l'imbrication, l'ordre d'exécution est désormais « linéaire » de haut en bas. Cette technique est très utile lors de l’écriture de plusieurs codes imbriqués.
A(); E(); ajax('url1', function(){ B(); D(); ajax('url2', function(){ C(); } }); // A => E => B => D => C
Le code précédent gère uniquement le rappel de réussite et ne gère pas le rappel d'exception. Ensuite, ajoutez le rappel de gestion des exceptions, puis discutez de la question de l'exécution « linéaire » du code.
A(); ajax('url1', function(){ B(); ajax('url2', function(){ C(); },function(){ D(); }); },function(){ E(); });
Après avoir ajouté le rappel de gestion des exceptions, la fonction de rappel de réussite B et la fonction de rappel d'exception E de l'url1 sont séparées. Cette situation « non linéaire » apparaît à nouveau.
Dans node, afin de résoudre le problème « non linéaire » causé par des rappels anormaux, une stratégie d'erreur d'abord a été formulée. Le premier paramètre de rappel dans le nœud est spécifiquement utilisé pour déterminer si une exception se produit
A(); get('url1', function(error){ if(error){ E(); }else { B(); get('url2', function(error){ if(error){ D(); }else{ C(); } }); } });
À ce stade, le problème "non linéaire" causé par le rappel a été essentiellement résolu. Malheureusement, en utilisant l'imbrication de rappel, les couches de if else et les fonctions de rappel, une fois que le nombre de couches imbriquées augmente, ce n'est pas très pratique à lire. De plus, une fois qu'une exception se produit lors du rappel, l'exception ne peut être gérée que dans la fonction de rappel actuelle.
Dans l'histoire de l'évolution asynchrone de JavaScript, une série de bibliothèques ont émergé pour résoudre les lacunes du rappel, et Promise est devenu le gagnant final et a été introduit avec succès dans ES6. Cela fournira une meilleure manière d'écrire "linéaire" et résoudra le problème selon lequel les exceptions asynchrones ne peuvent être interceptées que dans le rappel actuel.
La promesse est comme un intermédiaire qui promet de renvoyer un résultat asynchrone fiable. Tout d'abord, Promise signe un accord avec l'interface asynchrone. En cas de succès, la fonction de résolution est appelée pour notifier Promise. Lorsqu'une exception se produit, le rejet est appelé pour notifier Promise. D'un autre côté, Promise et callback signent également un accord, et Promise renverra une valeur fiable au rappel enregistré à ce moment-là et capturé à l'avenir.
// 创建一个 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)!