javascript的非同步操作方法有:1、回撥函數;2、事件監聽;3、「釋出/訂閱」模式;4、promise;5、generator;6、「async/await」。
本教學操作環境:windows7系統、javascript1.8.5版、Dell G3電腦。
非同步模式並不難理解,比如任務A、B、C,執行A之後執行B,但是B是一個耗時的工作,所以,把B放在任務佇列中,去執行C,然後B的一些I/O等返回結果之後,再去執行B,這就是非同步操作。
JavaScript語言的執行環境是「單執行緒」, 所謂單執行緒,就是一次只能完成一件任務, 如果有多個任務就需要排隊,一個完成了,繼續下一個,這種方式在實現來說是非常簡單的,但是如果一個任務耗時很長,那麼後面的任務就需要排隊等著,會拖延整個程式的執行。 常見的瀏覽器無響應(假死)就是因為某一段JavaScript程式碼長時間執行(比如死迴圈),導致整個頁面卡死,其他任務無法執行。
為了解決這個問題,JavaScript語言將任務的執行模式分為兩種:同步(Synchronous)和非同步(Asynchronous)。
同步任務執行的順序和排隊的順序是一致的,而非同步則需要有一個或者多個回撥函數,前一個任務結束後,不是執行後一個任務,而是執行回撥函數,後一個任務則是等著前一個任務結束就執行,所以程式的執行順序與任務的排列順序是不一致的,非同步的。
非同步模式非常重要,在瀏覽器端,耗時很長的操作都應該非同步執行,避免瀏覽器失去響應,最好的例子就是ajax操作,在伺服器端, 非同步操作甚至是唯一方式,因為執行環境是單執行緒的,如果允許同步執行所有的http請求,伺服器效能會急劇下降,很快就會失去響應。
JavaScript中非同步程式設計的方法有:
下面我來分別介紹這幾種非同步方法:
回撥函數是非同步程式設計中最基本的方法。假設有三個函數f1、f2、f3,f2需要等待f1的執行結果,而f3是獨立的,不需要f1和f2的結果,如果我們寫成同步,就是這樣的:
f1(); f2(); f3();
如果f1執行的很快,可以; 但是如果f1執行的很慢,那麼f2和f3就會被阻塞,無法執行。這樣的效率是非常低的。但是我們可以改寫,將f2寫成是f1的回撥函數,如下:
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才能做出真正的回撥函數。
另一種非同步的思路是採用事件驅動模式。任務的執行不取決於程式碼的順序, 而取決於某個事件是否發生。 還是以f1、f2、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函數中,否則會報錯。
【相關推薦:】
以上就是javascript有哪些非同步操作方法的詳細內容,更多請關注TW511.COM其它相關文章!