爲了不資源管理等複雜性的問題,
javascript被設計爲單線程的語言,即便有了html5 worker,也不能直接訪問dom.javascript
javascript 設計之初是爲瀏覽器設計的GUI編程語言,GUI編程的特性之一是保證UI線程必定不能阻塞,不然體驗不佳,甚至界面卡死。html
通常安卓開發,會有一個界面線程,一個後臺線程,保證界面的流暢。前端
因爲javascript是單線程,因此採用異步非阻塞的編程模式,javascript的絕大多數api都是異步api.vue
本文是本人的一個總結:從Brendan Eich剛設計的第一版javascript到如今的ES7,一步步總結javascript異步編程歷史。html5
說明:
本文全部源碼請訪問:https://github.com/etoah/note/tree/master/async
請安裝babel環境,用babel-node 執行對應例子java
那麼什麼是異步編程,異步編程簡單來講就是:執行一個指令不會立刻返回結果而執行下一個任務,而是等到特定的事件觸發後,才能獲得結果。node
如下是當有ABC三個任務,同步或異步執行的流程圖:jquery
示意圖來自stackoverflowgit
同步 angularjs
thread ->|----A-----||-----B-----------||-------C------|
異步
A-Start ---------------------------------------- A-End | B-Start ----------------------------------------|--- B-End | | C-Start -------------------- C-End | | V V V V V V thread-> |-A-|---B---|-C-|-A-|-C-|--A--|-B-|--C--|---A-----|--B--|
顯然,在宏觀上,同步程序是串行的執行各任務,執行單個任務時會阻塞純線程,異步能夠「並行」的執行任務。
異步編程時就須要指定異步任務完成後須要執行的指令,總的來講有如下幾種「指定異步指令」的方式:
下面會一步一步展示各類方式。
每一個編程語言對異步實現的方式不同,C#能夠用委託,java能夠用接口或基類傳入的方式,
早期的javascript的異步的實現也相似於這種類的屬性的方式:每一個類實例的相關回調事件有相應的handler(onclick,onchange,onload等)。
在DOM0級事件處理程序,就是將一個函數賦值給一個元素的屬性。
element.onclick=function(){ alert("clicked"); } window.onload=function(){ alert("loaded"); }
這種寫法簡單明瞭,同時會有如下幾個問題
window.onload=function(){ handlerA(); handlerB(); handlerc(); }
若是這三個handler來自三個不一樣的模塊,那這個文件模塊耦合度就爲3(華爲的計算方法)。依賴高,不利於複用和維護。
window.onload=function(){ console.log("handler 1"); } //... 不少其它框架,庫,主題 的代碼 var handlerbak=window.onload window.onload=function(){ handlerbak(); //這行註釋的話上面handler 1就會被覆蓋。 console.log("handler 2"); }
當代碼量大時,這種問題沒有warning也沒有error, 經驗不豐富的前端可能花費大量的時間查找問題。
事件handler容易被重寫,庫/框架的安全,寄託於使用者的對框架的熟練程度,極不安全。
因爲javascript支持函數式編程,JavaScript語言對異步編程的實現能夠用回調函數。
DOM2級事件解決了這個問題以上兩個問題
element.addEventListener("click",function(){ alert("clicked"); })
這裏其實是一個發佈訂閱模式,addEventListener至關於subscribe, dispatchEvent至關於publish,
很好的解決了訂閱者以前的依賴,jquery,vue,flux,angularjs均實現了相似的模式。
發佈訂閱模式雖解決了上面耦合和不安全的問題,可是在實現大型應用時,還會有如下問題。
step1(function (value1) { step2(value1, function(value2) { step3(value2, function(value3) { step4(value3, function(value4) { // Do something with value4 }); }); }); });
try{ setTimeout(function(){ JSON.parse("{'a':'1'}") console.log("aaaa") },0) } catch(ex){ console.log(ex); //不能catch到這個異常 }
當C操做依賴於B操做和C操做,而B與A沒有依賴關係時,不用第三方庫(如async,eventproxy)的話,B與A本能夠並行,卻串行了,性能有很大的提高空間。
流程圖以下:
graph LR Start-->A A-->B B-->C
但用promise後,能夠方便的用並行:
Promise:
graph LR Start-->A Start-->B A-->C B-->C
如上流程圖,Promise很好的解決了「並行」的問題,咱們看看用promise庫怎麼發送get請求:
import fetch from 'node-fetch' fetch('https://api.github.com/users/etoah') .then((res)=>res.json()) .then((json)=>console.log("json:",json))
能夠看到promise把原來嵌套的回調,改成級連的方式了,實際是一種代理(proxy)。
新建一個promise實例:
var promise = new Promise(function(resolve, reject) { // 異步操做的代碼 if (/* 異步操做成功 */){ resolve(value); } else { reject(error); } });
promise把成功和失敗分別代理到resolved 和 rejected .
同時還能夠級連catch異常。
到這裏異步的問題,有了一個比較優雅的解決方案了,若是要吹毛求疵,還有一些彆扭的地方,須要改進。
封裝,理解相對回調複雜,這是如下我公司項目的一段代碼(coffeescript),併發代碼,加上resolved,rejected的回調,
即便是用了coffee,混個業務和參數處理,第一眼看上去仍是比較懵,代碼可讀性並無想象中的好。
#併發請求companyLevel companyInfoP = companyinfoServicesP.companyLevel({hid: req.session.hid}) requestP(userOption).success((userInfo)-> roleOption = uri: "#{config.server_host}/services/rights/userroles?userid=#{user.userId}" method: 'GET' #保證companyInfo 寫入 Q.all([companyInfoP, requestP(roleOption)]).spread( (companyinfo, roles)-> Util.session.init req, user, roles.payload Util.session.set(req, "companyInfo", companyinfo.payload) Util.session.set(req, "roleids", roles.payload) u = Util.session.getInfo req return next {data: Util.message.success(u)} , (err)-> return next err )
在指明resolved 和 rejected的時,用的仍是最原始的回調的方式。
能不能用同步的方式寫異步代碼?
在ES5前是這基本不可實現,可是,ES6的語法引入了Generator, yeild的關鍵字能夠用同步的語法寫異步的程序。
簡單來講generators能夠理解爲一個可遍歷的狀態機。
語法上generator,有兩個特徵:
因爲generator是一個狀態機,因此須要手動調用next 才能執行,但TJ大神開發了co模塊,能夠自動執行generator。
import co from 'co'; co(function* (){ var now = Date.now(); yield sleep(150); //約等待150ms console.log(Date.now() - now); }); function sleep(ms){ return function(cb){ setTimeout(cb, ms); }; } import fetch from 'node-fetch' co(function* (){ let result= yield [ (yield fetch('https://api.github.com/users/tj')).json(), (yield fetch('https://api.github.com/users/etoah')).json(), ]; console.log("result:",result) });
不管是延遲執行,仍是併發的從兩個接口獲取數據,generator均可以用同步的方式編寫異步代碼。
注意:co模塊約定,yield命令後面只能是Thunk函數或Promise對象
ES7 引入了像C#語言中的 await,async關鍵字,並且babel已支持(引入plugins:transform-async-to-generator )
async函數徹底能夠看做多個異步操做,包裝成的一個Promise對象,而await命令就是內部then命令的語法糖。
import fetch from 'node-fetch'; (async function (){ let result= await fetch('https://api.github.com/users/etoah'); let json =await result.json(); console.log("result:",json); })(); //exception (async function (){ try{ let result= await fetch('https://api.3github.com/users/etoah'); let json =await result.json(); console.log("result:",json); } catch(ex){ console.warn("warn:",ex); } })()
簡單比較會發現,async函數就是將Generator函數的星號(*)替換成async,將yield替換成await,同時不須要co模塊,更加語義化。
可是與yeild又不徹底相同,標準沒有接收await*的語法( :( 查看詳情),
若需「並行」執行promise數組,推薦用Promise.All,因此須要並行請求時,須要這樣寫:
(async function (){ let result= await Promise.all([ (await fetch('https://api.github.com/users/tj')).json(), (await fetch('https://api.github.com/users/etoah')).json() ]); console.log("result:",result); })();
雖然說沒有不能用 await* , 整體來講結構仍是簡單清晰的
沒有任何callback,流程和異常捕獲是徹底同步的寫法。並且javascript語言級別支持這種寫法。能夠說這是異步的終極解決方案了。
到這裏,jser結合promise,yield,await的寫法,能夠和回調嵌套說拜拜了。
雖有這麼多的不一樣的異步編程方式,可是異步編程的本質並無變,只有對coder更友好了而已,但對工程化可讀性和可維護性有很大的改進。
全文完,若有不嚴謹的地方,歡迎指正。