一.js異步流程的由來
? ?? ?眾所周知,Javascript語言的執(zhí)行環(huán)境是單線程(single thread),作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動(dòng),以及操作DOM。若以多線程的方式操作這些DOM,則可能出現(xiàn)操作的沖突。假設(shè)有兩個(gè)線程同時(shí)操作一個(gè)DOM元素,線程1要求瀏覽器刪除DOM,而線程2卻要求修改DOM樣式,這時(shí)瀏覽器就無法決定采用哪個(gè)線程的操作。當(dāng)然,我們可以為瀏覽器引入“鎖”的機(jī)制來解決這些沖突,但這會(huì)大大提高復(fù)雜性,所以JavaScript從誕生開始就選擇了單線程執(zhí)行。
? ?? ?而單線程就是指一次只能完成一件任務(wù)。如果有多個(gè)任務(wù),就必須排隊(duì),前面一個(gè)任務(wù)完成,再執(zhí)行后面一個(gè)任務(wù)。因?yàn)閖avascript 設(shè)計(jì)之初是為瀏覽器設(shè)計(jì)的GUI編程語言,GUI編程的特性之一是保證UI線程一定不能阻塞,否則性能不好,可能會(huì)界面卡死,因?yàn)镴avaScript是單線程的,有一個(gè)致命問題是在某一時(shí)刻內(nèi)只能執(zhí)行特定的一個(gè)任務(wù),并且會(huì)阻塞其它任務(wù)執(zhí)行,為了解決這個(gè)問題,Javascript語言將任務(wù)的執(zhí)行模式分成同步(Synchronous)和異步(Asynchronous),在遇到類似I/O等耗時(shí)的任務(wù)時(shí)js會(huì)采用異步操作,而此時(shí)異步操作不進(jìn)入主線程、而進(jìn)入"任務(wù)隊(duì)列",只有"任務(wù)隊(duì)列"通知主線程,某個(gè)異步任務(wù)可以執(zhí)行了,該任務(wù)才會(huì)進(jìn)入主線程執(zhí)行,這時(shí)就不會(huì)阻塞其它任務(wù)執(zhí),而這種模式稱為js的事件循環(huán)機(jī)制(Event Loop)。
- 同步:調(diào)用者發(fā)出調(diào)用后,在沒有得到結(jié)果之前,該調(diào)用就不返回。后一個(gè)任務(wù)等待前一個(gè)任務(wù)結(jié)束,然后再執(zhí)行,程序的執(zhí)行順序與任務(wù)的排列順序是一致的、同步的,具有同步關(guān)系的一組任務(wù)相互發(fā)送的信息稱為消息或事件。
- 異步:調(diào)用者發(fā)出調(diào)用后不會(huì)立刻得到結(jié)果,該調(diào)用就返回了。每一個(gè)任務(wù)有一個(gè)或多個(gè)回調(diào)函數(shù)(callback),前一個(gè)任務(wù)結(jié)束后,不是執(zhí)行后一個(gè)任務(wù),而是執(zhí)行回調(diào)函數(shù),后一個(gè)任務(wù)則是不等前一個(gè)任務(wù)結(jié)束就執(zhí)行,所以程序的執(zhí)行順序與任務(wù)的排列順序是不一致的、異步的,線程就是實(shí)現(xiàn)異步的一個(gè)方式,異步是讓調(diào)用方法的主線程不需要同步等待另一線程的完成,從而可以讓主線程干其它的事情。
- 阻塞:指調(diào)用結(jié)果返回之前,調(diào)用者會(huì)進(jìn)入阻塞狀態(tài)等待。只有在得到結(jié)果之后才會(huì)返回。
- 非阻塞:指在不能立刻得到結(jié)果之前,該函數(shù)不會(huì)阻塞當(dāng)前線程,而會(huì)立刻返回。
- 事件循環(huán)機(jī)制:
(1)所有同步任務(wù)都在主線程上執(zhí)行,形成一個(gè)執(zhí)行棧(execution context stack)。
(2)主線程之外,還存在一個(gè)"任務(wù)隊(duì)列"(task queue)。只要異步任務(wù)有了運(yùn)行結(jié)果,就在"任務(wù)隊(duì)列"之中放置一個(gè)事件。
(3)一旦"執(zhí)行棧"中的所有同步任務(wù)執(zhí)行完畢,系統(tǒng)就會(huì)讀取"任務(wù)隊(duì)列",看看里面有哪些事件。那些對應(yīng)的異步任務(wù),于是結(jié)束等待狀態(tài),進(jìn)入執(zhí)行棧,開始執(zhí)行。
(4)主線程不斷重復(fù)上面的第三步,形成一個(gè)事件的循環(huán)。
事件循環(huán)機(jī)制示意圖
- 阻塞非阻塞和同步異步的主要區(qū)別在于前者是相對于調(diào)用者來說,后者是相對于被調(diào)用者來說。舉個(gè)栗子,把js比作一個(gè)老公的話,有一天上班的時(shí)候老公在微信約她老婆今天晚上去吃飯,如果老婆看到消息后馬上同意或者拒絕,對老婆來說這就是同步(老公的消息被老婆返回了,同時(shí)也得到了結(jié)果),如果老婆看到消息后回復(fù)說我晚上可能會(huì)加班還不確定,過段時(shí)間確定了我再來發(fā)條消息通知你結(jié)果(可以理解為回調(diào)函數(shù)),對老婆來說這就是異步(老公的消息被老婆返回了,但是還沒得到結(jié)果,需要等待)。而在老婆還沒有給出最終通知結(jié)果時(shí)(不管是同步回復(fù)還是異步回復(fù)),如果此時(shí)老公打開另一個(gè)微信窗口約小三明天晚上去吃飯,此時(shí)對老公來說就是非阻塞的,而如果老公在老婆沒有最終通知結(jié)果之前一直在那等著而沒干其他事情,對老公來說這就是阻塞的。顯而易見,在這里老公是調(diào)用者,老婆是被調(diào)用者。
- 還是上面那個(gè)栗子,如果老婆說要過段時(shí)間才能通知老公最后結(jié)果(也就是異步的時(shí)候),此時(shí)老公也不能在老婆通知前什么都不干就待在那里,老公沒有分身,也就是說老公不是多線程的,他會(huì)把這個(gè)異步事件先擱置(也就是放到任務(wù)隊(duì)列里) ,作為單線程的他只能親自去處理其他事情(主線程中處理執(zhí)行棧),等老婆通知后再來處理這件事情(把這個(gè)異步事件從任務(wù)隊(duì)列中取回來在主線程中執(zhí)行)。所以當(dāng)js采用異步模式的時(shí)候js就是非阻塞了,這也就是為什么說node.js是非阻塞異步I/O了,因?yàn)楫惒胶褪录h(huán)機(jī)制的特性使它是非阻塞的。
二.js為什么要演進(jìn)異步流程控制
? ?? ?"異步模式"非常重要。在瀏覽器端,耗時(shí)很長的操作都應(yīng)該異步執(zhí)行,避免瀏覽器失去響應(yīng),最好的例子就是Ajax操作。在服務(wù)器端,"異步模式"甚至是唯一的模式,因?yàn)閳?zhí)行環(huán)境是單線程的,如果允許同步執(zhí)行所有http請求,服務(wù)器性能會(huì)急劇下降,很快就會(huì)失去響應(yīng)。最早異步模式采用的是回調(diào)函數(shù)的方法,但是這種方法不利于代碼的閱讀和維護(hù),各個(gè)部分之間高度耦合,流程會(huì)很混亂,而且每個(gè)任務(wù)只能指定一個(gè)回調(diào)函數(shù),這樣就很容易陷入回調(diào)地獄,所以異步流程控制模式慢慢衍生出許多方式,下面主要來介紹這些方式有哪些。
三.js異步流程控制的幾種主要方式
1.回調(diào)函數(shù)
有兩個(gè)任務(wù)函數(shù)taskFun1和taskFun2,如果按同步方式寫
taskFun1();
taskFun2();
taskFun1()如果是一個(gè)很耗時(shí)的任務(wù),會(huì)嚴(yán)重阻塞taskFun2()的執(zhí)行,用回調(diào)函數(shù)可以這樣寫:
function taskFun1(callbackFun){
setTimeout(function () {
// do something
callbackFun();
}, 3000);
}
taskFun1(taskFun2);
- 優(yōu)點(diǎn):簡單、容易理解和部署,
- 缺點(diǎn):不利于代碼的閱讀和維護(hù),各個(gè)部分之間高度耦合,流程會(huì)很混亂,而且每個(gè)任務(wù)只能指定一個(gè)回調(diào)函數(shù)。
2.事件監(jiān)聽
另一種思路是采用事件驅(qū)動(dòng)模式。任務(wù)的執(zhí)行不取決于代碼的順序,而取決于某個(gè)事件是否發(fā)生。
taskFun1.on("event", taskFun2);
function taskFun1(){
setTimeout(function () {
// taskFun1的任務(wù)代碼
taskFun1.trigger('event');
}, 2000);
}
/* taskFun1.trigger('event')表示執(zhí)行完成后,立即觸發(fā)事件,從而開始執(zhí)行taskFun2。*/
- 優(yōu)點(diǎn):比較容易理解,可以綁定多個(gè)事件,每個(gè)事件可以指定多個(gè)回調(diào)函數(shù),耦合度很低,有利于實(shí)現(xiàn)模塊化
- 缺點(diǎn):整個(gè)程序都要變成事件驅(qū)動(dòng)型,事件不能得到流程控制,運(yùn)行流程會(huì)變得很不清晰。
3.發(fā)布/訂閱
上一節(jié)的"事件",完全可以理解成"信號"。假定,存在一個(gè)"信號中心",某個(gè)任務(wù)執(zhí)行完成,就向信號中心"發(fā)布"(publish)一個(gè)信號,其他任務(wù)可以向信號中心"訂閱"(subscribe)這個(gè)信號,從而知道什么時(shí)候自己可以開始執(zhí)行。這就叫做"發(fā)布/訂閱模式",又稱"觀察者模式"。
element.subscribe("event", taskFun2);
function taskFun1(){
setTimeout(function () {
// taskFun1的任務(wù)代碼
element.publish("event");
}, 2000);
}
- 優(yōu)點(diǎn):可以完全掌握事件被訂閱的次數(shù),以及訂閱者的信息,管理起來特別方便。
4.Promise對象
關(guān)于Promises的具體介紹和實(shí)現(xiàn),可以參考用ES6實(shí)現(xiàn)一個(gè)簡單易懂的Promise
比如平時(shí)我們常用的axios插件就是采用了promise模式:
axios.get('./demo.txt')
.then(function(response){
console.log(response);
})
.catch(function(err){
console.log(err);
});
而實(shí)現(xiàn)的機(jī)制就是promise把成功和失敗分別代理到resolved 和 rejected .
var promise = new Promise(function(resolve, reject) {
// 異步操作的代碼
if (/* 異步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
- 優(yōu)點(diǎn):回調(diào)函數(shù)變成了鏈?zhǔn)綄懛ǎ绦虻牧鞒炭梢钥吹煤芮宄?,可以?shí)現(xiàn)許多強(qiáng)大的功能,同時(shí)還可以捕獲到catch異常。
- 缺點(diǎn):寫法和理解起來都相對費(fèi)勁
5.Generator與co相結(jié)合
與promise不同的是,Generator設(shè)計(jì)的初衷并不是為了來控制異步流程的,這種寫法是express和koa框架的作者拿Generator與co相結(jié)合的一種寫法,由于generator是一個(gè)狀態(tài)機(jī),所以需要手動(dòng)調(diào)用next 才能執(zhí)行,node框架的作者開發(fā)了co模塊,可以自動(dòng)執(zhí)行g(shù)enerator,可以理解為一種geek寫法。
function readFile(filename) {
return new Promise(function (resolve, reject) {
fs.readFile(filename, 'utf8', function (err, data) {
err ? reject(err) : resolve(data);
});
})
}
function *read() {
console.log('開始');
let a = yield readFile('1.txt');
console.log(a);
let b = yield readFile('2.txt');
console.log(b);
let c = yield readFile('3.txt');
console.log(c);
return c;
}
co(read).then(function (data) {
console.log(data);
});
- 優(yōu)點(diǎn):可以用同步的方式編寫異步代碼
- 缺點(diǎn):不夠直觀,沒有語義化
6.await,async
await,async是ES7 引入了的關(guān)鍵字,async函數(shù)完全可以看作多個(gè)異步操作,包裝成的一個(gè)Promise對象,實(shí)質(zhì)上是generator+promise的語法糖
*async function read(){
//await后面必須跟一個(gè)promise,
let a = await readFile('./1.txt');
console.log(a);
let b = await readFile('./2.txt');
console.log(b);
let c = await readFile('./3.txt');
console.log(c);
return 'ok';
}*/
- 優(yōu)點(diǎn):相比于之前的方式有很好的語義,實(shí)現(xiàn)也比較簡單,被認(rèn)為是目前最優(yōu)的異步流程控制模式。
