一般,代碼是由上往下依次執行的。若是有多個任務,就必需排隊,前一個任務完成,後一個任務纔會執行。這種執行模式稱之爲: 同步(synchronous) 。新手容易把計算機用語中的同步,和平常用語中的同步弄混淆。如,「把文件同步到雲端」中的同步,指的是「使...保持一致」。而在計算機中,同步指的是任務從上往下依次執行的模式。好比:javascript
例 1 java
A(); B(); C();
在上述代碼中,A、B、C 是三個不一樣的函數,每一個函數都是一個不相關的任務。在同步模式下,計算機會先執行 A 任務,再執行 B 任務,最後執行 C 任務。在大部分狀況,同步模式都沒問題。可是若是 B 任務是一個耗時很長網絡的請求,而 C 任務剛好是展示新頁面,B 與 C 沒有依賴關係。這就會致使網頁卡頓的現象。有一種解決方案,將 B 放在 C 後面去執行,但惟一有些不足的是,B 的網絡請求會遲一些再發送。node
還有另外一種更完美解決方案,將 B 任務分紅的兩個部分。一部分是,當即執行網絡請求的任務;另外一部分是,在請求數據回來後執行的任務。這種一部分在當即執行,另外一部分在將來執行的模式稱爲 異步(asynchronous) 。僞代碼以下:ajax
例 2 promise
A(); // 在如今發送請求 ajax('url1',function B() { // 在將來某個時刻執行 }) C(); // 執行順序 A => C => B
實際上,JavaScript 引擎先執行了調用了瀏覽器的網絡請求接口的任務(一部分任務),再由瀏覽器發送網絡請求並監聽請求返回(這個任務不禁 JavaScript 引擎執行,而是瀏覽器);等請求放回後,瀏覽器再通知 JavaScript 引擎,開始執行回調函數中的任務(另外一部分)。JavaScript 異步能力的本質是瀏覽器或 Node 的多線程能力。瀏覽器
將來執行的函數一般也叫 callback。使用 callback 的異步模式,解決了阻塞的問題,可是也帶了一些其餘問題。在最開始,咱們的函數是從上往下書寫的,也是從上往下執行的,這很是符合咱們的思惟習慣,可是如今卻被 callback 打斷了!在上面一段代碼中,它跳過 B 任務,先執行了 C任務!這種異步「非線性」的代碼會比同步「線性」的代碼,更難閱讀,所以也更容易滋生 BUG。網絡
試着判斷下面這段代碼的執行順序,你會對「非線性」代碼比「線性」代碼更難以閱讀,體會更深。多線程
例 3 併發
A(); ajax('url1', function(){ B(); ajax('url2', function(){ C(); } D(); }); E(); // 下面是答案,你猜對了嗎? // A => E => B => D => C
在例 3 中,咱們的閱讀代碼視線是 A => B => C => D => E
,可是執行順序倒是 A => E => B => D => C
。從上往下執行的順序被 Callback 打亂了,這就是非線性代碼帶來的糟糕之處。異步
上面的例子中,咱們能夠經過將 ajax
後面執行的任務 E
和 任務 D
提早,來進行代碼優化。這種技巧在寫多重嵌套的代碼時,是很是有用的。改進後,以下。
例 4
A(); E(); ajax('url1', function(){ B(); D(); ajax('url2', function(){ C(); } }); // 稍做優化,代碼更容易看懂 // A => E => B => D => C
在例 4 中,只有處理了成功回調,並沒處理異常回調。接下來,把異常處理回調加上,再來討論代碼「線性」執行的問題。
例 5
A(); ajax('url1', function(){ B(); ajax('url2', function(){ C(); },function(){ D(); }); },function(){ E(); });
例 5 中,加上異常處理回調後,url1
的成功回調函數 B 和異常回調函數 E,被分開了。這種「非線性」的狀況又出現了。
在 node 中,爲了解決的異常處理「非線性」的問題,制定了錯誤優先的策略。node 中 callback 的第一個參數,專門用於判斷是否發生異常。
例 6
A(); get('url1', function(error){ if(error){ E(); }else { B(); get('url2', function(error){ if(error){ D(); }else{ C(); } }); } });
到此,callback 引發的「非線性」問題基本獲得解決。遺憾的是,一旦嵌套層數多起來,閱讀起來還不是很方便。此外,callback 一旦出現異常,只能在當前回調內部處理異常,並無一個總體的異常觸底方案。
在 JavaScript 的異步進化史中,涌現出一系列解決 callback 弊端的庫,而 Promise 成爲了最終的勝者,併成功地被引入了 ES6 中。它將提供了一個更好的「線性」書寫方式,並解決了異步異常只能在當前回調中捕獲的問題。
Promise 就像一箇中介,它承諾會將一個可信任的異步結果返回。簽定協議的兩方分別是異步接口和 callback。首先 Promise 和異步接口簽定一個協議,成功時,調用 resolve
函數通知 Promise,異常時,調用 reject
通知 Promise。另外一方面 Promise 和 callback 也簽定一個協議,當異步接口的 resolve
或 reject
被調用時,由 Promise 返回可信任的值給 then
和 catch
中註冊的 callback。
一個最簡單的 promise 示例以下:
例 7
// 建立一個 Promise 實例(異步接口和 Promise 簽定協議) var promise = new Promise(function (resolve,reject) { ajax('url',resolve,reject); }); // 調用實例的 then catch 方法 (成功回調、異常回調與 Promise 簽定協議) promise.then(function(value) { // success }).catch(function (error) { // error })
Promise 是個很是不錯的中介,它只返回可信的信息給 callback。怎麼理解可信的概念呢?準確的講,就是 callback 必定會被異步調用,且只會調用一次。好比在使用第三方庫的時候,因爲某些緣由,(假的)「異步」接口不可靠,它執行了同步代碼,而沒有進入異步邏輯,如例 8。
例 8
var promise1 = new Promise(function (resolve) { // 因爲某些緣由致使「異步」接口,被同步執行了 if (true ){ // 同步代碼 resolve('B'); } else { // 異步代碼 setTimeout(function(){ resolve('B'); },0) } }); // promise依舊會異步執行 promise1.then(function(value){ console.log(value) }); console.log('A'); // A => B (先 A 後 B)
再好比,因爲某些緣由,異步接口不可靠,resolve
或 reject
被執行了兩次。但 Promise 只會通知 callback ,第一次異步接口返回的結果。如例 9:
例 9
var promise2 = new Promise(function (resolve) { // resolve 被執行了 2 次 setTimeout(function(){ resolve("第一次"); },0) setTimeout(function(){ resolve("第二次"); },0) }); // 但 callback 只會被調用一次, promise2.then(function(msg){ console.log(msg) // "第一次" console.log('A') }); // A (只有一個)
介紹完 Promise 的特性後,來看看它如何利用鏈式調用,解決 callback 模式下,異步代碼可讀性的問題。鏈式調用指的是:函數 return
一個能夠繼續執行的對象,該對象能夠繼續調用,而且 return
另外一個能夠繼續執行的對象,如此反覆達到不斷調用的結果。如例 10:
例 10
// return 一個能夠繼續執行的 Promise 對象 var fetch = function(url){ return new Promise(function (resolve,reject) { ajax(url,resolve,reject); }); } A(); fetch('url1').then(function(){ B(); // 返回一個新的 Promise 實例 return fetch('url2'); }).catch(function(){ C(); // 異常的時候也能夠返回一個新的 Promise 實例 return fetch('url2'); // 使用鏈式寫法調用這個新的 Promise 實例的 then 方法 }).then(function() { // 能夠繼續 return,也能夠不繼續 return,結束鏈式調用 D(); }) // A B C D (順序執行)
如此反覆,不斷返回一個 Promise 對象,使 Promise 擺脫了 callback 層層嵌套的問題和異步代碼「非線性」執行的問題。
另外,Promise 還解決了一個難點,callback 只能捕獲當前錯誤異常。Promise 和 callback 不一樣,每一個 callback 只能知道本身的報錯狀況,但 Promise 代理着全部的 callback,全部 callback 的報錯,均可以由 Promise 統一處理。因此,能夠經過在最後設置一個 catch
來捕獲以前未捕獲異常。
Promise 解決 callback 的異步調用問題,但 Promise 並無擺脫 callback,它只是將 callback 放到一個能夠信任的中間機構,這個中間機構去連接 callback 和異步接口。此外,鏈式調用的寫法並非很是優雅。接下來介紹的異步(async)函數方案,會給出一個更好的解決方案。
異步(async)函數是 ES7 的一個新的特性,它結合了 Promise,讓咱們擺脫 callback 的束縛,直接用「同步」方式,寫異步函數。注意,這裏的同步指的是寫法同步,但實際依舊是異步執行的。
聲明異步函數,只需在普通函數前添加一個關鍵字 async
便可,如:
async function main(){}
在異步函數中,可使用 await
關鍵字,表示等待後面表達式的執行結果,再往下繼續執行。表達式通常都是 Promise 實例。如,例 11:
例 11
var timer = function (delay) { return new Promise(function create(resolve,reject) { if(typeof delay !== 'number'){ reject(new Error('type error')); } setTimeout(resolve,delay,'done'); }); } async function main{ var value = await timer(100); // 不會馬上執行,等待 100ms 後纔開始執行 console.log(value); // done } main();
異步函數和普通函數的調用方式同樣,最早執行 main()
函數。以後,會當即執行 timer(100)
函數。等到( await
)後面的 promise 函數( timer(100)
)返回結果後,程序纔會執行下一行代碼。
異步函數和普通函數寫法基本相似,除了前面提到的聲明方式相似和調用方式同樣以外,它也可使用 try...catch
來捕捉異常,也能夠傳入參數。但在異步函數中使用 return
是沒有做用的,這和普通的 callback 函數 return
沒有做用是同樣緣由。callback 或者異步函數是單獨放在 JavaScript 棧(stack)中執行的,這時同步代碼已經執行完畢。
在異步函數中,使用 try...catch
異常捕獲的方案,代替了 Promise catch
的異常捕獲的方案。示例以下:
例 12
async function main(delay){ try{ // timer 在例 11 中有過聲明 var value1 = await timer(delay); var value2 = await timer(''); var value3 = await timer(delay); }catch(err){ console.error(err); // Error: type error // at create (<anonymous>:5:14) // at timer (<anonymous>:3:10) // at A (<anonymous>:12:10) } } main(0);
更神奇的是,異步函數也遵循,「函數是第一公民」的準則。也能夠看成值,傳入普通函數和異步函數中執行。須要注意的是,在異步函數中使異步函數用時要使用 await
,否則異步函會被同步執行。例子以下:
例 12
async function doAsync(delay){ // timer 在例 11 中有過聲明 var value1 = await timer(delay); console.log('A') } async function main(main){ doAsync(0); console.log('B') } main(main); // B A
這個時候打印出來的值是 B A
。說明 doAsync
函數中的 await timer(delay)
並被同步執行了。若是要正確異步地執行 doAsync
函數,須要該函數以前添加 await
關鍵字,以下:
async function main(delay){ var value1 = await timer(delay); console.log('A') } async function doAsync(main){ await main(0); console.log('B') } doAsync(main); // A B
因爲異步函數採用類同步的書寫方法,因此在處理多個併發請求,新手可能會像下面同樣書寫:
例 13
var fetch = function (url) { return new Promise(function (resolve,reject) { ajax(url,resolve,reject); }); } async function main(){ try{ var value1 = await fetch('url1'); var value2 = await fetch('url2'); conosle.log(value1,value2); }catch(err){ console.error(err) } } main();
但這樣會致使 url2
的請求必需等到 url1
的請求回來後纔會發送。若是 url1
與 url2
沒有相互的依賴關係,將這兩個請求同時發送實現的效果會更好。
Promise.all
的方法,能夠很好的處理併發請求。Promise.all
接受將多個 Promise 實例爲參數,並將這些參數包裝成一個新的 Promise 實例。這樣,Promise.all
中全部的請求會第一時間發送出去;在全部的請求成功回來後纔會觸發 Promise.all
的 resolve
函數;當有一個請求失敗,則當即調用 Promise.all
的 reject
函數。
var fetch = function (url) { return new Promise(function (resolve, reject) { ajax(url, resolve, reject); }); } async function main(){ try{ var arrValue = await Promise.all[fetch('url1'),fetch('url2')]; conosle.log(arrValue[0], arrValue[1]); }catch(err){ console.error(err) } } main();
最後對異步函數的內容作個小結:
聲明: async function main(){}
異步函數邏輯:可使用 await
調用: main()
捕獲異常: try...catch
傳入參數: main('第一個參數')
return:不生效
異步函數做爲參數傳入其餘函數:能夠
處理併發邏輯:Promise.all
目前使用最新的 Chrome/node 已經支持 ES7 異步函數的寫法了,另外也能夠經過 Babel 以將異步函數轉義爲 ES5 的語法執行。你們能夠本身動手試試,使用異步函數,用類同步的方式,書寫異步代碼。