JavaScript异步编程核心概念-Generator

175 阅读6分钟

        相比于传统回调函数的方式,Promise去处理异步调用最大的优势,就是可以通过链式调用解决回调地狱问题。使用Promise去处理异步任务的串联执行它的表现就是反复的调用 then 方法,形成整体的任务链条从而实现所有任务的串联执行。但是这样写仍有大量的回调函数,虽然说他们之间没有相互的嵌套,但是他们还是没有办法达到我们传统同步代码的那种可读性。如果说是传统同步代码的方式,就像如下方式写的异步代码:

try {
    const value1 = ajax('/api/url1')
    console.log(value1)
    const value2 = ajax('/api/url2')
    console.log(value2)
} catch (error) {
    console.error(error)
}

ES2015提出的Generator异步解决方案,提供了一种更贴近同步代码的编程方案。Generator生成器函数在语法上,就是在普通函数的基础上前面加了一个星号 *。当调用一个生成器函数时它并不会马上执行函数,而是得到一个生成器对象。直到手动的调用这个对象的next方法,这个函数的函数体才会开始执行。其次在函数内部可以随时使用yield关键词,向外去返回一个值。然后在next方法返回对象当中去拿到这样一个返回值,在返回值对象中用一个 done 属性用来去表示这个生成器是否已经全部执行完毕。yield 关键词不会像 return 语句一样立即去结束这个函数的执行,它只是暂停我们这个生成器函数的执行,直到外界下一次调用生成器对象的next 方法时,它就会继续从 yield 这个位置往后执行。如果说调用生成器函数的next方法时传入一个参数的话,所传入的这个参数它会作为 yield 这个语句的返回值。也就是说在 yield 左边实际上是可以接收到这样一个值的,如果在外部调用的是生成器的 throw 方法,这个方法就可以对生成器函数内部去抛出一个异常。内部在往下执行的时候就会的得到这个异常,并且可以通过 try catch 去捕获这个异常。

function * foo () {
    console.log('start')
    try {
        const res = yield 'foo'
        console.log(res)
    } catch (e) {
        console.error(e)
    }
}
const generator = foo()
const result = generator.next()
console.log(result)
generator.next('bar')
generator.throw(new Error('Generator Error'))

利用yield暂停生成器函数执行的特点,来使用生成器函数去实现一个更优的异步编程体验。先来定义一个 main 的生成器函数,然后在这个函数内部使用 yield 去返回一个 ajax 调用。也就是返回了一个 Promise 对象出去,然后去接受一下这个 yield 语句的返回值。完成以后就可以在外界去调用这个这个生成器函数去得到一个生成器对象,然后再去调用这个对象的next方法那样的话 main 函数就会执行到第一个 yield 的位置,也就是会执行到第一个 ajax 调用。这里 next 方法返回对象的value 就是 ajax 返回Promise 对象,所以说就可以在这个后面通过then的方式去指定这个Promise的回调,在这个Promise的回调当中就可以拿到这个Promise的执行结果。此时就可以通过再调用一次next把我们得到data传递出去,这样的话main函数就可以接着继续往下执行,而且所传递进去的data会作为当前这个yield的返回值,这样得到话就可以拿到这个result。这样就在Promise内部彻底消灭了Promise回调,有了一种近乎于同步代码的体验。

function * main () {
    const user = yield ajax('/api/usrs.json')
    console.log(user)
}
const g = main()
const result = g.next()
result.value.then(data => {
    g.next(data)
})

结合递归的方法,可以实现一个更通用的生成器函数的执行器。首先定义一个函数 handlerResult 接收一个 result 参数,这result 就是 next 方法所返回的 result 在这个函数的内部应该先去判断resultdone属性是不是为true来判断生成器是不是结束了。如果说结束了就可以直接 return,反之如果这个生成器没有结束,这个resultvalue就是一个Promise对象。就可以使用这个对象的then方法去处理它的请求结果,在这个请求的回调当中可以继续去使用next让生成器函数接着往下执行,并且把这里面得到的数据传递进去。这个next方法返回的又是下一个 result,并再次将这个结果交给 handlerResult 进行递归处理。在外界只需要调用一下handlerResult 然后传入第一次next结果就可以了,后面的话只要生成器不结束这个递归就会一直执行下去。这里还需要处理异常情况,方法就是在then方法中添加一个失败的回调。在它当中就可以直接去调用生成器对象的throw方法,让这个生成器函数在继续执行时得到一个异常。在main函数内部就可以通过 try catch 捕获这个异常。

function * main () {
    try {
        const user = yield ajax('/api/unser.json')
        console.log(user)
        const post = yield ajax('/api/posr.json')
        console.log(post)
        const urls = yield ajax('/api/urls.json')
    } catch (error) {
        console.log(error)
    }
}
function co (generator) {
    const g = gernerator()
    function handlerResult (result) {
        if (result.done) return
        result.value.then(data => {
            handlerResult(g.next(data))
        }, error => {
            g.throw(error)
        })
    }
    handlerResult(g.next())
}
co(main)

有了Generator过后Javascript中的异步编程基本上就已经与同步代码有类似的体验了,但是使用Generator这种异步方案还需要自己手动去编写一个执行器函数比较麻烦。而在ES2017当中新增了一个 async await 同样提供了这种扁平化的异步编程体验,而且呢它是语言层面标准的异步编程语法,所以说使用起来就会更加的方便。其实,async函数就是生成器函数一种更方便的语法糖,在语法上async函数跟Generator函数是非常类似的。只要把生成器函数修改为一个使用async关键词去修饰的普通函数,然后在内部所有的yield关键词替换成 await 关键词就可以了。并且可以直接在外面调用这个函数,执行这个函数的话,内部这个执行过程会跟Generator函数会是完全一样的。相比于Generator async函数最大的好处,就是它不需要再去配合一个类似与Co这样的执行器。因为它是语言层面的标准异步编程语法,其次async函数可以返回一个Promise 对象,这样的话就更加利于对整体代码进行控制。除此之外,关于async函数还需要注意的一点就是,async当中使用的这个await关键词它只能够出现在async函数体中。

async main () {
    const usrs = await ajax('/api/user.json')
    console.log(users)
    const post = await ajax('/api/post.json')
    console.log(post)
    const urls = await ajax('/api/url.json')
    console.log(urls)
}
cosnt promise = main()
promise.then(() => {
    console.log('all complete')
})