大纲
- JS与python的同步和异步
- generator的执行器
- 一个delay函数
- delay函数中await的返回值
- 总结
- 老师看见对方立刻世纪东方
JS与python的同步和异步
- JS的一般语句和函数都是和python一样,是同步的,死循环会阻塞REPL,比如JS中:
/* jafascript */
setTimeout(console.log, 1000, '1 sec passed')
while(true) {
if (isBreak) { break }
}
这段代码中,setTimeout的回调永远不会执行,除非while语句被break。如果是1秒内被break,则回调在第1秒钟被执行,否则回调被while阻塞,一旦while被break,回调立刻执行,这个可以在while里用new Date()来控制。
JS的许多函数是异步函数,比如setTimeout,以及请求url,读写文件等,异步函数会立即返回,所以后续操作只能通过回调函数。
python没有异步函数,只能使用gevent等异步库,并且在开头执行monkey_patch来给源生的同步函数打补丁使其成为异步的。
JS的异步函数和gevent库的异步函数略有不同,gevent是基于协程的,代码写起来是顺序的,比JS的回调地狱要舒服很多。
es6中支持async,await,配合generator和Promise,可以实现基于协程的异步调用,使用起来和gevent一样方便。callback,Promise和协程的写法对比如下,假设需要从文件a.txt读取到代表下一个文件名的文字'b.txt',然后读取b.txt,读到c.txt,然后在c.txt中读到real data:
/* callback*/
let fn= 'a.txt'
read(fn, (err, data) => {
if(err) {handle(err)}
fn = data // b.txt
read(fn, (err, data) => {
if(err) {handle(err)}
fn = data // c.txt
read(fn, (err, data) => {
if(err) {handle(err)}
// data === 'real data'
doSomethingWith(data)
})
})
})
/* Promise */
let fn = 'a.txt'
read(fn)
.then(read)
.then(read)
.then(doSomethingWith)
/* or */
read(fn)
.then(filename => read(filename))
.then(filename => read(filename))
.then(data => doSomethingWith(data))
/* coroutine */
async funciton sequenceRead(fn) {
let filename
filename = await read(fn) // filename = 'b.txt'
filename = await read(filename) // filename = 'c.txt'
let data = await read(filename) // data = 'real data'
doSomethingWith(data)
在语义上,还是最后的coroutine(协程)最清晰,这简直和python的同步读取文件一模一样。另外需要注意一个前提,协程中yield之后或者await之后的东西必须是一个Promise。
generator的执行器
- generator加上执行器就能实现协程。回调函数通过一个将来会被回调的函数来取得将来的控制权,Promise通过then注册一个函数,器本质还是将来被回调,只不过嵌套回调可以写成现行的.then。generator是通过next来触发异步调用(一个Promise),但是该Promise注册的回调函数中,有next语句,这样就能在Promise执行完后,执行权能自动地回到原generator里。如果考察手动实现执行器的话,那么对于上面顺序读取文件的例子,将是这样的:
/* 生成器 */
let fn = 'a.txt'
function* sequenceRead(fn) {
let filename
filename = yield read(fn)
fllename = yield read(filename)
let data = yield read(filename)
doSomethingWith(data)
/* 手动执行 */
let g = sequenceRead(fn)
g.next().value.then(filename => {
g.next(filename).value.then(filename => {
// 递归回调
// ...
})
})
手动执行的代码会非常恶心,就是一种回调地狱,但是他的每次调用模式都是一样的,所以可以用递归函数来非常简洁的实现。其中有一点比较搞的是,JS的next可带参数,python不行。JS中的next所带的参数,会赋值给前一次yield。所以对于这句:
g.next(filename).value.then(filename => {
//...
})
next里的filename是前面的then方法里,read所得到的数据(此处是b.txt),这个next将此filename赋值给generator中对应的前一个yield,所以这个值为'b.txt'的filename将赋值给filename = yield read(fn)
中的filename。async/await提供了一种自动执行器,async
相当于function*
,await
相当于yield
,但是相当于并不代表等同于,async函数返回的不是generator,而是Promise,且async函数自带执行器。
- 考虑如下例子:
const timeout = (t, msg) => {
return new Promise(res => {
let func = m => {
console.log("异步执行了:", m)
res(m)
}
setTimeout(func, t, msg)
})
}
// ------------------------------------------------- //
async function func(n) {
let arr = []
for (let i=0; i<n; i++) arr.push(i)
let ps = arr.map(i => {
let t = Math.random()*1000*5 // 随机0~5秒的timeout
let msg = `id: ${i}, cost: ${t} ms.`
return timeout(t, msg)
})
for (let i=0; i<n; i++) {
let msg = await ps[i]
console.log('=>同步取回了', msg)
}
}
这段代码演示了异步并行地执行timeout,然后同步地取回他们的值。其中异步执行的log都是符合现实时间的顺序出现,但是因为是0~5秒随机的时间,所以id此时是无需的。一旦出现了id为0,即第0个已经执行完毕,则立刻取回,然后接下去取回id为1的,然后是id=2的,但是id=2的还没准备好,因此会等待,即使期间其他id的准备好了,await语句还是在等待id=2的Promise:
let msg = await ps[i]
console.log('=>同步取回了', msg)
所以会呈现如下图所示的log次序:
一个delay函数
- 实现要求是,如同python的
time.sleep
函数效果一样,将下一个语句延迟t毫秒:
async function test() {
let t1 = new Date()
let res = await delay(x=>x*2, 1500, 15)
let t2 = new Date()
console.log(t2-t1)
console.log(res)
}
运行结果如下图:
可看出,
await
语句将let t1 = new Date()
和let t2 = new Date()
隔开了1500毫秒(实际会比1500多一些),res
是取回的await
函数的返回值(深究起来,其实就是delay执行后所返回的Promise的then方法中,那个回调函数的参数:delay(x=>x*2, 1500, 15).then(res=>{doSomethingWith(res)})
,就是这个then中的res)。
- delay函数是一个Promise或者async函数才能被await(async函数本身也是返回了Promise):
const delay = async (func, t, ...args) => {
await new Promise((res, rej) => {
setTimeout(res, t) // 就是这个res,隐含调用了next。
})
// 最后return的东西,并不是最终被async函数return的,
// 实际上async函数return的是一个Promise,这个Promise的
// then方法中的回调函数的参数,才是下面return的内容
return func(...args)
}
delay函数中await的返回值
- 如上面那个delay中的await语句
await new Promise((res, rej) => {
// 就是这个res,隐含调用了next。
setTimeout(res, t)
})
因为await
相当于yield
,调用这个async函数后,因为遇到yield就会转移执行权,而这个res会隐含调用next,使得setTimeout的时间到点后,自动将执行权交回此处,实现顺序执行。如果此处将该句写成如下:
let x = await new Promise((res, rej) => {
setTimeout(res, t, 'test string')
})
那么时间到点后,x
就等于'test string'
总结
-
async / await
书写起来最方便,最好用。 -
async / await
是自带执行器的generator / yield
。 -
generator
协程实现的关键是,yield
出来的Promise
,在其then
注册的回调函数中,待取得数据就调用next
,交回执行权。多个await
就会嵌套地递归调用next
。