按照维基百科上的解释:独立于主控制流以外发生的事件就叫作异步。好比说有一段顺序执行的代码node
void function main() { fA(); fB(); }();
fA => fB 是顺序执行的,永远都是 fA 在 fB 的前面执行,他们就是 同步 的关系。加入这时使用 setTimeout
将 fA 延后git
void function main() { setTimeout(fA, 1000); fB(); }();
这时,fA 相对于 fB 就是异步的。main 函数只是声明了要在一秒后执行一次 fA,而并无马上执行它。这时,fA 的控制流就独立于 main 以外。github
由于 setTimeout
的存在,至少在被 ECMA 标准化的那一刻起,JavaScript 就支持异步编程了。与其余语言的 sleep
不一样,setTimeout
是异步的——它不会阻挡当前程序继续往下执行。编程
然而异步编程真正发展壮大,Ajax 的流行功不可没。Ajax 中的 A(Asynchronous)真正点到了异步的概念——这仍是 IE五、IE6 的时代。数组
异步任务执行完毕以后怎样通知开发者呢?回调函数是最朴素的,容易想到的实现方式。因而从异步编程诞生的那一刻起,它就和回调函数绑在了一块儿。promise
例如 setTimeout。这个函数会起一个定时器,在超过指定时间后执行指定的函数。好比在一秒后输出数字 1,代码以下:浏览器
setTimeout(() => { console.log(1); }, 1000);
常规用法。若是需求有变,须要每秒输出一个数字(固然不是用 setInterval),JavaScript 的初学者可能会写出这样的代码:babel
for (let i = 1; i < 10; ++i) { setTimeout(() => { // 错误! console.log(i); }, 1000); }
执行结果是等待 1 秒后,一次性输出了全部结果。由于这里的循环是同时启了 10 个定时器,每一个定时器都等待 1 秒,结果固然是全部定时器在 1 秒后同时超时,触发回调函数。koa
解法也简单,只须要在前一个定时器超时后再启动另外一个定时器,代码以下:异步
setTimeout(() => { console.log(1); setTimeout(() => { console.log(2); setTimeout(() => { console.log(3); setTimeout(() => { console.log(4); setTimeout(() => { console.log(5); setTimeout(() => { // ... }, 1000); }, 1000); }, 1000) }, 1000) }, 1000) }, 1000);
层层嵌套,结果就是这样的漏斗形代码。可能有人想到了新标准中的 Promise,能够改写以下:
function timeout(delay) { return new Promise(resolve => { setTimeout(resolve, delay); }); } timeout(1000).then(() => { console.log(1); return timeout(1000); }).then(() => { console.log(2); return timeout(1000); }).then(() => { console.log(3); return timeout(1000); }).then(() => { console.log(4); return timeout(1000); }).then(() => { console.log(5); return timeout(1000); }).then(() => { // .. });
漏斗形代码是没了,但代码量自己并没减小多少。Promise
并没能干掉回调函数。
由于回调函数的存在,循环就没法使用。不能循环,那么只能考虑递归了,解法以下:
let i = 1; function next() { console.log(i); if (++i < 10) { setTimeout(next, 1000); } } setTimeout(next, 1000);
注意虽然写法是递归,但因为 next
函数都是由浏览器调用的,因此实际上并无递归函数的调用栈结构。
不少语言都引入了协程来简化异步编程,JavaScript 也有相似的概念,叫作 Generator。
MDN 上的解释:Generator 是一种能够中途退出以后重入的函数。他们的函数上下文在每次重入后会被保持。简而言之,Generator
与普通 Function
最大的区别就是:Generator
自身保留上次调用的状态。
举个简单的例子:
function *gen() { yield 1; yield 2; return 3; } void function main() { var iter = gen(); console.log(iter.next().value); console.log(iter.next().value); console.log(iter.next().value); }();
代码的执行顺序是这样:
gen
,获得一个迭代器 iter
。注意此时并未真正执行 gen
的函数体。iter.next()
,执行 gen
的函数体。yield 1
,将 1 返回,iter.next()
的返回值即为 { done: false, value: 1 },输出 1iter.next()
。从上次 yield
出去的地方继续往下执行 gen
。yield 2
,将 2 返回,iter.next()
的返回值即为 { done: false, value: 2 },输出 2iter.next()
。从上次 yield
出去的地方继续往下执行 gen
。return 3
,将 3 返回,return
表示整个函数已经执行完毕。iter.next()
的返回值即为 { done: true, value: 3 },输出 3调用 Generator 函数只会返回一个迭代器,当用户主动调用了 iter.next()
后,这个 Generator 函数才会真正执行。
你可使用 for ... of
遍历一个 iterator,例如
for (var i of gen()) { console.log(i); }
输出 1 2
,最后 return 3
的结果不算在内。想用 Generator
的各项生成一个数组也很简单,Array.from(gen())
或直接用 [...gen()]
便可,生成 [1, 2]
一样不包含最后的 return 3
。
Generator 也叫半协程(semicoroutine),天然与异步关系匪浅。那么 Generator 是异步的吗?
既是也不是。前面提到,异步是相对的,例如上面的例子
function *gen() { yield 1; yield 2; return 3; } void function main() { var iter = gen(); console.log(iter.next().value); console.log(iter.next().value); console.log(iter.next().value); }();
咱们能够很直观的看到,gen 的方法体与 main 的方法体在交替执行,因此能够确定的说,gen 相对于 main 是异步执行的。然而此段过程当中,整个控制流都没有交回给浏览器,因此说 gen 和 main 相对于浏览器是同步执行的。
回到最初的问题:
for (let i = 0; i < 10; ++i) { setTimeout(() => { console.log(i); }, 1000); // 等待上面 setTimeout 执行完毕 }
关键在于如何等待前面的 setTimeout
触发回调后再执行下一轮循环。若是使用 Generator
,咱们能够考虑在 setTimeout
后 yield
出去(控制流返还给浏览器),而后在 setTimeout
触发的回调函数中 next
,将控制流交还回给代码,执行下一段循环。
let iter; function* run() { for (let i = 1; i < 10; ++i) { setTimeout(() => iter.next(), 1000); yield; // 等待上面 setTimeout 执行完毕 console.log(i); } } iter = run(); iter.next();
代码的执行顺序是这样:
run
,获得一个迭代器 iter
。注意此时并未真正执行 run
的函数体。iter.next()
,执行 run
的函数体。setTimeout
,启动一个定时器,回调函数延后 1 秒执行。yield
(即 yield undefined
),控制流返回到最后的 iter.next()
以后。由于后面没有其余代码了,浏览器得到控制权,响应用户事件,执行其余异步代码等。setTimeout
超时,执行回调函数 () => iter.next()
。iter.next()
。从上次 yield
出去的地方继续往下执行,即 console.log(i)
,输出 i 的值。这样即实现了相似同步 sleep 的要求。
上面的代码毕竟须要手工定义迭代器变量,还要手工 next
;更重要的是与 setTimeout
紧耦合,没法通用。
咱们知道 Promise
是异步编程的将来。能不能把 Promise
和 Generator
结合使用呢?这样考虑的结果就是 async 函数。
用 async
获得代码以下
function timeout(delay) { return new Promise(resolve => { setTimeout(resolve, delay); }); } async function run() { for (let i = 1; i < 10; ++i) { await timeout(1000); console.log(i); } } 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 执行结束 doSomething(a, b);
我我的比较喜欢以下写法:
const [ a, b ] = await Promise.all([ queryA(), queryB() ]); doSomething(a, b);
将 await
和 Promise
结合使用,效果更佳!
现在 async
函数已经被各大主流浏览器实现(除了 IE)。若是要兼容旧版浏览器,可使用 babel
将其编译为 Generator
。若是还要兼容只支持 ES5 的浏览器,还能够继续把 Generator
编译为 ES5
。编译后的代码量比较大,当心代码膨胀。
若是是用 node 写 Server,那就不用纠结了直接用就是了。koa 是用 async
是你的好帮手。