異步模式並不難理解,好比任務A、B、C,執行A以後執行B,可是B是一個耗時的工做,因此,把B放在任務隊列中,去執行C,而後B的一些I/O等返回結果以後,再去執行B,這就是異步操做。node
JavaScript語言的執行環境是「單線程」, 所謂單線程,就是一次只能完成一件任務, 若是有多個任務就須要排隊,一個完成了,繼續下一個,這種方式在實現來講是很是簡單的,可是若是一個任務耗時很長,那麼後面的任務就須要排隊等着,會拖延整個程序的執行。 常見的瀏覽器無響應(假死)就是由於某一段JavaScript代碼長時間運行(好比死循環),致使整個頁面卡死,其餘任務沒法執行。 jquery
爲了解決這個問題,JavaScript語言將任務的執行模式分爲兩種:同步(Synchronous)和異步(Asynchronous)。 git
同步任務執行的順序和排隊的順序是一致的,而異步則須要有一個或者多個回調函數,前一個任務結束後,不是執行後一個任務,而是執行回調函數,後一個任務則是等着前一個任務結束就執行,因此程序的執行順序與任務的排列順序是不一致的,異步的。github
異步模式很是重要,在瀏覽器端,耗時很長的操做都應該異步執行,避免瀏覽器失去響應,最好的例子就是ajax操做,在服務器端, 異步操做甚至是惟一方式,由於執行環境是單線程的,若是容許同步執行全部的http請求,服務器性能會急劇降低,很快就會失去響應。ajax
JavaScript中異步編程的方法有:編程
下面我來分別介紹這幾種異步方法:json
回調函數是異步編程中最基本的方法。假設有三個函數f一、f二、f3,f2須要等待f1的執行結果,而f3是獨立的,不須要f1和f2的結果,若是咱們寫成同步,就是這樣的:api
f1();
f2();
f3()
若是f1執行的很快,能夠; 可是若是f1執行的很慢,那麼f2和f3就會被阻塞,沒法執行。這樣的效率是很是低的。可是咱們能夠改寫,將f2寫成是f1的回調函數,以下:promise
function f1(callback){ setTimeout(function () { // f1的任務代碼 callback(); }, 1000); }
那麼這時候執行代碼就是這樣:瀏覽器
f1(f2);
f3()
這樣,就是一個異步的執行了,即便f1很費時間,可是因爲是異步的,那麼f3()就會很快的獲得執行,而不會受到f1和f2的影響。
注意: 若是咱們把f1寫成這樣呢?
function f1(callback){ // f1的任務代碼 callback(); }
而後,咱們一樣能夠這麼調用:
f1(f2);
f3()
這時候仍是異步的嗎? 答案:不是異步。 這裏的回調函數並不是真正的回調函數,若是沒有利用setTimeout含函數,那麼f3()的執行一樣須要等到f1(f2)徹底執行完畢,這裏要注意。而咱們就是利用setTImeout才能作出真正的回調函數。
另外一種異步的思路是採用事件驅動模式。任務的執行不取決於代碼的順序, 而取決於某個事件是否發生。 仍是以f一、f二、f3爲例子。 首先,爲f1綁定一個事件(這裏採用jquery的寫法):
f1.on('done', f2); f3()
這裏的意思是: 當f1發生了done事件,就執行f2, 而後,咱們對f1進行改寫:
function f1(){ setTimeout(function () { // f1的任務代碼 f1.trigger('done'); }, 1000); }
f1.trigger('done')表示, 執行完成後,當即觸發done事件,從而開始執行f2。
這種方法的優勢就是比較容易理解,能夠綁定多個事件,每一個事件能夠指定多個回調函數,並且能夠去耦合,有利於實現模塊化,缺點就是整個程序都要變成事件驅動型,運行流程會變得很不清晰。
第二種方法的事件,實際上咱們徹底能夠理解爲「信號」,即f1完成以後,觸發了一個 'done',信號,而後再開始執行f2。
咱們假定,存在一個「信號中心」,某個任務執行完成,就向信號中心「發佈」(publish)一個信號,其餘任務能夠向信號中心「訂閱」這個信號, 從而知道何時本身能夠開始執行。 這個就叫作「發佈/訂閱模式」, 又稱爲「觀察者」模式 。
這個模式有多種實現, 下面採用Ben Alman的Tiny PUb/Sub,這是jQuery的一個插件。
首先,f2向"信號中心"jquery訂閱"done"信號,
jQuery.subscribe("done", f2);
而後,f1進行以下改寫:
function f1(){ setTimeout(function () { // f1的任務代碼 jQuery.publish("done"); }, 1000); }
jquery.pushlish("done")的意思是: f1執行完成後,向「信號中心」jQuery發佈「done」信號,從而引起f2的執行。
此外,f2完成執行後,也能夠取消訂閱(unsubscribe)。
jQuery.unsubscribe("done", f2);
這種方法的性質和「事件監聽」很是相似,可是明顯是優於前者的,由於咱們能夠經過查看「消息中心」,瞭解到存在多少信號、每一個信號有多少個訂閱者,從而監控程序的運行。
promise是commonjs工做組提出來的一種規範,目的是爲異步編程提供統一接口。
簡答的說,它的思想是每個異步任務返回一個promise對象,該對象有一個then方法,容許指定回調函數。 好比,f1的回調函數f2,能夠寫成:
f1().then(f2);
f1要進行下面的改寫(這裏使用jQuery的實現):
function f1(){ var dfd = $.Deferred(); setTimeout(function () { // f1的任務代碼 dfd.resolve(); }, 500); return dfd.promise; }
這樣的優勢在於,回調函數編程了鏈式寫法,程序的流程能夠看得很清楚,並且有一整套的配套方法,能夠實現不少強大的功能 。
如:指定多個回調函數:
f1().then(f2).then(f3);
再好比,指定發生錯誤時的回調函數:
f1().then(f2).fail(f3);
並且,他還有一個前面三種方法都沒有的好處:若是一個任務已經完成,再添加回調函數,該回調函數會當即執行。 因此,你不用擔憂是否錯過了某個事件或者信號,這種方法的肯定就是編寫和理解,都比較困難。
在ES6誕生以前,異步編程的方法,大體有下面四種:
沒錯,這就是上面講得幾種異步方法。 而generator函數將JavaScript異步編程帶入了一個全新的階段!
好比,有一個任務是讀取文件進行處理,任務的第一段是向操做系統發出請求,要求讀取文件。而後,程序執行其餘任務,等到操做系統返回文件,再接着執行任務的第二段(處理文件)。這種不連續的執行,就叫作異步。
相應地,連續的執行就叫作同步。因爲是連續執行,不能插入其餘任務,因此操做系統從硬盤讀取文件的這段時間,程序只能乾等着。
協程
傳統的編程語言中,早就有了異步編程的解決方案,其中一種叫作協程,意思是多個線程互相協做,完成異步任務。
協程優勢像函數,又有點像線程,運行流程以下:
A
開始執行。A
執行到一半,進入暫停,執行權轉移到協程B
。B
交還執行權。A
恢復執行。上面的協程A,就是異步任務,由於它分爲兩段(或者多段)執行。
舉例來講,讀取文件的協程寫法以下:
function *asyncJob() { // ...其餘代碼 var f = yield readFile(fileA); // ...其餘代碼 }
上面代碼的函數asyncJob是一個協程,奧妙就在於yield命令, 它表示執行到此處,執行權交給其餘協程,也就是說yield命令是異步兩個階段的分界線。
協程遇到yield命令就暫停,等到執行權返回,再從暫停的地方繼續向後執行,它的最大優勢就是代碼的寫法很是像同步操做,若是去除yield命令,簡直是如出一轍。
協程的Generator函數實現
Generator函數是協程在ES6中的實現,最大特色就是能夠交出函數的執行權(即暫停執行)。
整個Generator函數就是一個封裝的異步任務,或者說異步任務的容器。 異步任務須要暫停的地方,都用yield語句註明。 以下:
function* gen(x) { var y = yield x + 2; return y; } var g = gen(1); g.next() // { value: 3, done: false } g.next() // { value: undefined, done: true }
在調用gen函數時 gen(1), 會返回一個內部指針(即遍歷器)g。 這是Generator函數不一樣於普通函數的另外一個地方,即執行它(調用函數)不會返回結果, 返回的一個指針對象 。調用指針g的next方法,會移動內部指針(即執行異步任務的第一階段),指向第一個遇到的yield語句,這裏咱們是x + 2,可是實際上這裏只是舉例,實際上 x + 2 這句應該是一個異步操做,好比ajax請求。 換言之,next方法的做用是分階段執行Generator函數。每次調用next方法,會返回一個對象,表示當前階段的信息(value屬性和done屬性)。 value屬性是yield語句後面表達式的值,表示當前階段的值;done屬性是一個布爾值,表示Generator函數是否執行完畢,便是否還有下一個階段。
Generator函數的數據交換和錯誤處理
Generator 函數能夠暫停執行和恢復執行,這是它能封裝異步任務的根本緣由。除此以外,它還有兩個特性,使它能夠做爲異步編程的完整解決方案:函數體內外的數據交換和錯誤處理機制。
next
返回值的value屬性,是 Generator 函數向外輸出數據;next
方法還能夠接受參數,向 Generator 函數體內輸入數據。
function* gen(x){ var y = yield x + 2; return y; } var g = gen(1); g.next() // { value: 3, done: false } g.next(2) // { value: 2, done: true }
上面代碼中,第一next
方法的value
屬性,返回表達式x + 2
的值3
。第二個next
方法帶有參數2
,這個參數能夠傳入 Generator 函數,做爲上個階段異步任務的返回結果,被函數體內的變量y
接收。所以,這一步的value
屬性,返回的就是2
(變量y
的值)。
Generator 函數內部還能夠部署錯誤處理代碼,捕獲函數體外拋出的錯誤。
function* gen(x){ try { var y = yield x + 2; } catch (e){ console.log(e); } return y; } var g = gen(1); g.next(); g.throw('出錯了'); // 出錯了
上面代碼的最後一行,Generator 函數體外,使用指針對象的throw
方法拋出的錯誤,能夠被函數體內的try...catch
代碼塊捕獲。這意味着,出錯的代碼與處理錯誤的代碼,實現了時間和空間上的分離,這對於異步編程無疑是很重要的。
異步任務的封裝
下面看看如何使用 Generator 函數,執行一個真實的異步任務。
var fetch = require('node-fetch'); function* gen(){ var url = 'https://api.github.com/users/github'; var result = yield fetch(url); console.log(result.bio); }
上面代碼中,Generator 函數封裝了一個異步操做,該操做先讀取一個遠程接口,而後從 JSON 格式的數據解析信息。就像前面說過的,這段代碼很是像同步操做,除了加上了yield
命令。
執行這段代碼的方法以下。
var g = gen(); var result = g.next(); result.value.then(function(data){ return data.json(); }).then(function(data){ g.next(data); });
上面代碼中,首先執行 Generator 函數,獲取遍歷器對象,而後使用next
方法(第二行),執行異步任務的第一階段。因爲Fetch
模塊返回的是一個 Promise 對象,所以要用then
方法調用下一個next
方法。
能夠看到,雖然 Generator 函數將異步操做表示得很簡潔,可是流程管理卻不方便(即什麼時候執行第一階段、什麼時候執行第二階段)。
以下:
function* gen(x) { yield 1; yield 2; yield 3; return 4; } var a = gen(); console.log(a.next()); console.log(a.next()); console.log(a.next()); console.log(a.next());
最終,打印臺輸出
即開始調用gen(),並無真正的調用,而是返回了一個生成器對象,a.next()的時候,執行第一個yield,並馬上暫停執行,交出了控制權; 接着,咱們就能夠去a.next() 開始恢復執行。。。 如此循環往復。
每當調用生成器對象的next的方法時,就會運行到下一個yield表達式。 之因此稱這裏的gen()爲生成器函數,是由於區別以下:
async函數基於Generator又作了幾點改進:
不少人都認爲這是異步編程的終極解決方案,由此評價就可知道該方法有多優秀了。它基於Promise使用async/await來優化then鏈的調用,其實也是Generator函數的語法糖。 async 會將其後的函數(函數表達式或 Lambda)的返回值封裝成一個 Promise 對象,而 await 會等待這個 Promise 完成,並將其 resolve 的結果返回出來。
await獲得的就是返回值,其內部已經執行promise中resolve方法,而後將結果返回。使用async/await的方式寫回調任務:
async function dolt(){ console.time('dolt'); const time1=300; const time2=await step1(time1); const time3=await step2(time2); const result=await step3(time3); console.log(`result is ${result}`); console.timeEnd('dolt'); } dolt();
能夠看到,在使用await關鍵字所在的函數必定要是async關鍵字修飾的。
功能還很新,屬於ES7的語法,但使用Babel插件能夠很好的轉義。另外await只能用在async函數中,不然會報錯。
參考文章:
https://juejin.im/entry/58ed90268d6d8100580c715b