異步編程對JavaScript來講很是重要,由於JavaScript的語言環境是單線程的,若是沒有異步編程將變得很是可怕,估計根本沒法使用。這篇文章就來總結一下從最原始的回調函數到如今的ES六、ES7的新方法。jquery
文章並不會具體介紹每種方法的原理,若是不是特別懂須要詳細瞭解的同窗能夠看阮一峯的ES6入門。阮大大介紹得很是具體,從原理到用法。es6
單線程就是指進程中只有一個線程。單線程執行程序時,按照代碼的順序,上一個任務完成後纔會執行下一個任務。同一個時間只作一件事情。ajax
JavaScript的主要做用就是操做DOM,若是兩段JS同時操做一個DOM,會引發渲染的衝突。因此JavaScript只能是單線程的。
HTML5中提出的Web Worker,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此,這個新標準並無改變JavaScript單線程的本質。編程
同步是指任務一件一件的按順序完成,上一件沒有完成就沒法作下一件;而異步則是指,開始作一件事以後就放在那兒等待結果,不須要守着,繼續作下一件事便可。json
異步能夠解決JavaScript單線程效率低、同一事件只能作一件事情的問題。api
console.log(1); setTimeOut(()=>{ console.log(2); },1000) console.log(3);
這段代碼首先會打印1,而後打印3,過1000ms之後打印2。promise
然而這段代碼內部是如何運行的呢,打印1和打印3的命令是同步命令,因此直接按順序放到主進程中執行,setTimeOut裏的是一個異步命令,在1000ms之後會被放入異步隊列中。而主進程會經過事件循環(event loop)不斷地從異步隊列中取出命令,而後執行。當主進程在1000ms之後查詢到了打印2的命令時,便把這個函數拿到主進程中執行。異步
這裏所有用ajax連續調用例子,接口是豆瓣的真實接口,能夠獲得具體的數據,但有限制每小時150次。async
這是最原始的一種異步解決方法。回調函數,就是指一件事作完之後,拿到結果後要作的事情。異步編程
var urlBase = 'https://api.douban.com/'; var start = 0,count = 5; $.ajax({ url: urlBase+'v2/book/user/1219073/collections', type: 'GET', dataType: 'jsonp', data:{ start:start, count:count }, success: function(data){ console.log(data); start+=count; $.ajax({ url: urlBase+'v2/book/user/1219073/collections', type: 'GET', dataType: 'jsonp', data:{ start:start, count:count }, success:function(data){ console.log(data); start+=count; $.ajax({ url: urlBase+'v2/book/user/1219073/collections', type: 'GET', dataType: 'jsonp', data:{ start:start, count:count }, success:function(data){ console.log(data); } }) } }) } })
這是用jquery的ajax方法調用的豆瓣某我的的收藏的圖書,start和count是豆瓣提供的接口參數,start表明從哪一條數據開始獲取,count表明一共獲取多少條數據。
從上面的代碼能夠看到多個回調函數的嵌套,若是須要調用得越多,回調也堆積得越多,多了之後代碼就很難維護,時間久了本身也要花好久才能看懂代碼。
將每一次回調的方法封裝成函數,代碼量會減小不少。
var urlBase = 'https://api.douban.com/'; var start = 0,count = 5; function ajax(start,count,cb){ $.ajax({ url: urlBase+'v2/book/user/1219073/collections', type: 'GET', dataType: 'jsonp', data:{ start:start, count:count }, success:function(data){ console.log(data); start+=count; cb && cb(start); } }) } ajax(start,count,function(start){ ajax(start,count,function(start){ ajax(start,count) }) });
可是這樣依然沒有解決「回調地獄」的問題,當每次回調的邏輯操做變得愈來愈多的時候,代碼依然難以維護。
Promise對象是ES6提出的一種對異步編程的解決方案,但它不是新的語法,而是一種新的寫法,容許將回調函數的嵌套改爲鏈式調用。
雖說Promise是ES6提出的標準,但其實jQuery在1.5版本之後就提出了相似的東西,叫作deferred對象。具體學習能夠看jQuery的deferred對象詳解。
const urlBase = 'https://api.douban.com/'; let start = 0,count = 5; function ajax(start,count){ let dtd = $.Deferred(); $.ajax({ url: urlBase+'v2/book/user/1219073/collections', type: 'GET', dataType: 'jsonp', data:{ start:start, count:count }, success:function(data){ start+=count; dtd.resolve(data,start); }, error:function(err){ dtd.reject(err); } }) return dtd; } ajax(start,count).then((data1,start) => { console.log(data1); return ajax(start,count); }).then((data2,start) => { console.log(data2); return ajax(start,count); }).then((data3,start) => { console.log(data3); }).catch((err) => { console.log('這裏出錯啦'); })
從這段代碼能夠看出來,寫法和promise很是類似了,能夠猜想promise就是從deferred演化而來的。
一樣的功能實現能夠改爲如下寫法:
const urlBase = 'https://api.douban.com/'; let start = 0,count = 5; function ajax(start,count){ return new Promise(function(resolve,reject){ $.ajax({ url: urlBase+'v2/book/user/1219073/collections', type: 'GET', dataType: 'jsonp', data:{ start:start, count:count }, success:function(data){ start+=count; resolve(data,start); }, error:function(err){ reject(err); } }) }) } ajax(start,count).then((data1,start) => { console.log(data1); return ajax(start,count); }).then((data2,start) => { console.log(data2); return ajax(start,count); }).then((data3,start) => { console.log(data3); }).catch((err) => { console.log('這裏出錯啦'); })
Promise使用.then方法解決了回調的問題,但代碼依然冗餘,且語義不強,放眼望去全是.then方法,很難找出須要修改的地方。
Generator函數也是ES6中提出的異步編程解決方法,整個 Generator 函數就是一個封裝的異步任務,或者說是異步任務的容器。最大特色就是能夠交出函數的執行權(即暫停執行)。
異步操做須要暫停的地方,都用yield語句註明。
const urlBase = 'https://api.douban.com/'; let start = 0,count = 5; function ajax(start,count){ return new Promise(function(resolve,reject){ $.ajax({ url: urlBase+'v2/book/user/1219073/collections', type: 'GET', dataType: 'jsonp', data:{ start:start, count:count }, success:function(data){ start+=count; resolve(data); }, error:function(err){ reject(err); } }) }) } let gen = function*(){ yield ajax(start,count); start+=count; yield ajax(start,count); start+=count; yield ajax(start,count); } let g = gen(); g.next().value.then((data1) => { console.log(data1); g.next().value.then((data2) => { console.log(data2); g.next().value.then((data3) => { console.log(data3); }) }) })
這樣在gen函數內三個ajax請求就看起來很是像同步的寫法了,可是執行的過程並不清晰,且須要手動.next來執行下一個操做。這並非咱們想要的完美異步方案。
async函數是ES7提出的一種異步解決方案,它與generator並沒有大的不一樣,並且能夠說它就是generator的一種語法糖。它的語法只是把generator函數裏的*換成了async,yield換成了await,但它同時有幾個優勢。
(1)內置執行器。這表示它不須要不停的next來使程序繼續向下進行。
(2)更好的語義。async表明異步,await表明等待。
(3)更廣的適用性。await命令後面能夠跟Promise對象,也能夠是原始類型的值。
(4)返回的是Promise。
const urlBase = 'https://api.douban.com/'; let start = 0,count = 5; function ajax(start,count){ return new Promise(function(resolve,reject){ $.ajax({ url: urlBase+'v2/book/user/1219073/collections', type: 'GET', dataType: 'jsonp', data:{ start:start, count:count }, success:function(data){ start+=count; resolve(data); }, error:function(err){ reject(err); } }) }) } async function getData(){ let data = null; try{ for(let i = 0;i < 3;i++){ data = await ajax(start,count); console.log(data); start+=count; } } catch(err){ console.log(err); } } getData();
用async函數改寫以後語義清晰,代碼量也減小了,而且內部自帶執行器,感受很符合想象中的異步解決方法。
到此就把幾種常見的異步回調方法介紹完了,我我的感受用async+promise是最好的辦法。固然爲了更加深入的理解這些異步解決辦法,必定要多多的用到項目中,多用纔會多理解。