探索javascrip-asyncawait

56次阅读

共计 5887 个字符,预计需要花费 15 分钟才能阅读完成。

在讲 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 的一个语法糖

正文完
 0