JavaScript異步流程控制全攻略

一.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)的異步流程控制模式李请。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瞧筛,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子导盅,更是在濱河造成了極大的恐慌较幌,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件白翻,死亡現(xiàn)場(chǎng)離奇詭異乍炉,居然都是意外死亡绢片,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門岛琼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來底循,“玉大人,你說我怎么就攤上這事槐瑞∥醯樱” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵困檩,是天一觀的道長(zhǎng)祠挫。 經(jīng)常有香客問我,道長(zhǎng)悼沿,這世上最難降的妖魔是什么等舔? 我笑而不...
    開封第一講書人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮显沈,結(jié)果婚禮上软瞎,老公的妹妹穿的比我還像新娘。我一直安慰自己拉讯,他們只是感情好涤浇,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著魔慷,像睡著了一般只锭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上院尔,一...
    開封第一講書人閱讀 51,443評(píng)論 1 302
  • 那天蜻展,我揣著相機(jī)與錄音,去河邊找鬼邀摆。 笑死纵顾,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的栋盹。 我是一名探鬼主播施逾,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼例获!你這毒婦竟也來了汉额?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤榨汤,失蹤者是張志新(化名)和其女友劉穎蠕搜,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體收壕,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡妓灌,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年轨蛤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片虫埂。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡俱萍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出告丢,到底是詐尸還是另有隱情,我是刑警寧澤损谦,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布岖免,位于F島的核電站,受9級(jí)特大地震影響照捡,放射性物質(zhì)發(fā)生泄漏颅湘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一栗精、第九天 我趴在偏房一處隱蔽的房頂上張望闯参。 院中可真熱鬧,春花似錦悲立、人聲如沸鹿寨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)脚草。三九已至,卻和暖如春原献,著一層夾襖步出監(jiān)牢的瞬間馏慨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工姑隅, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留写隶,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓讲仰,卻偏偏與公主長(zhǎng)得像慕趴,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子叮盘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容