JS异步编程,3/3,async-await,generator,eventloop
Posted 小韩说课
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JS异步编程,3/3,async-await,generator,eventloop相关的知识,希望对你有一定的参考价值。
今天整理一下JS中异步相关问题,缘起是同事的讨论。大概是三部分,1,异步是什么。2异步的语法。3异步的实现eventloop。 今天是第3部分,由以下的话题组成:
6 async-await7 Generator,生成器8 EventLoop 事件循环9 总结
上一篇说到了 异步是什么? 为什么要异步?以及回调和promise JS中的单线程语言是什么意思,请移步:
6 async-await
在 es7(ES2017) 中,提供了一种新的异步语法 async,该语法的目的就是定义一个异步执行的函数,内部实现是对Promise的封装,演示如下:
const fs = require('fs')
async function f() {
// 同步读取文件内容
return fs.readFileSync('./data', 'utf8')
}
f().then(v => console.log(v))
console.log('after run')
// 执行结果
after run
hello Kang!(./data文件内容)
分析以上代码,定义的函数 f,由于使用了 async 关键字,使得该函数变为了一个异步执行的函数。注意 f() 函数为异步函数与函数主体代码没有任何关系,本例中 fs.readFileSync
是一个同步方法。
在 async 异步函数被调用时,会返回一个Promise对象,而异步函数 f() 的返回值,就是Promise中resolve()的参数。因此后续可以使用 .then() 继续处理异步执行完毕后的代码。重要一点是f()函数为异步,是因为返回了Promise对象。而f()本身并没有异步!
再看执行结果,先输出的 after run,可见f()的调用是异步的。之后输出了文件内容,可见f()函数的返回值传到了then中。
既然是对Promise的封装,上面的代码可以改写为Promise的版本:
const fs = require('fs')
let promise = new Promise((resolve, reject)=>{
resolve(fs.readFileSync('./data', 'utf8'))
})
promise.then(v => console.log(v))
console.log('after run')
// 执行结果
after run
hello Kang!
对比这个Promise语法,使用async可以Promise的传参过程,而且async更加直观。
说完 async,再说 await,async wait,等待,用在async异步函数内。指的是await在等待异步调用的结果,可以用于在多个连续异步调用间传递数据。await通常后需要一个Promise对象,await可以获取该异步的结果。演示如下:
const fs = require('fs')
async function fr() {
c1 = await new Promise((resolve, reject) => {
// 异步执行代码
fs.readFile('./file-1', 'utf8', (e, content) => {
resolve(content)
})
})
console.log(c1)
return new Promise((resolve, reject) => {
// 异步执行代码
fs.readFile(c1, 'utf8', (e, content) => {
resolve(content)
})
})
}
fr().then((c)=>{
console.log(c)
})
console.log('after run')
// 输出结果
after run
./file-2
file 2 content
分析以上示例代码,fr() 有由两个异步请求实现,第一个异步请求的结果,是第二个请求的文件名。那就意味着需要在第一个异步请求成功后,再执行第二个异步请求。通常的做法,就是在异步请求1中的成功回调中,去调用异步请求2,这就面临的典型的回调地狱。使用 async+await 后,await用来等待异步请求1的结果,再作为参数给异步请求2来使用,而且不需要在异步1的成功回调内进行调用,两个异步请求在语法上是并行的,没有嵌套关系,比之前的Promise语法还要同步化!额外的,await 不就是把这个Promise变成阻塞执行?
关于 async+await 的更完整的语法,可移步:http://js.hellokang.net/async.html 和 http://js.hellokang.net/await.html。
7 Generator,生成器
Generator生成器之所以在异步执行中被讨论,是因为yield方法可以暂停(启动)代码的执行,可以通过.next()启动固定位置的代码,以达到多个异步顺序执行的目的。示例如下:
function* file_read() {
yield fs.readFile('./file-1', 'utf8', (err, content_1) => {
console.log(content_1)
fr.next()
})
yield fs.readFile('./file-2', 'utf8', (err, content_2) => {
console.log(content_2)
})
}
fr = file_read()
fr.next()
在异步代码前使用了yield,就可以暂停了异步的执行,需要的时候(第一个异步执行成功时),再启动。若没有这个语法,需要在第一个异步回调中嵌入第二个异步,又是回调地狱。
一个基于generator的库co.js,就是该类的实现。可以参考:https://www.npmjs.com/package/co
个人认为生成器不应该在异步中讨论,生成器主要是用来得到迭代器对象的,这个语法有点可以异步了。
8 EventLoop 事件循环
Js 的异步执行(并发执行)依赖于事件循环模式。JS是单线程,指的是其主线程指令为非并发执行的。一旦执行到异步代码(例如,setTimetout,AJAX,异步IO),异步任务不会阻塞主线程,而是先去注册处理函数再交由其他线程执行该异步代码。当其他线程执行完毕该异步任务后,会将该异步代码对应的处理函数加入到某个消息队列中(一般称之为:EventQueue)。当前的主线程代码执行完毕后,会从该消息队列中获取需要执行的任务,也就是异步代码注册的回到函数。
之所以叫事件循环,可以理解为一下的伪代码:
while (eventQueue.waitForMessage()) {
eventQueue.processNextMessage();
}
eventQueue.waitForMessage() 为同步状态,等待消息到达。
小结下就是:JS在执行时是的主线程是同步的,通过异步代码可以执行并发任务,但是并发任务由非主线程执行,因此不会干扰到主线程的执行,同时JS运行时(runtime)会不断监听事件消息队列,若存在可用的事件就去执行(但一定注意,不是事件一存在就执行,而是要等到主线程的当前任务执行结束,例如本文中的大的for)。
关于event事件,JS还有更细致的划分,通常分为macro-tast(宏任务)和micro-tast(微任务),使用的eventQueue和执行顺序也不尽相同,此处不予讨论。可以移步 http://js.hellokang.net/eventloop.html。
9 总结
异步执行,通常需要解决3个问题:
什么调用会异步执行? 例如:setTimeout,异步IO(AJAX,异步文件读写,fs.readFile)这些会异步执行。
异步执行后如何处理回调? 例如:传参callback,promise.then,async解决的是如何更好的执行回调函数的问题。
多个相关的异步如何保证顺序执行? 例如:回调地狱(回调中继续回调),promise.then.then..,await等都是解决该问题。
如果可以将异步执行从以上三个角度理解,那么各种语法解决的问题就显而易见啦。
讨论到此!
以上是关于JS异步编程,3/3,async-await,generator,eventloop的主要内容,如果未能解决你的问题,请参考以下文章