剛開始學習JavaScript編程時,你可能就已經知道,JavaScript是單線程(Single Thread)執行的。單線程的意思是一次只能執行一個方法,只有等一個方法返回纔會去執行另一個方法。winform編程時如果UI線程中等待的話便會造成UI假死,但是在Web編程中沒有線程的概念,也就是說如果代碼等待則UI便會卡死。
爲了解決這個問題,Javascript語言將任務的執行模式分成兩種:同步(Synchronous)和異步(Asynchronous)。
同步模式便是上述所說的UI卡死的情況;
異步模式的話最普遍的例子就是Ajax方法。
本文總結一些JavaScript中異步編程的方法,如若文中有錯誤或者有其他異步方法,望不吝賜教。
在下文的示例中會使用setTimeout模擬耗時操作。
1、 回調方法(Callback)
回調是我剛開始接觸JavaScript異步時使用的方法,相信也是大多數人的體驗。
var x = 0;
function f1(callback) {
setTimeout(function() {
x += 1;
callback();
}, 100);
}
function f2() {
alert(x);
}
f1(f2);
使用回調方法大致就是f1(f2)這樣的情況,但是當回調的層級過多的時候代碼便會呈現金字塔結構,使代碼變得難看並且不易理解和維護,重構的過程也會充滿各種陷阱。優點是這種方式會最原始最直接的,目前所有瀏覽器都能支持,所以也有很多人依然在用這種方式。
2、 事件監聽
爲了方便演示,下面示例使用JQuery的寫法。
var x = 0;
f1.on('done', f2);
function f1(){
setTimeout(function () {
x += 1;
f1.trigger('done');
}, 1000);
}
function f2() {
alert(x);
}
f1();
通過f1.on(‘done’, f2)添加事件監聽,當執行f1()方法,trigger觸發’done’事件,此時事件被捕捉並響應,執行f2方法。
使用事件監聽機制可以避免回調方法的多重嵌套,使代碼扁平化。但是使整個程序變成事件驅動模式,使流程更加不清晰(你有時很難看懂執行那個方法後觸發事件跳轉到另外的方法)。而且會破壞方法的原子性。
3、觀察者模式
觀察者模式與事件監聽機制類似,下面示例同樣使用JQuery的寫法。
var x = 0;
jQuery.subscribe('done', f2);
function f1(){
setTimeout(function () {
x += 1;
jQuery.publish('done');
}, 1000);
}
function f2() {
alert(x);
jQuery.unsubscribe("done", f2);
}
f1();
可以看到,代碼上跟事件監聽的差異不大,差別只是
f1.on(‘done’, f2)變成了jQuery.subscribe(‘done’, f2),
f1.trigger(‘done’))變成了jQuery.publish(‘done’)。
實際上前者使用的是監聽,而後者更類似於通知。與前者相比的話優點是通過信號來統一控制。
4、Promise對象
ES6 原生提供了 Promise 對象。
所謂 Promise,就是一個對象,用來傳遞異步操作的消息。它代表了某個未來纔會知道結果的事件(通常是一個異步操作),並且這個事件提供統一的 API,可供進一步處理。
ES6的Promise來源於Promise/A+。使用Promise來進行異步流程控制,有幾個需要注意的問題,
在We have a problem with promises一文中有很好的總結。
Promise 對象有以下兩個特點。
(1)對象的狀態不受外界影響。Promise 對象代表一個異步操作,有三種狀態:Pending(進行中)、Resolved(已完成,又稱 Fulfilled)和 Rejected(已失敗)。只有異步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。這也是 Promise 這個名字的由來,它的英語意思就是「承諾」,表示其他手段無法改變。
(2)一旦狀態改變,就不會再變,任何時候都可以得到這個結果。Promise 對象的狀態改變,只有兩種可能:從 Pending 變爲 Resolved 和從 Pending 變爲 Rejected。只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果。就算改變已經發生了,你再對 Promise 對象添加回調函數,也會立即得到這個結果。這與事件(Event)完全不同,事件的特點是,如果你錯過了它,再去監聽,是得不到結果的。
有了 Promise 對象,就可以將異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數。此外,Promise 對象提供統一的接口,使得控制異步操作更加容易。
Promise 也有一些缺點。首先,無法取消 Promise,一旦新建它就會立即執行,無法中途取消。其次,如果不設置回調函數,Promise 內部拋出的錯誤,不會反應到外部。第三,當處於 Pending 狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。
var promise = new Promise(function(resolve, reject) {
if (/* 異步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then(function(value) {
// success
}, function(value) {
// failure
});
Promise 構造函數接受一個函數作爲參數,該函數的兩個參數分別是 resolve 方法和 reject 方法。
如果異步操作成功,則用 resolve 方法將 Promise 對象的狀態,從「未完成」變爲「成功」(即從 pending 變爲 resolved);
如果異步操作失敗,則用 reject 方法將 Promise 對象的狀態,從「未完成」變爲「失敗」(即從 pending 變爲 rejected)。
更多關於Promise的特性,可以參考:阮一峯ECMAScript 6 入門
5、Generator方法
Generator 函數是協程在 ES6 的實現,最大特點就是可以交出函數的執行權(即暫停執行)。
function* gen(x){
var y = yield x + 2;
return y;
}
上面代碼就是一個 Generator 函數。它不同於普通函數,是可以暫停執行的,所以函數名之前要加星號,以示區別。
整個 Generator 函數就是一個封裝的異步任務,或者說是異步任務的容器。異步操作需要暫停的地方,都用 yield 語句註明。Generator 函數的執行方法如下。
var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }
上面代碼中,調用 Generator 函數,會返回一個內部指針(即遍歷器 )g 。這是 Generator 函數不同於普通函數的另一個地方,即執行它不會返回結果,返回的是指針對象。調用指針 g 的 next 方法,會移動內部指針(即執行異步任務的第一段),指向第一個遇到的 yield 語句,上例是執行到 x + 2 爲止。
換言之,next 方法的作用是分階段執行 Generator 函數。每次調用 next 方法,會返回一個對象,表示當前階段的信息( value 屬性和 done 屬性)。value 屬性是 yield 語句後面表達式的值,表示當前階段的值;done 屬性是一個布爾值,表示 Generator 函數是否執行完畢,即是否還有下一個階段。
更多關於Generator的特性,可以參考:Generator 函數的含義與用法
5、async/await
async/await是ES7引進的新特性,即async function和await關鍵字,目前ES7由於瀏覽器支持等原因還未能投入生產環境,我們也只能暫時瞭解一下。
以下是async/await的一個簡單示例:
async function testFun() {
let res, a, b, c, d;
try {
res = await f1(a, b);
res = await f2(c, res);
res = await f3(d);
return res;
} catch (err) {
return handleError(err);
}
}
testFun();
代碼跟Promise&Generator實現類似,此處不加以贅述。
以上就是目前JavaScript中實現異步的一些方法,有些直接複製他人的博文,參考的文章也均已給出,裏面有更加詳細的介紹。