摘要:簽訂協議的兩方分別是異步接口和。在異步函數中,使用異常捕獲的方案,代替了的異常捕獲的方案。需要注意的是,在異步函數中使異步函數用時要使用,不然異步函會被同步執行。
同步與異步
通常,代碼是由上往下依次執行的。如果有多個任務,就必需排隊,前一個任務完成,后一個任務才會執行。這種執行模式稱之為: 同步(synchronous) 。新手容易把計算機用語中的同步,和日常用語中的同步弄混淆。如,“把文件同步到云端”中的同步,指的是“使...保持一致”。而在計算機中,同步指的是任務從上往下依次執行的模式。比如:
例 1
A(); B(); C();
在上述代碼中,A、B、C 是三個不同的函數,每個函數都是一個不相關的任務。在同步模式下,計算機會先執行 A 任務,再執行 B 任務,最后執行 C 任務。在大部分情況,同步模式都沒問題。但是如果 B 任務是一個耗時很長網絡的請求,而 C 任務恰好是展現新頁面,B 與 C 沒有依賴關系。這就會導致網頁卡頓的現象。有一種解決方案,將 B 放在 C 后面去執行,但唯一有些不足的是,B 的網絡請求會遲一些再發送。
還有另一種更完美解決方案,將 B 任務分成的兩個部分。一部分是,立即執行網絡請求的任務;另一部分是,在請求數據回來后執行的任務。這種一部分在立即執行,另一部分在未來執行的模式稱為 異步(asynchronous) 。偽代碼如下:
例 2
A(); // 在現在發送請求 ajax("url1",function B() { // 在未來某個時刻執行 }) C(); // 執行順序 A => C => B
實際上,JavaScript 引擎先執行了調用了瀏覽器的網絡請求接口的任務(一部分任務),再由瀏覽器發送網絡請求并監聽請求返回(這個任務不由 JavaScript 引擎執行,而是瀏覽器);等請求放回后,瀏覽器再通知 JavaScript 引擎,開始執行回調函數中的任務(另一部分)。JavaScript 異步能力的本質是瀏覽器或 Node 的多線程能力。
callback未來執行的函數通常也叫 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 一旦出現異常,只能在當前回調內部處理異常,并沒有一個整體的異常觸底方案。
promise在 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)函數異步(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 (:5:14) // at timer ( :3:10) // at A ( :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 的語法執行。大家可以自己動手試試,使用異步函數,用類同步的方式,書寫異步代碼。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/90840.html
摘要:閉包利用的,其實就是作用域嵌套情況下,內部作用域可以訪問外部作用域這一特性。之所以要將閉包和垃圾回收策略聯系在一起,是因為這涉及到閉包的一些重要特性,如變量暫時存儲和內存泄漏。因為匿名函數回調的閉包實際引用的是變量,而非變量的值。 本文旨在總結在js學習過程中,對閉包的思考,理解,以及一些反思,如有不足,還請大家指教 閉包---closure 閉包是js中比較特殊的一個概念,其特殊之處...
摘要:但是的的出現碉堡的新朋友,我們可以輕松寫出同步風格的代碼同時又擁有異步機制,可以說是目前最簡單,最優雅,最佳的解決方案了。不敢說這一定是終極的解決方案,但確實是目前最優雅的解決方案 一、異步解決方案的進化史 JavaScript的異步操作一直是個麻煩事,所以不斷有人提出它的各種解決方案。可以追溯到最早的回調函數(ajax老朋友),到Promise(不算新的朋友),再到ES6的Gener...
摘要:作者珂珂滬江前端開發工程師本文為原創文章,有不當之處歡迎指出。只對未來發生的事情做出兩種基本情況的應對成功和失敗。在異步轉同步這條道路上,只是一個出彩的點,他還尚有一些缺陷和不足,并不是我們最終的解決方案。 作者:珂珂 (滬江前端開發工程師)本文為原創文章,有不當之處歡迎指出。轉載請標明出處。 一個新事物的產生必然是有其歷史原因的。為了更好的以同步的方式寫異步的代碼,人們在JS上操碎了...
摘要:然而,臨近規范發布時,有建議提及未來的版本號切換為編年制,比如用同來指代在年末前被定稿的所有版本。總得來說就是版本號不再那么重要了,開始變得更像一個萬古長青的活標準。 你不知道的JS(下卷)ES6與之未來 第一章:ES的今與明 在你想深入這本書之前,你應該對(在讀此書時)JavaScript的最近標準掌握熟練,也就是ES5(專業來說是ES 5.1)。在此,我們決定全方面地談論關于將近的...
摘要:異步編程一般用來調取接口拉數據。通過我描述的篇幅,就知道異步編程比同步編程麻煩許多。遠古時期,異步編程是通過回調函數來解決的。 半理解系列--Promise的進化史 學過js的都知道,程序有同步編程和異步編程之分,同步就好比流水線,一步一個腳印,做完上個任務才做下一個,異步編程好比客服,客服接了一個電話,收到了一個任務,然后把任務交給另外的人來處理,同時,繼續接聽下一個電話,等到另外的...
閱讀 920·2021-11-16 11:45
閱讀 2126·2021-10-09 09:44
閱讀 1341·2019-08-30 14:03
閱讀 1127·2019-08-26 18:28
閱讀 3328·2019-08-26 13:50
閱讀 1715·2019-08-23 18:38
閱讀 3450·2019-08-23 18:22
閱讀 3589·2019-08-23 15:27