【引自老文章的部落格】什麼是異步(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