Maison >interface Web >js tutoriel >Quelles sont les méthodes de programmation asynchrone en JavaScript ? Introduction aux méthodes de programmation asynchrone JavaScript
Ce que cet article vous apporte, c'est quelles sont les méthodes de programmation asynchrone en JavaScript ? L'introduction des méthodes de programmation asynchrone JavaScript a une certaine valeur de référence. Les amis dans le besoin peuvent s'y référer, j'espère que cela vous sera utile.
Nous savons que l'environnement d'exécution du langage Javascript est "mono-thread". Cela signifie qu’une seule tâche peut être accomplie à la fois. S'il existe plusieurs tâches, elles doivent être mises en file d'attente, la tâche précédente est terminée, puis la tâche suivante est exécutée.
Bien que ce mode soit relativement simple à mettre en œuvre et que l'environnement d'exécution soit relativement simple, tant qu'une tâche prend beaucoup de temps, les tâches suivantes doivent être mises en file d'attente, ce qui retardera l'exécution de l'ensemble du programme. L'absence de réponse courante du navigateur (mort suspendue) est souvent causée par un certain morceau de code Javascript exécuté pendant une longue période (comme une boucle infinie), ce qui bloque la page entière à cet endroit et empêche d'autres tâches d'être effectuées.
Afin de résoudre ce problème, le langage Javascript divise le mode d'exécution des tâches en deux types : synchrone et asynchrone. Cet article présente principalement plusieurs méthodes de programmation asynchrone, et par comparaison, nous obtenons la meilleure solution de programmation asynchrone !
1. Synchronisation et asynchrone
On peut généralement comprendre qu'asynchrone signifie qu'une tâche est divisée en deux parties, la première partie est exécutée en premier, puis les autres tâches sont exécutés, etc. Lorsque vous êtes prêt, revenez en arrière et exécutez le deuxième paragraphe. Le code classé derrière la tâche asynchrone s'exécutera immédiatement sans attendre la fin de la tâche asynchrone. Autrement dit, les tâches asynchrones n'ont pas d'effet "bloquant". Par exemple, il existe une tâche qui lit les fichiers à traiter. Le processus d'exécution asynchrone est le suivant :
Ce type d'exécution discontinue est appelé asynchrone. . En conséquence, l'exécution continue est appelée synchronisation
Le "mode asynchrone" est très important. Du côté du navigateur, les opérations de longue durée doivent être effectuées de manière asynchrone pour éviter que le navigateur ne réponde. Le meilleur exemple est celui des opérations Ajax. Côté serveur, le "mode asynchrone" est même le seul mode, car l'environnement d'exécution est monothread, et si toutes les requêtes http peuvent être exécutées de manière synchrone, les performances du serveur chuteront considérablement et il ne répondra plus très rapidement. Ensuite, nous présenterons six méthodes de programmation asynchrone.
2. Fonction de rappel (Callback)
La fonction de rappel est la méthode la plus basique de fonctionnement asynchrone. Le code suivant est un exemple de fonction de rappel :
ajax(url, () => { // 处理逻辑 })
Mais la fonction de rappel a une faiblesse fatale, c'est-à-dire qu'il est facile d'écrire Callback hell (Callback hell). En supposant que plusieurs requêtes ont des dépendances, vous pouvez écrire le code suivant :
ajax(url, () => { // 处理逻辑 ajax(url1, () => { // 处理逻辑 ajax(url2, () => { // 处理逻辑 }) }) })
L'avantage de la fonction de rappel est qu'elle est simple, facile à comprendre et à mettre en œuvre, mais l'inconvénient est qu'elle n'est pas propice à la lecture et à la maintenance du code. Chaque partie Le couplage élevé entre elles rend la structure du programme confuse et le processus difficile à suivre (en particulier lorsque plusieurs fonctions de rappel sont imbriquées), et une seule fonction de rappel peut être spécifiée pour chaque tâche. De plus, il ne peut pas utiliser try catch pour détecter les erreurs et ne peut pas revenir directement.
3. Surveillance des événements
De cette façon, l'exécution des tâches asynchrones ne dépend pas de l'ordre du code, mais du fait qu'un événement se produise .
Voici deux fonctions f1 et f2. L'intention de la programmation est que f2 doit attendre que f1 soit terminé avant de pouvoir être exécuté. Tout d'abord, liez un événement à f1 (la méthode d'écriture jQuery utilisée ici)
f1.on('done', f2);
La ligne de code ci-dessus signifie que lorsque l'événement terminé se produit dans f1, f2 sera exécuté. Ensuite, réécrivez f1 :
function f1() { setTimeout(function () { // ... f1.trigger('done'); }, 1000); }
Dans le code ci-dessus, f1.trigger('done') signifie qu'une fois l'exécution terminée, l'événement done sera déclenché immédiatement, commençant ainsi à exécuter f2.
L'avantage de cette méthode est qu'elle est relativement facile à comprendre, qu'elle peut lier plusieurs événements, que chaque événement peut spécifier plusieurs fonctions de rappel et qu'elle peut être "découplée", ce qui est propice à la modularisation. L'inconvénient est que l'ensemble du programme doit être piloté par des événements et que le processus en cours devient très flou. Lors de la lecture du code, il est difficile de voir le flux principal.
4. Publier et s'abonner
Nous supposons qu'il existe un "centre de signal". Lorsqu'une tâche est terminée, un signal est "publié" vers le centre de signal. . , d'autres tâches peuvent « s'abonner » à ce signal du centre de signal pour savoir quand elles peuvent commencer l'exécution. C'est ce qu'on appelle le « modèle de publication-abonnement » (modèle de publication-abonnement), également connu sous le nom de « modèle d'observateur » (modèle d'observateur).
Tout d'abord, f2 s'abonne au signal terminé du centre de signal jQuery.
jQuery.subscribe('done', f2);
Ensuite, f1 est réécrit comme suit :
function f1() { setTimeout(function () { // ... jQuery.publish('done'); }, 1000); }
Dans le code ci-dessus, jQuery.publish('done') signifie qu'après l'exécution de f1, envoyez un message à le centre de signal jQuery Libère le signal terminé, déclenchant l'exécution de f2.
Une fois l'exécution de f2 terminée, vous pouvez vous désinscrire (désabonnement)
jQuery.unsubscribe('done', f2);
这种方法的性质与“事件监听”类似,但是明显优于后者。因为可以通过查看“消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
五、Promise/A+
Promise本意是承诺,在程序中的意思就是承诺我过一段时间后会给你一个结果。 什么时候会用到过一段时间?答案是异步操作,异步是指可能比较长时间才有结果的才做,例如网络请求、读取本地文件等
Pending----Promise对象实例创建时候的初始状态
Fulfilled----可以理解为成功的状态
Rejected----可以理解为失败的状态
这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,比如说一旦状态变为 resolved 后,就不能再次改变为Fulfilled
let p = new Promise((resolve, reject) => { reject('reject') resolve('success')//无效代码不会执行 }) p.then( value => { console.log(value) }, reason => { console.log(reason)//reject } )
当我们在构造 Promise 的时候,构造函数内部的代码是立即执行的
new Promise((resolve, reject) => { console.log('new Promise') resolve('success') }) console.log('end') // new Promise => end
每次调用返回的都是一个新的Promise实例(这就是then可用链式调用的原因)
如果then中返回的是一个结果的话会把这个结果传递下一次then中的成功回调
如果then中出现异常,会走下一个then的失败回调
在 then中使用了return,那么 return 的值会被Promise.resolve() 包装(见例1,2)
then中可以不传递参数,如果不传递会透到下一个then中(见例3)
catch 会捕获到没有捕获的异常
接下来我们看几个例子:
// 例1 Promise.resolve(1) .then(res => { console.log(res) return 2 //包装成 Promise.resolve(2) }) .catch(err => 3) .then(res => console.log(res))
// 例2 Promise.resolve(1) .then(x => x + 1) .then(x => { throw new Error('My Error') }) .catch(() => 1) .then(x => x + 1) .then(x => console.log(x)) //2 .catch(console.error)
// 例3 let fs = require('fs') function read(url) { return new Promise((resolve, reject) => { fs.readFile(url, 'utf8', (err, data) => { if (err) reject(err) resolve(data) }) }) } read('./name.txt') .then(function(data) { throw new Error() //then中出现异常,会走下一个then的失败回调 }) //由于下一个then没有失败回调,就会继续往下找,如果都没有,就会被catch捕获到 .then(function(data) { console.log('data') }) .then() .then(null, function(err) { console.log('then', err)// then error }) .catch(function(err) { console.log('error') })
Promise不仅能够捕获错误,而且也很好地解决了回调地狱的问题,可以把之前的回调地狱例子改写为如下代码:
ajax(url) .then(res => { console.log(res) return ajax(url1) }).then(res => { console.log(res) return ajax(url2) }).then(res => console.log(res))
它也是存在一些缺点的,比如无法取消 Promise,错误需要通过回调函数捕获。
六、生成器Generators/ yield
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,Generator 最大的特点就是可以控制函数的执行。
语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
Generator 函数除了状态机,还是一个遍历器对象生成函数。
可暂停函数, yield可暂停,next方法可启动,每次返回的是yield后的表达式结果。
yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
我们先来看个例子:
function *foo(x) { let y = 2 * (yield (x + 1)) let z = yield (y / 3) return (x + y + z) } let it = foo(5) console.log(it.next()) // => {value: 6, done: false} console.log(it.next(12)) // => {value: 8, done: false} console.log(it.next(13)) // => {value: 42, done: true}
可能结果跟你想象不一致,接下来我们逐行代码分析:
首先 Generator 函数调用和普通函数不同,它会返回一个迭代器
当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
当执行第二次 next 时,传入的参数12就会被当作上一个yield表达式的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 12,所以第二个 yield 等于 2 12 / 3 = 8
当执行第三次 next 时,传入的参数13就会被当作上一个yield表达式的返回值,所以 z = 13, x = 5, y = 24,相加等于 42
我们再来看个例子:有三个本地文件,分别1.txt,2.txt和3.txt,内容都只有一句话,下一个请求依赖上一个请求的结果,想通过Generator函数依次调用三个文件
//1.txt文件 2.txt
//2.txt文件 3.txt
//3.txt文件 结束
let fs = require('fs') function read(file) { return new Promise(function(resolve, reject) { fs.readFile(file, 'utf8', function(err, data) { if (err) reject(err) resolve(data) }) }) } function* r() { let r1 = yield read('./1.txt') let r2 = yield read(r1) let r3 = yield read(r2) console.log(r1) console.log(r2) console.log(r3) } let it = r() let { value, done } = it.next() value.then(function(data) { // value是个promise console.log(data) //data=>2.txt let { value, done } = it.next(data) value.then(function(data) { console.log(data) //data=>3.txt let { value, done } = it.next(data) value.then(function(data) { console.log(data) //data=>结束 }) }) }) // 2.txt=>3.txt=>结束
从上例中我们看出手动迭代Generator
函数很麻烦,实现逻辑有点绕,而实际开发一般会配合 co
库去使用。co
是一个为Node.js和浏览器打造的基于生成器的流程控制工具,借助于Promise,你可以使用更加优雅的方式编写非阻塞代码。
安装co
库只需:npm install co
上面例子只需两句话就可以轻松实现
function* r() { let r1 = yield read('./1.txt') let r2 = yield read(r1) let r3 = yield read(r2) console.log(r1) console.log(r2) console.log(r3) } let co = require('co') co(r()).then(function(data) { console.log(data) }) // 2.txt=>3.txt=>结束=>undefined
我们可以通过 Generator 函数解决回调地狱的问题,可以把之前的回调地狱例子改写为如下代码:
function *fetch() { yield ajax(url, () => {}) yield ajax(url1, () => {}) yield ajax(url2, () => {}) } let it = fetch() let result1 = it.next() let result2 = it.next() let result3 = it.next()
七、async/await
1.Async/Await简介
使用async/await,你可以轻松地达成之前使用生成器和co函数所做到的工作,它有如下特点:
async/await是基于Promise实现的,它不能用于普通的回调函数。
async/await与Promise一样,是非阻塞的。
async/await使得异步代码看起来像同步代码,这正是它的魔力所在。
一个函数如果加上 async ,那么该函数就会返回一个 Promise
async function async1() { return "1" } console.log(async1()) // -> Promise {<resolved>: "1"}</resolved>
Generator函数依次调用三个文件那个例子用async/await写法,只需几句话便可实现
let fs = require('fs') function read(file) { return new Promise(function(resolve, reject) { fs.readFile(file, 'utf8', function(err, data) { if (err) reject(err) resolve(data) }) }) } async function readResult(params) { try { let p1 = await read(params, 'utf8')//await后面跟的是一个Promise实例 let p2 = await read(p1, 'utf8') let p3 = await read(p2, 'utf8') console.log('p1', p1) console.log('p2', p2) console.log('p3', p3) return p3 } catch (error) { console.log(error) } } readResult('1.txt').then( // async函数返回的也是个promise data => { console.log(data) }, err => console.log(err) ) // p1 2.txt // p2 3.txt // p3 结束 // 结束
如果请求两个文件,毫无关系,可以通过并发请求
let fs = require('fs') function read(file) { return new Promise(function(resolve, reject) { fs.readFile(file, 'utf8', function(err, data) { if (err) reject(err) resolve(data) }) }) } function readAll() { read1() read2()//这个函数同步执行 } async function read1() { let r = await read('1.txt','utf8') console.log(r) } async function read2() { let r = await read('2.txt','utf8') console.log(r) } readAll() // 2.txt 3.txt
八、总结
1.JS 异步编程进化史:callback -> promise -> generator -> async + await
2.async/await 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里。
3.async/await可以说是异步终极解决方案了。
(1) async/await函数相对于Promise,优势体现在:
处理 then 的调用链,能够更清晰准确的写出代码
并且也能优雅地解决回调地狱问题。
当然async/await函数也存在一些缺点,因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低,代码没有依赖性的话,完全可以使用 Promise.all 的方式。
(2) async/await函数对 Generator 函数的改进,体现在以下三点:
内置执行器。
Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。
更广的适用性。 co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
更好的语义。 async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
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!