探索javascript -> async、await

86193951 2019-11-03

在讲async之前,先简单的提一下promise。
首先,先来纠正一下很多人普遍的错误观点 --> 'promise是异步的', 看代码:

console.log(1);
let p1 = new Promise(r => { console.log(2); r() });
p1.then(_ => console.log(3));
console.log(4);
let p2 = new Promise(r => { console.log(5); r() });
p2.then(_ => console.log(6));
console.log(7);
// 打印 1 2 4 5 7 3 6

从打印结果来看,我们就可以断定promise是同步的,那么我就说promise是同步的,then是异步的!也不是,简单说一下原因:
先说结果:promise的then也是同步的。这样输出的原因在于,在new promise(fn) fn的r() 函数是异步的会挂起线程,执行到then的时候,then中的代码块会马上开始执行(注意我说地是开始执行),只是把成功的回调函数放到了resovledCallbacks中,但是就算状态修改完毕为fulfiled的时候,上面的执行 then(fn)中 fn里面的代码执行是会异步操作,也不是立即执行console 因为then的内部实现方式根据promisA规范中也是有一个settimeout 在延时器内部执行aaa的 所以then方法肯定同步函数 但是其实表现的永远都是异步 因为两个settimeout都保证它是异步去执行成功或失败的回调函数的,说具体点其实是r()内部设置了一个延时执行回调,延时setTimeout的最小值,也就是说r才是异步的,再看

console.log(1);
let p1 = new Promise(r => { console.log(2); r() });
p1.then(console.log(3));
console.log(4);
let p2 = new Promise(r => { console.log(5); r() });
p2.then(console.log(6));
console.log(7);

// 打印 1 2 3 4 5 6 7

明白了吧?
对于它所解决的问题主要可以总结成:

  • 回调地狱,代码难以维护, 常常第一个的函数的输出是第二个函数的输入这种现象
  • promise可以支持多个并发的请求,获取并发请求中的数据
  • promise可以解决异步的问题,本身不能说promise是异步的

promise就不再多介绍了,有时间大家可以再深入研究一下
话说回来,promise真的完全解决了callback hell吗?
先来一个场景有四个函数,要求 按顺序执行,也就是需要等到前一个promise full之后才能运行,且后一个promise是需要用到上一个promise所返回的值,比如

function f1() {
    return new Promise(resolve => {
        setTimeout(_ => resolve('f1'), 500)
    })
}

function f2(params) {
    return new Promise(resolve => {
        console.log(params);
        setTimeout(_ => resolve(params + 'f2'), 500)
    })
}

function f3(params) {
    return new Promise(resolve => {
        console.log(params);
        setTimeout(_ => resolve(params + 'f3'), 500)
    })
}

function f4(params) {
    return new Promise(resolve => {
        console.log(params);
        setTimeout(_ => resolve(params + 'f4'), 500)
    })
}

我们一般都会这样写

f1().then(res => {
    return f2(res)
}).then(res => {
    return f3(res)
}).then(res => {
    return f4(res)
});

或者再精简一下

f1().then(f2).then(f3).then(f4);

虽然看上去美观了不少,但是也存在一些问题,比如如果不用第一种方法来写,用第二种,那么可以知道它的可读性很差,我们 单看f1().then(f2).then(f3).then(f4);这段代码其实是完全看不出f1,f2,f3,f4到底有什么联系,也更读不出f2,f3,f4都用了上一层的输出作为输入,最理想的表达我认为应该这样

f1();
f2();
f3();
f4();

不过,如果这样那就不能保证我们的函数是按照顺序依次执行了更别说输入输出联系起来。
这样,我们的async登场
于是,你可以这样写

void (async function() {
    let r1 = await f1()
    let r2 = await f2(r1)
    let r3 = await f3(r2)
    await f4(r3)
})();

怎样,是不是简单明了,简单介绍一下:
ES7 提出的async 函数,终于让 JavaScript 对于异步操作有了终极解决方案。No more callback hell。
async 函数是 Generator 函数的语法糖。使用 关键字 async 来表示,在函数内部使用 await 来表示异步。
想较于 Generator,Async 函数的改进在于下面四点(这四段是我在别的地方找到的,总结的也很好):

  • 内置执行器。Generator 函数的执行必须依靠执行器,而 Aysnc 函数自带执行器,调用方式跟普通函数的调用一样
  • 更好的语义。async 和 await 相较于 * 和 yield 更加语义化
  • 更广的适用性。co 模块约定,yield 命令后面只能是 Thunk 函数或 Promise对象。而 async 函数的 await 命令后面则可以是 Promise 或者 原始类型的值(Number,string,boolean,但这时等同于同步操作)
  • 返回值是 Promise。async 函数返回值是 Promise 对象,比 Generator 函数返回的 Iterator 对象方便,可以直接使用 then() 方法进行调用
    (co模块其实就是将Generator和Promise结合起来,自动执行Generator)

asynch函数也有返回值,是一个promise对象,所以我们可以用.then

async function f() {
    return 1
}
console.log(f())     // Promise { 1 }

但是要注意,如果函数执行过程中遇到了await就会先返回,我们再看

async function f() {
    await 2;
    return 1
}
console.log(f()); // Promise { <pending> }

虽然我代码中reutnr 1但是可以看到结果却是返回了一个pending状态的Promise对象,其中async函数内部return语句返回的值,会成为then方法回调函数的参数

async function f() {
    await 2;
    return 1
}
f().then(res => {
    console.log(res) // 1
})

值得注意的是我一直在强调函数执行,想要表达的就是虽然await是等待执行的意思,但是也并不会对外部产生有副作用的影响

async function f() {
    await 2;
    console.log('a')
    return 1
}
f()
console.log('b') // b  a

从打印结果上我们看到了虽然程序执行中遇到了await但是它并没有阻塞到外部的代码执行,所以说还是没有改变Javascript异步的本质,不过至少我们可以在async函数中去很好地控制我们的流程,我们来看一道题来瞧瞧这个语法糖的强大之处。
目标 : 发送30次ajax请求,要求30个请求是串行的(即发送请求时必须等待前一个请求res)
这道题如果我们用常规的promisepromise.then的方法实现起来会有一些难度,我们先模拟一个ajax请求,假定每个ajax的timeresponse都是400ms ->

function ajax(n) {
    return new Promise((rs, rj) => {
        setTimeout(() => {
            console.log(n);
            rs()
        }, 400)
    })
}

Promise实现:

let n = 50
let task = ajax(n);
function run() {
    task.then(_ => {
        --n && (task = ajax(n)) && run()
    })
}
run();

Generator实现

let num = 50;

function* Ge() {
    while (true) {
        yield ajax(num)
    }
}
let re = Ge()
function run() {
    let result = re.next().value
    result.then(_ => {
        num-- && run()
    })
}
run()

async实现

let n = 50
async function run() {
    while (n--) await ajax(n)
}
run()

做个对比之后一目了然
刚才说async其实就是Generator的语法糖
肯定有人会好奇async是怎样的实现原理,想要理解它,还是得学习生成器(generator)。毕竟async只是generator的语法糖,跳过它直接学习async当然会错过很多。async 就等于Generator+自动执行器。
话题回到前边的例子

void (async function() {
        let r1 = await f1()
        let r2 = await f2(r1)
        let r3 = await f3(r2)
        await f4(r3)
    })();

我们说过async中如果遇到await的话就会等待后边的Promise返回结果(同步除外),所以上面的代码中的执行顺序是f1->f2->f3,那这样就带来一个问题,我们要向让f1,2,3同时并发执行怎么办?
我们知道Promise是同步的,当我们new Promise(...)的时候,事实上是已经开始执行了,只不过返回结果是一个带状态的P,那我们如果想让f1,2,3并行的话也就有办法了

void (async function() {
    let r1 = new Promise(...)
    let r2 = new Promise(...)
    let r3 = new Promise(...)
    await r1
    await r2
    await r3
})();

这就相当于new Promise中的代码块是同时进行的,至于状态由pending变成full的时间长短由业务需求以及场合来决定,另一种方法可能会更加直观一些

void (async function() {
    let re = await Promise.all([p1, p2, p3])
})();

其中re为一个数组,值分别对应p1, 2, 3;换做race当然也可以Promise.race([p1, p2, p3])

如果请求多的话,我们也可以使用map, foreach并行执行

function plist(n) {
    return new Promise(resolve => {
        console.log('start:' + n)
        setTimeout(_ => {
            resolve(n)
        }, 2000)
    })
}

let c = [...new Array(100).keys()]
let pros = c.map(async n => {
    return await plist(n)
})
for (let p of pros) {
    p.then(res => console.log('end:' + res))
}

map与forEach 都是并行执行promise数组,但for-in for-of for都是串行的。知道这两点我们可以高效的处理很多异步请求。
最后简单地说下async的错误处理方式
我们都知道在promise中的异常或者reject都是无法通过try catch来捕获,例如

try {
    Promise.reject('an normal error')
} catch (e) {
    console.log('error comming')
    console.log(e)
}

这个错误try catch是捕获不到的,会报一个UnhandledPromiseRejectionWarning的未捕获reject的错误描述,再比如
function fn() {

try {
        new Promise(resolve => {
            JSON.parse(a)
            resolve()
        })
    } catch (e) {
        console.log('error comming')
        console.log(e)
    }
}

fn()

这里直接抛出ReferenceError异常,我们再把它放在async中

async function fn() {
    try {
        await new Promise(resolve => {
            JSON.parse(a)
            resolve()
        })
    } catch (e) {
        console.log('error comming')
        console.log(e)
    }
}

fn()

神奇了,异常竟然被捕获了,其实这个地方我也不是很确定其真正的原因,我觉得重点其实就在于await做了什么,对执行环境产生了什么影响,先说一下我的观点 因为promise是非阻塞的也就是说对于promise外部的try,catch来说,内部的promise属于异步执行,而try cathch是无法捕获异步错误的, 而await表示等待promise执行,等待这个承诺的执行结果, 并暂停当前async执行环境的代码执行,也就是说在async下,await我们甚至可以认为它是同步的,阻塞的!所以我们可以认为这个错误是同步抛出也就是(await new Promise(...))抛出的,所以会被捕获。
不过,我却不建议用这种方式来捕获async中的异常,一是代码结构看起来混乱,二是如果try/catch的catch部分有异常,我们应该如何处理呢?所以我建议用async().catch来处理,因为async不管有没有返回值,都是返回一个promise对象

async function fn() {

}
console.log(fn().then)    // [Function ...]

并且async也可以使用return 来返回一个promise

async function fn() {
    // return await Promise.resolve(1)
    // return Promise.resolve(1)
}

关于async先简单介绍到这里,下一篇文章我会讲一下Generator,到时候让大家知道为什么我们用 Generator 很少或者为什么说 async是 Generator的一个语法糖

相关推荐