按照[維基百科](https://en.wikipedia.org/wiki/Asynchrony_(computer_programming%29)上的解釋:獨立於主控制流以外發生的事件就叫作異步。好比說有一段順序執行的代碼node
void function main() { fA(); fB(); }();
fA => fB 是順序執行的,永遠都是 fA 在 fB 的前面執行,他們就是 同步 的關係。加入這時使用 setTimeout
將 fB 延後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 的時代。segmentfault
異步任務執行完畢以後怎樣通知開發者呢?回調函數是最樸素的,容易想到的實現方式。因而從異步編程誕生的那一刻起,它就和回調函數綁在了一塊兒。數組
例如 setTimeout。這個函數會起一個定時器,在超過指定時間後執行指定的函數。好比在一秒後輸出數字 1,代碼以下:promise
setTimeout(() => { console.log(1); }, 1000);
常規用法。若是需求有變,須要每秒輸出一個數字(固然不是用 setInterval),JavaScript 的初學者可能會寫出這樣的代碼:瀏覽器
for (let i = 1; i < 10; ++i) { setTimeout(() => { // 錯誤! console.log(i); }, 1000); }
執行結果是等待 1 秒後,一次性輸出了全部結果。由於這裏的循環是同時啓了 10 個定時器,每一個定時器都等待 1 秒,結果固然是全部定時器在 1 秒後同時超時,觸發回調函數。babel
解法也簡單,只須要在前一個定時器超時後再啓動另外一個定時器,代碼以下: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
是你的好幫手。
本文亦發表於 segmentfault:http://www.javashuo.com/article/p-alerdqdy-mv.html