Generator介绍

那相比于传统回调函数的方式,Promise去处理异步调用最大的优势呢就是可以通过链式调用解决回调嵌套过深的问题。

那使用Promise去处理异步任务的串联执行,他的表现就是一个then然后去处理一个异步调用,最终整体会形成一个任务的链条,从而实现所有任务的串联执行。

ajax('/api/url1').then(value => {
    return ajax('/api/url2')
}).then(value => {
    return ajax('/api/url3')
}).then(value => {
    return ajax('/api/url4')
}).catch(error => {
    console.error(error)
})

但是呢这样写仍然会有大量的回调函数,虽然说他们相互之间没有嵌套但是呢他们还是没有办法达到我们传统同步代码那种可读性。

那如果说我们是传统同步代码的方式,那我们的代码呢,可能是我们下面例子显示的这种样式。

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

那很明显,这种方式去写我们的代码他是最简洁,也是最容易阅读和理解的。那接下来我们就来看两种更优的异步编程写法。

那首先呢就是ES2015提供的Generator, 也就是生成器函数。

那在此之前呢我们已经简单了解过生成器函数的语法和他的一些基本的特点,我们这里呢再快速来看一下。

那首先呢,语法上生成器函数就是在普通函数的基础上多了一个 * 号,那我们去调用一个生成器函数他并不会立即去执行这个函数,而是得到一个生成器对象。

那直到我们手动调用这个对象的next方法,这个函数的函数体才会开始执行。

function * foo () {
    console.log('start')
}

const generator = foo()

generator.next()

那其次我们可以在函数的函数体中随时使用yield关键词向外返回一个值。那我们可以在next方法返回的对象中拿到这样一个值。

另外在返回的对象中还有一个done属性,用来去表示这个生成器是否已经全部执行完了。

function * foo () {
    console.log('start')
    yield 'foo'
}

const generator = foo()

const result = generator.next()
console.log(result) // {value: 'foo', done: false}

而且yield关键词并不会像return语句一样立即去结束这个函数的执行,他只是暂停我们这个生成器函数的执行,直到我们外界下一次去调用我们生成器对象的next方法时他就会继续从yield这个位置向下执行。

另外呢如果说我们调用生成器对象next方法时如果我们传入了一个参数的话,那所传入的这个参数会作为yield这个语句的返回值,也就是说我们在yield的左边实际上是可以接收到这个值的。

function * foo () {
    console.log('start')
    const res = yield 'foo'
    console.log(res) // bar
}

const generator = foo()

const result = generator.next()
console.log(result) // {value: 'foo', done: false}

generator.next('bar')

那除此之外呢,如果说我们在外部手动调用的是生成器对象的throw方法,那这个方法呢就可以对生成器内部就是生成器函数内部去抛出一个异常。那异步在继续向下执行的时候就会得到这个异常。

我们可以通过try catch 去捕获这里得到的异常。

function * foo () {
    console.log('start')

    try {
        const res = yield 'foo'
        console.log(res) // bar
    } catch (e) {
        console.log(e)
    }
}

const generator = foo()

const result = generator.next()
console.log(result) // {value: 'foo', done: false}

// generator.next('bar')
generator.throw(new Error('Generator error'))

那这里我们再来回顾一下这个生成器函数的一个执行过程。

那首先呢我们调用foo函数他并没有立即执行这个函数,只是得到了一个生成器对象。

然后我们第一次去调用这个生成器的next方法那我们这个foo函数的函数体才会开始执行,一直会执行到yield这个关键词所在的位置,把yield后面这个值给他返回出去,然后我们foo函数就会暂停下来,然后yield后面的值就会作为next方法的返回对象里面的value去返回。

然后再往后我们再调用一次next方法,只不过这一次我们传入了一个字符串参数,那这个时候呢foo函数就会从我们刚刚暂停的位置继续往下执行,而我们这里传入的bar字符串会作为yield这个语句的返回值,那foo函数继续执行呢会继续执行到下一个yield的位置。

那如果我们在外部调用的是生成器的throw方法,那这个方法也会让我们生成器函数继续往下执行,不过呢,他的作用是抛出一个异常,所以说我们foo函数继续执行的时候就得到了这样一个异常。

那理解了生成器函数的一个特点,以及他的执行过程过后呢,我们再来看看如何去使用Generator去管理我们的异步流程。

Generator异步方案

这里我们其实完全可以借助于yield他可以暂停生成器函数执行这样一个特点,来去使用生成器函数来去实现一个更优的异步编程体验,那我们来看具体的实现方式。

那这里我们先定义一个叫做main的生成器函数,然后在这个生成器的内部我们使用yield返回一个ajax函数的调用,那也就是返回了一个promise对象出去。

然后我们接收一下这个yield语句的返回值,我们去把他打印出来。

完成以后我们就可以在外界去调用这个生成器函数去得到一个生成器对象,然后我们再去调用一下这个对象的next方法。

function ajax (url) {
    return new Promise(function (resolve, reject) {

        var xhr = new XMLHttpRequest()

        xhr.open('GET', url)

        xhr.responseType = 'json'

        xhr.onload = function () {
            if (this.status === 200) {
                resolve(this.response)
            } else {
                reject(new Error(this.statusText))
            }
        }

        xhr.sned()
    })
}

function * main () {
    const users = yield ajax('/api/users.json')
    console.log(users)
}

const g = main()
g.next()

那这样的话我们的main函数就会执行到第一个yield的位置,也就是会去执行第一个ajax调用。

那这里next方法返回的对象的value呢就是yield所返回的promise对象, 所以说我们就可以在这个后面通过then的方式去指定这个promise的回调。在这个回调当中就可以拿到这个promise的执行结果。

那此时我们就可以通过再调用一次next把我们得到的这个data传递进去。

const g = main()
const result = g.next()
result.value.then(data => {
    g.next(data)
})

那这样的话我们这个main函数就可以接着继续往下执行了,而且我们传递进去的data会作为我们当前这个yield的返回值,那我们这样的话就可以拿到这个users。

我们运行之后可以发现想要的结果就被打印出来了。

那这样对于promise函数的内部我们就彻底消灭了promise的回调,有了一种近乎于同步代码的体验。

那我们可以再到main函数当中再去添加下一个yield的操作,那这一次呢,我们再去请求另外一个地址,然后呢我们同样把这个结果打印出来。

那此时呢,我们在外部第二次调用next的结果呢他就也会是一个promise对象,所以说我们仍然可以接收到它可以调用它value当中的then方法,按照相同的方式去处理我们这个promise。

function * main () {
    const users = yield ajax('/api/users.json')
    console.log(users)
    const posts = yield ajax('/api/posts.json')
    console.log(posts)
    const urls = yield ajax('/api/urls.json')
    console.log(urls)
}

const g = main()
const result = g.next()
result.value.then(data => {
    const result2 = g.next(data)
    result2.value.then(data => {
        result3 = g.next(data)
        result3.value.then(data => {
        g.next(data)
    })
    })
})

那以此类推,如果我们在main函数中有多次使用yield的方式去返回promise的对象,而且每一次返回的都是promise对象,那我们这里完全就可以不断在结果对象的then当中去调用next直到我们next所返回对象的done属性为true,也就是说我们这个main函数已经完全被执行完了过后再停止。

所以说我们这应该在每次去调用这个then方法之前先去判断一下结果的done属性是不是为true,如果是为true的话那就代表这个生成器已经结束了就没有必要再继续了。

const g = main()
const result = g.next()
result.value.then(data => {
    const result2 = g.next(data)

    if (result2.done) return

    result2.value.then(data => {
        const result3 = g.next(data)

        if (result3.done) return

        result3.value.then(data => {
            g.next(data)
        })
    })
})

那很明显这里我们完全可以使用递归的方式去不断迭代,直到我们返回对象的done属性为true,也就是生成器执行结束了过后我们结束这样一个递归。

那这里我们就尝试以递归的方式来去实现一个更通用的生成器函数执行器。

我们这里先去定一个函数叫做handleResult, 那这个函数我们让他接收一个result参数,那这个result实际上就是next方法返回的那样一个result。

那在这个函数的内部我们首先应该去判断一下这个result的done属性是不是为true,也就是这个生成器是否已经结束了,如果说结束了我们这个handleResult就没必要继续往下执行了,我们就直接return掉。

反之如果说这个生成器没有结束的话,那result的value就应该是一个promise对象,那我们就可以使用这个对象的then方法去处理他的请求结果。

那在请求的这个回调当中我们继续使用next让我们的生成器函数继续往下执行,并且我们把这里得到的数据传递进去。

那这个next方法他返回的又会是下一个result,那我们应该将这个result再次交给当前这个handleResult函数进行递归。

那这样的话我们只需要在外界去调用一下我们这个handleResult然后传入我们第一次next的结果就可以了。

const g = gengertor()
function handleResult (result) {
    if (result.done) return

    result.value.then(data => {
        handleResult(g.next(data))
    })
}

handleResult(g.next())

那后面的话只要我们的生成器不结束,那这个递归就会一直执行下去,会把我们在这个生成器函数当中所有的这种异步调用全部依次执行下去。

当然这里我们还需要去处理一下当这个promise失败过后的处理逻辑,因为目前这种情况下我们根本没有考虑promise失败的问题,那一旦任何一个promise失败了,那这里就会出现一个未捕获的异常。

那其实处理失败也非常简单,我们可以直接在这个promise的then方法中去添加一个失败的回调,那在失败的回调当中我们就可以直接调用生成器对象的throw方法,让这个生成器函数他在继续执行时得到一个异常就可以了。

const g = gengertor()
function handleResult (result) {
    if (result.done) return

    result.value.then(data => {
        handleResult(g.next(data))
    }, error => {
        g.throw(error)
    })
}

handleResult(g.next())

那这样的话我们就可以在main函数的内部通过try catch的方式去捕获这个异常,那得到这个异常之后我们尝试把他打印出来。

我们故意将urls地址写错,测试一下。

function * main () {
    try {
        const users = yield ajax('/api/users.json')
        console.log(users)
        const posts = yield ajax('/api/posts.json')
        console.log(posts)
        const urls = yield ajax('/api/urls11.json')
        console.log(urls)
    } catch (e) {
        console.log(e)
    }
}

运行后可以发现这个异常被捕获到了。

那以上这样一个过程实际上我们就完成了生成器函数的一个执行器,那这样对于这个生成器函数的调用逻辑呢实际上是完全可以复用的,我们可以把它封装成一个公共的函数。

例如我们这里再去定义一个叫做co的函数,那这个函数内部我们去接收一个生成器函数,然后在内部我们按照刚刚的执行逻辑我们去对这个生成器函数去执行。

那这样一个co的函数在下一次你在使用到生成器函数去完成异步编程的时候就可以使用到。

function co (generator) {
    const g = generator()
    function handleResult (result) {
        if (result.done) return

        result.value.then(data => {
            handleResult(g.next(data))
        }, error => {
            g.throw(error)
        })
    }
    handleResult(g.next())
}

co(main)

当然像这样的生成器函数执行器呢在社区当中早就有一个完善的库,就叫做co (https://github.com/tj/co)

那你也可以自己尝试去使用一下,那这中co的异步方案呢实际上在15年之前是特别流行的,但是后来呢因为在语言本身有了async/await过后, 这种方案就相对来讲没有那么普及了。

不过呢使用Generator这个方案,他最明显的一个变化就是让我们异步调用再次回归到扁平化,这也是我们JavaScript异步编程发展过程当中很重要的一步。

所以说你不应该仅仅了解他的用法,还应该去理解他是如何工作的,当然了,我们在日后的开发过程中我们可能还是会选择最新的async/await的方式。

async/await

有了Generator过后,Javascript中的异步编程基本就已经与同步代码有类似的体验了,但是使用Generator这种异步方案我们还需要自己手动去编写一个执行器函数,就像我们在上一个例子当中定义的co函数一样。所以说会比较麻烦。

而在ECMAScript2017的标准当中,新增了一个叫做async的函数,那他同样提供了这种扁平化的异步编程体验。

而且呢他是语言层面标准的异步编程语法,所以说使用起来就会更加的方便一点。

那其实async函数就是生成器函数一种更方便的语法糖,所以说语法上async函数和Generator函数是非常类似的。

我们只需要把生成器函数修改成使用async关键词去修饰的普通函数,然后在内部素有的yield关键字我们把它替换成await就可以了。

// function * main () {
async function main () {
    try {
        // const users = yield ajax('/api/users.json')
        const users = await ajax('/api/users.json')
        console.log(users)
        // const posts = yield ajax('/api/posts.json')
        const posts = await ajax('/api/posts.json')
        console.log(posts)
        const urls = await ajax('/api/urls11.json')
        // const urls = yield ajax('/api/urls11.json')
        console.log(urls)
    } catch (e) {
        console.log(e)
    }
}

那这样的话,我们这样一个函数就是一个标准的async函数了,我们可以直接在外面去调用这个函数。

main()

那执行这个函数的话,内部这个执行过程呢他会跟我们刚刚的Generator函数是完全一样的,所以说他的效果也是完全一致的。

那相比于Generator,async函数最大的好处呢就是他不需要再去配合一个类似于co这样的执行器,因为他是语言层面的标准异步编程语法。

其次呢async函数可以给我们返回一个promise对象,那这样的话就更加利于我们对整体进行控制。

那除此之外呢关于async函数还有一个需要大家注意的点就是,async当中使用的这个await关键词他只能出现在async函数内部,他不能直接在外部,也就是最顶层作用域去使用。

不过呢,关于在最外层直接去使用await的功能呢现在已经在开发了,不久以后有可能会出现在标准当中,那到时候我们去使用async函数就会更加方便一些。

那以上就是我们对async函数基本的了解,那如果说你了解我们刚刚所说的生成器的异步方案,那async函数呢他只是写法上有略微的差异,其他所有的东西都是相同的,所以说他也没有什么太值得我们去深究的东西。