Skip to content

从Promise到Generator和async&await

概念

Promise

一个 Promise 对象代表一个在这个 promise 被创建出来时不一定已知的值。它让您能够把异步操作最终的成功返回值或者失败原因和相应的处理程序关联起来。 这样使得异步方法可以像同步方法那样返回值:异步方法并不会立即返回最终的值,而是会返回一个 promise,以便在未来某个时候把值交给使用者。

如果没有Promise,我们的代码类似这样:

console.log(1)
ajax({
    method: 'get',
    url: '/static/test.json',
    success:function(res){
        console.log(2)
        if(res.success){
            $.ajax({
                method: 'get',
                url: '/static/test.json',
                success:function(res){
                    console.log(3)
                    // ..
                }
            })
        }
    }
})
console.log(4)
1
4
2
3

有了Promise我们的代码类似这样:

console.log(1)
ajax({
    method: 'get',
    url: '/static/test.json'
}).then(res=>{
    console.log(2)
    return ajax({
        method: 'get',
        url: '/static/test.json'
    })
}).then(res=>{
    console.log(3)
    //...
})
console.log(4)
1
4
2
3

简单来说,promise做的事情,就是将我们从层层回调的写法中解脱出来转成一种同步方法的写法来构建逻辑,但是,实际上,它还是一个异步!!! 只是会将异步的结果通过then进行传递,并不会改变改变执行的顺序和时间。

ES7 async和await ,作为ES6 genertor函数语法糖,在使用上比generator函数方便的,Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。

Generator

Generator函数是一个可以控制的多步执行并且每一步执行都能返回结果的函数:

声明一个Generator函数:

function* genFn(x) {
    yield x + 1;
    yield x + 2;
    return x + 3
}
let gVm = genFn(10);
console.log(gVm ,typeof gVm) // 2. Generator.html:18 genFn {<suspended>} "object"

和正常函数返回结果不一样,Generator函数执行会返回一个对象,对象有三个方法:

  • next: 执行函数,遇到yield,并返回后面表达式的结果作为value。需要特别注意的是遇到yield,所以next次数会比yieldreturn之前)多一次,并且调用时候传入的值会作为暂停处的yield表达式的值。 如上面的例子中,第一次调用next(),从函数体开头执行,然后x+1,继续执行碰到yield暂停,返回yield后面的表达式结果x+1
    let next1 = gVm.next('next1');
    console。log(next1);//{value: 11, done: false}
    第二次调用next,需要注意的是,上一次调用next,我们只执行到yield的右侧的x+1就停止了,所以这次开始,首先从yield 关键字开始执行,如果next传递了参数,那么就会将传递的参数当作yield的结果赋值,如gVm.next('next2')中就将'next1'赋给了 let next1 = yield x + 1;中的next1
            function* genFn(x) {
                let next1 = yield x + 1;
                console.log(next1)
                let next2 = yield x + 2;
                return x + 3
            }
            let gVm = genFn(10);
            let next1 = gVm.next('next1');
            let next2 = gVm.next('next2');
            let next3 = gVm.next('next3');
            console.log(next1,next2,next3)
  • return:直接结果Generator函数,将返回值的done编程true,并返回传入值作为value,后续调用next,因为状态已经执行完毕,所以无效。
  • throw:抛出一个错误。

因为这种逐步性,如果我们使用来控制ajax等异步操作的时候的,可以这么写:

function* genFn(data) {
    let result1 = yield ajax('./test1.json')
    let result2 = yield ajax('./test2.json')
    return [result1, result2]
}

看上去是同步代码,实际上执行还是异步,但是根据这个,我们能控制genFn 以一种类似同步的顺序进行执行:

通常我们一个通过回调的ajax方法类似如这样:

js
function ajax(url, cb) {
    let xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.send();
    xhr.onreadystatechange = function () {
        if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
            cb && typeof cb  === 'function' &&  cb(JSON.parse(xhr.responseText))
        }
    }
}

我们通过回调方式启动next:

js
        function* genFn(data) {
            console.log("start")
            let result1 = yield ajax('./test1.json', function (data) {
                console.log('ajax1 cb')
                gVm.next(data);
            })
            console.log('result1', result1)
            let result2 = yield ajax('./test2.json', function (data) {
                console.log('ajax2 cb')
                gVm.next(data)
            })
            console.log('result2', result2)
            console.log("end")
        }
        console.log(1)
        let gVm = genFn(10);
        console.log(2)
        let next1 = gVm.next();
        console.log(3)
        // 1
        // 2
        // start
        // 3
        // result1 cb
        // result1 {success: 0, data: Array(0)}
        // result2 cb
        // result2 {success: 0, data: Array(0)}
        // end

从结果1>2>start>3,这里的ajax请求依然是异步,但是start> 'ajax1 cb' > result1 > ajax2 cb > result2 > end genFn函数内部却是以一种同步的顺序进行执行。

Generator + Promise

基于promise,我们可以使用另一种更好的写法:

js
 function ajax(url) {
            return new Promise((resolve, reject) => {
                let xhr = new XMLHttpRequest();
                xhr.open('GET', url, true);
                xhr.send();
                xhr.onreadystatechange = function () {
                    if (xhr.readyState === XMLHttpRequest.DONE) {
                        if (xhr.status === 200) {
                            resolve(JSON.parse(xhr.responseText))
                        } else {
                            reject(JSON.parse(xhr.responseText))
                        }
                    }
                }
            })

        }

        function* genFn(data) {
            console.log("start")
            let result1 = yield ajax('./test1.json')
            console.log('result1', result1)
            let result2 = yield ajax('./test2.json')
            console.log('result2', result2)
            console.log("end")
        }
        console.log(1)
        let gVm = genFn(10);
        console.log(2)
        let next1 = gVm.next();
        next1.value.then(res => {
            console.log('next1 cb')
            return gVm.next(res).value
        }).then(res => {
            console.log('next2 cb')
            return gVm.next(res).value
        })
        console.log(3)

结果第一种一致,但是更符合我们的阅读和编程习惯。

在多条请求的情况这种方式就狠高效了,我们修改一下程序,阻塞停止的地方换成赋值操作,而不是发送请求:

js
    function* genFn(data) {
        console.log("start")
        let ajax1 = ajax('./test1.json')
        let ajax2 = ajax('./test2.json')
        let result1 = yield ajax1;
        console.log('result1', result1)
        let result2 = yield ajax2;
        console.log('result2', result2)
        console.log("end")
    }

在后面的请求不是基于前面请求结果的情况下,我们这样反处理,两个请求顺序发出,同时请求不阻塞,但是阻塞赋值的先后。

这种写法已经很完美了,但是需要我们自己拿到返回的promise进行手动处理,判断成功和失败的数据返回。在es7中提供了更甜的棒棒糖async/await

async/await

有了async/await,上述代码我们的可以改写为:

需要特别注意的是,await只能在async关键字修饰的函数内部使用,并且await只能接口promise.

js
    // 等待请求
    async function genFn(data) {
        console.log("start")
        let result1 = await ajax('./test1.json')
        console.log('result1', result1)
        let result2 = await ajax('./test2.json')
        console.log('result2', result2)
        console.log("end")
    }
    // 等待赋值
    async function genFn(data) {
        console.log("start")
        let ajax1 = ajax('./test1.json')
        let ajax2 = ajax('./test2.json')
        let result1 = await ajax1;
        console.log('result1', result1)
        let result2 = await ajax2;
        console.log('result2', result2)
        console.log("end")
    }

    console.log(1)
    let gVm = genFn(10);
    console.log(2)

async/await使用起来,效果和上述仍然一致,阻塞内部的调用顺序,将异步转为一种类似同步的执行顺序,但是比起上面的方法,从可读性和语义化上大大优化了,更少了需要热弄处理promise的返回值处理,大大减少了代码量,但是是否优于Generator?两者之间并没有优劣之分:

  • async/await在处理promise的层面面上更加快捷和方便,但是如果需要处理其余的情况的(数组,对象...),需要手动包装这些值为promise才能使用。
  • Generator/yield容性更广泛,并没有限制接收类。

关于async的介绍,在阮一峰老师的ES6入门教程中说到:

async 函数是什么?一句话,它就是 Generator 函数的语法糖。

将 async/await 转成 generator 和 promise 来实现:

js
let test = function () {
  // ret 为一个Promise对象,因为ES6语法规定 async 函数的返回值必须是一个 promise 对象
  let ret = _asyncToGenerator(function* () {
    for (let i = 0; i < 10; i++) {
      let result = yield sleep(1000);
      console.log(result);
    }
  });
  return ret;
}();

// generator 自执行器
function _asyncToGenerator(genFn) {
  return new Promise((resolve, reject) => {
    let gen = genFn();
    function step(key, arg) {
      let info = {};
      try {
        info = gen[key](arg);
      } catch (error) {
        reject(error);
        return;
      }
      if (info.done) {
        resolve(info.value);
      } else {
        return Promise.resolve(info.value).then((v) => {
          return step('next', v);
        }, (error) => {
          return step('throw', error);
        });
      }
    }
    step('next');
  });
}