Promise, generator & async function, 以及对比python

相关github草稿代码在此处
相关es6教程在此处

大纲

  • JS与python的同步和异步
  • generator的执行器
  • 一个delay函数
  • delay函数中await的返回值
  • 总结
  • 老师看见对方立刻世纪东方
JS与python的同步和异步
  1. 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()来控制。

  1. JS的许多函数是异步函数,比如setTimeout,以及请求url,读写文件等,异步函数会立即返回,所以后续操作只能通过回调函数。

  2. python没有异步函数,只能使用gevent等异步库,并且在开头执行monkey_patch来给源生的同步函数打补丁使其成为异步的。

  3. JS的异步函数和gevent库的异步函数略有不同,gevent是基于协程的,代码写起来是顺序的,比JS的回调地狱要舒服很多。

  4. 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的执行器
  1. 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函数自带执行器。

  1. 考虑如下例子:
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次序:

async函数的同步与异步演示
一个delay函数
  1. 实现要求是,如同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)
}

运行结果如下图:

async| await演示

可看出,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)。

  1. 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的返回值
  1. 如上面那个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'

总结
  1. async / await书写起来最方便,最好用。
  2. async / await是自带执行器的generator / yield
  3. generator协程实现的关键是,yield出来的Promise,在其then注册的回调函数中,待取得数据就调用next,交回执行权。多个await就会嵌套地递归调用next
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容