JavaScript异步编程--Generator

281 阅读3分钟

Generator

生成器函数和普通的函数差别不大,就是在function 字段后加一个星号(function * foo(){}),在生成器函数内部可以随时使用yield返回一个值,yield不像return那样停止函数的执行,而是暂时停止执行,当我们再调用next函数时继续往下执行。 当我们在调用生成器函数时,函数并不会立即执行,而是给我们返回一个生成器对象,当我们去调用生成器对象的next函数时,函数才会执行,执行后得到返回的值,这个值是一个对象,value是具体返回的值内容,done标识这个生成器是否全部执行完了。同时生成器对象有throw方法抛出异常,当我们在外部调用throw方法时也会让生成器函数内部的yield往下执行,只不过执行的是捕获异常。

示例:

    function * foo() {
        console.log('begin')
        
        try {
            yield 'foo' // 返回foo
        } catch (e) {
            console.log(e)
        }
    }
    
    const generator = foo() // 返回生成器对象
    
    // 调用next函数,foo中执行到yield并返回foo
    const res = generator.next()
    console.log(res) // =>{ value: 'foo', done: true}
    
    // 调用throw方法抛出异常,此时生成器函数内部将捕获这个异常
    generator.throw(new Error('generator error'))

经过上边我们知道yield可以暂停函数内部的执行,那我们就可以通过yield来实现更优的异步编程体验。

示例:

    // 定义一个生成器
    function * main() {
        const users = yield ajax('/api/users.json')
        console.log(users)
        
        const posts = yield ajax('/api/posts.json')
        console.log(post)
    }
    
    // 调用生成器函数得到生成器对象
    const g = main()
    
    // 调用next函数
    // 此时res为 { value: Promise, done: false }
    const res = g.next()
    // 我们在调用value中保存的Promise得到ajax返回的数据
    res.value.then(data => {
        // 此时再用next并把data作为参数传递进去,此时users当中保存的就是data
        const res2 = g.next(data)
        // 此处我们通过done属性判断生成器函数是否执行完成
        if (res2.done) return
        res2.value.then(data => {
            g.next()
        })
    })

由上我们可以看出我们可以通过递归的方式不断去迭代,知道我们生成器返回的done为true结束递归,那我们来实现一下。

示例:

    function * main() {
        try {
            const users = yield ajax('/api/users.json')
            console.log(users)
            
            const posts = yield ajax('/api/posts.json')
            console.log(post)
        } catch(e) {
            console.log(e)
        }
    }
    
    // 调用生成器函数得到生成器对象
    const g = main()
    
    function handleResult(result) { // 传入的result为生成器返回的结果
        // 首先判断下生成器函数是否执行完成
        if (result.done) return
        // 如果没执行完就继续执行
        result.value.then(data => {
            // 继续调用handleResult
            handleResult(g.next())
        }, err => {
            // 如果异步操作发生异常我们进行捕获并调用生成器对象的throw方法抛出异常
            g.throw(err)
        })
    }
    
    // 使用
    handleResult(g.next())

其实我们还可以将上边的获取生成器对象以及调用递归函数封装到一个新的函数中供其他地方使用。

示例:

    function * main() {
        try {
            const users = yield ajax('/api/users.json')
            console.log(users)
            
            const posts = yield ajax('/api/posts.json')
            console.log(post)
        } catch(e) {
            console.log(e)
        }
    }
    
    function co(generator) {
        const g = generator()
    
        function handleResult(result) { 
            if (result.done) return
            result.value.then(data => {
                handleResult(g.next())
            }, err => {
                g.throw(err)
            })
        }
        handleResult(g.next())
    }
    
    // 使用
    co(main)

Async/Await 语法糖

有了生成器之后我们发现操作异步调用变得扁平化,但是我们也发现我们在使用时需要自己封装co函数。在ES7中新增了Async和Await语法,它是语言层面异步编程规范的标准,使用起来更方便。

示例:只需要去掉星号,在function前添加async并把yield替换成await即可

    async function main() {
        try {
            const users = await ajax('/api/users.json')
            console.log(users)
            
            const posts = await ajax('/api/posts.json')
            console.log(post)
        } catch(e) {
            console.log(e)
        }
    }
    
    // 使用
    // 此时给我们返回是一个promise
    const promise = main()
    promise.then(() => {
        // do something
    })

async/await是Generator的语法糖,在使用await时必须配合async使用