【引自老帖子的博客】什么是异步(asynchrony)
按照维基百科上的解释:独立于主控制流之外发生的事件就叫做异步。比如说有一段顺序执行的代码
void function main() {
fa();
fb();
}();
fa => fb 是顺序执行的,永远都是 fa 在 fb 的前面执行,他们就是 同步 的关系。加入这时使用 settimeout 将 fb 延后
settimeout(fa, 1000);
这时,fa 相对于 fb 就是异步的。main 函数只是声明了要在一秒后执行一次 fa,而并没有立刻执行它。这时,fa 的控制流就独立于 main 之外。
javascript——天生异步的语言
因为 settimeout 的存在,至少在被 ecma 标准化的那一刻起,javascript 就支持异步编程了。与其他语言的 sleep 不同,settimeout 是异步的——它不会阻挡当前程序继续往下执行。
然而异步编程真正发展壮大,ajax 的流行功不可没。ajax 中的 a(asynchronous)真正点到了异步的概念——这还是 ie5、ie6 的时代。
回调函数——异步编程之痛
异步任务执行完毕之后怎样通知开发者呢?回调函数是最朴素的,容易想到的实现方式。于是从异步编程诞生的那一刻起,它就和回调函数绑在了一起。
例如 settimeout。这个函数会起一个定时器,在超过指定时间后执行指定的函数。比如在一秒后输出数字 1,代码如下:
settimeout(() => {
console.log(1);
}, 1000);
常规用法。如果需求有变,需要每秒输出一个数字(当然不是用 setinterval),javascript 的初学者可能会写出这样的代码:
for (let i = 1; i < 10; ++i) {
settimeout(() => { // 错误!
console.log(i);
}, 1000);
}
执行结果是等待 1 秒后,一次性输出了所有结果。因为这里的循环是同时启了 10 个定时器,每个定时器都等待 1 秒,结果当然是所有定时器在 1 秒后同时超时,触发回调函数。
解法也简单,只需要在前一个定时器超时后再启动另一个定时器,代码如下:
settimeout(() => {
console.log(2);
settimeout(() => {
console.log(3);
settimeout(() => {
console.log(4);
settimeout(() => {
console.log(5);
settimeout(() => {
// ...
}, 1000);
}, 1000);
}, 1000)
}, 1000)
}, 1000)
层层嵌套,结果就是这样的漏斗形代码。可能有人想到了新标准中的 promise,可以改写如下:
function timeout(delay) {
return new promise(resolve => {
settimeout(resolve, delay);
});
timeout(1000).then(() => {
return timeout(1000);
}).then(() => {
console.log(2);
console.log(3);
console.log(4);
console.log(5);
// ..
});
漏斗形代码是没了,但代码量本身并没减少多少。promise 并没能干掉回调函数。
因为回调函数的存在,循环就无法使用。不能循环,那么只能考虑递归了,解法如下:
let i = 1;
function next() {
console.log(i);
if (++i < 10) {
settimeout(next, 1000);
}
settimeout(next, 1000);
注意虽然写法是递归,但由于 next 函数都是由浏览器调用的,所以实际上并没有递归函数的调用栈结构。
generator——javascript 中的半协程
很多语言都引入了协程来简化异步编程,javascript 也有类似的概念,叫做 generator。
mdn 上的解释:generator 是一种可以中途退出之后重入的函数。他们的函数上下文在每次重入后会被保持。简而言之,generator 与普通 function 最大的区别就是:generator 自身保留上次调用的状态。
举个简单的例子:
function *gen() {
yield 1;
yield 2;
return 3;
var iter = gen();
console.log(iter.next().value);
代码的执行顺序是这样:
请求 gen,得到一个迭代器 iter。注意此时并未真正执行 gen 的函数体。
调用 iter.next(),执行 gen 的函数体。
遇到 yield 1,将 1 返回,iter.next() 的返回值即为 { done: false, value: 1 },输出 1
调用 iter.next()。从上次 yield 出去的地方继续往下执行 gen。
遇到 yield 2,将 2 返回,iter.next() 的返回值即为 { done: false, value: 2 },输出 2
遇到 return 3,将 3 返回,return 表示整个函数已经执行完毕。iter.next() 的返回值即为 { done: true, value: 3 },输出 3
调用 generator 函数只会返回一个迭代器,当用户主动调用了 iter.next() 后,这个 generator 函数才会真正执行。
你可以使用 for ... of 遍历一个 iterator,例如
for (var i of gen()) {
输出 1 2,最后 return 3 的结果不算在内。想用 generator 的各项生成一个数组也很简单,array.from(gen()) 或直接用 [...gen()] 即可,生成 [1, 2] 同样不包含最后的 return 3。
generator 是异步的吗
generator 也叫半协程(semicoroutine),自然与异步关系匪浅。那么 generator 是异步的吗?
既是也不是。前面提到,异步是相对的,例如上面的例子
我们可以很直观的看到,gen 的方法体与 main 的方法体在交替执行,所以可以肯定的说,gen 相对于 main 是异步执行的。然而此段过程中,整个控制流都没有交回给浏览器,所以说 gen 和 main 相对于浏览器是同步执行的。
用 generator 简化异步代码
回到最初的问题:
for (let i = 0; i < 10; ++i) {
// 等待上面 settimeout 执行完毕
关键在于如何等待前面的 settimeout 触发回调后再执行下一轮循环。如果使用 generator,我们可以考虑在
settimeout 后 yield 出去(控制流返还给浏览器),然后在 settimeout 触发的回调函数中
next,将控制流交还回给代码,执行下一段循环。
let iter;
function* run() {
for (let i = 1; i < 10; ++i) {
settimeout(() => iter.next(), 1000);
yield; // 等待上面 settimeout 执行完毕
iter = run();
iter.next();
请求 run,得到一个迭代器 iter。注意此时并未真正执行 run 的函数体。
调用 iter.next(),执行 run 的函数体。
循环开始,i 初始化为 1。
执行 settimeout,启动一个定时器,回调函数延后 1 秒执行。
遇到 yield(即 yield undefined),控制流返回到最后的 iter.next() 之后。因为后面没有其他代码了,浏览器获得控制权,响应用户事件,执行其他异步代码等。
1 秒后,settimeout 超时,执行回调函数 () => iter.next()。
调用 iter.next()。从上次 yield 出去的地方继续往下执行,即 console.log(i),输出 i 的值。
一次循环结束,i 自增为 2,回到第 4 步继续执行
……
这样即实现了类似同步 sleep 的要求。
async、await——用同步语法写异步代码
上面的代码毕竟需要手工定义迭代器变量,还要手工 next;更重要的是与 settimeout 紧耦合,无法通用。
我们知道 promise 是异步编程的未来。能不能把 promise 和 generator 结合使用呢?这样考虑的结果就是 async 函数。
用 async 得到代码如下
async function run() {
await timeout(1000);
run();
按照 chrome 的设计文档,async 函数内部就是被编译为 generator 执行的。run 函数本身会返回一个
promise,用于使主调函数得知 run 函数什么时候执行完毕。所以 run() 后面也可以 .then(xxx),甚至直接 await
run()。
注意有时候我们的确需要几个异步事件并行执行(比如调用两个接口,等两个接口都返回后执行后续代码),这时就不要过度使用 await,例如:
const a = await querya(); // 等待 querya 执行完毕后
const b = await queryb(); // 执行 queryb
dosomething(a, b);
这时 querya 和 queryb 就是串行执行的。可以略作修改:
const promisea = querya(); // 执行 querya
const b = await queryb(); // 执行 queryb 并等待其执行结束。这时同时 querya 也在执行。
const a = await promisea(); // 这时 queryb 已经执行结束。继续等待 querya 执行结束
我个人比较喜欢如下写法:
const [ a, b ] = await promise.all([ querya(), queryb() ]);
将 await 和 promise 结合使用,效果更佳!
结束语
如今 async 函数已经被各大主流浏览器实现(除了 ie)。如果要兼容旧版浏览器,可以使用 babel 将其编译为
generator。如果还要兼容只支持 es5 的浏览器,还可以继续把 generator 编译为 es5。编译后的代码量比较大,小心代码膨胀。
如果是用 node 写 server,那就不用纠结了直接用就是了。koa 是用 async 是你的好帮手。
作者:老帖子
来源:51cto