一.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ì)列"文判,看看里面有哪些事件过椎。那些對(duì)應(yīng)的異步任務(wù),于是結(jié)束等待狀態(tài)戏仓,進(jìn)入執(zhí)行棧疚宇,開始執(zhí)行亡鼠。
(4)主線程不斷重復(fù)上面的第三步,形成一個(gè)事件的循環(huán)敷待。
事件循環(huán)機(jī)制示意圖
- 阻塞非阻塞和同步異步的主要區(qū)別在于前者是相對(duì)于調(diào)用者來說间涵,后者是相對(duì)于被調(diào)用者來說。舉個(gè)栗子榜揖,把js比作一個(gè)老公的話勾哩,有一天上班的時(shí)候老公在微信約她老婆今天晚上去吃飯,如果老婆看到消息后馬上同意或者拒絕举哟,對(duì)老婆來說這就是同步(老公的消息被老婆返回了思劳,同時(shí)也得到了結(jié)果),如果老婆看到消息后回復(fù)說我晚上可能會(huì)加班還不確定妨猩,過段時(shí)間確定了我再來發(fā)條消息通知你結(jié)果(可以理解為回調(diào)函數(shù))潜叛,對(duì)老婆來說這就是異步(老公的消息被老婆返回了,但是還沒得到結(jié)果壶硅,需要等待)威兜。而在老婆還沒有給出最終通知結(jié)果時(shí)(不管是同步回復(fù)還是異步回復(fù)),如果此時(shí)老公打開另一個(gè)微信窗口約小三明天晚上去吃飯森瘪,此時(shí)對(duì)老公來說就是非阻塞的牡属,而如果老公在老婆沒有最終通知結(jié)果之前一直在那等著而沒干其他事情,對(duì)老公來說這就是阻塞的扼睬。顯而易見逮栅,在這里老公是調(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í)很長(zhǎng)的操作都應(yīng)該異步執(zhí)行,避免瀏覽器失去響應(yīng),最好的例子就是Ajax操作拓哺。在服務(wù)器端勇垛,"異步模式"甚至是唯一的模式,因?yàn)閳?zhí)行環(huán)境是單線程的士鸥,如果允許同步執(zhí)行所有http請(qǐng)求闲孤,服務(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):簡(jiǎ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é)的"事件",完全可以理解成"信號(hào)"。假定阿宅,存在一個(gè)"信號(hào)中心"候衍,某個(gè)任務(wù)執(zhí)行完成,就向信號(hào)中心"發(fā)布"(publish)一個(gè)信號(hào)洒放,其他任務(wù)可以向信號(hào)中心"訂閱"(subscribe)這個(gè)信號(hào)蛉鹿,從而知道什么時(shí)候自己可以開始執(zhí)行。這就叫做"發(fā)布/訂閱模式"往湿,又稱"觀察者模式"妖异。
element.subscribe("event", taskFun2);
function taskFun1(){
setTimeout(function () {
// taskFun1的任務(wù)代碼
element.publish("event");
}, 2000);
}
- 優(yōu)點(diǎn):可以完全掌握事件被訂閱的次數(shù),以及訂閱者的信息领追,管理起來特別方便他膳。
4.Promise對(duì)象
關(guān)于Promises的具體介紹和實(shí)現(xiàn),可以參考用ES6實(shí)現(xiàn)一個(gè)簡(jiǎn)單易懂的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):寫法和理解起來都相對(duì)費(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對(duì)象壕探,實(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)也比較簡(jiǎn)單,被認(rèn)為是目前最優(yōu)的異步流程控制模式李请。