美文网首页
Promise, generator & async funct

Promise, generator & async funct

作者: 王俊宇 | 来源:发表于2017-04-20 18:48 被阅读0次

相关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

相关文章

网友评论

      本文标题:Promise, generator & async funct

      本文链接:https://www.haomeiwen.com/subject/zkxlzttx.html