JS異步操作—概述

單線程模型

單線程模型指的是饰躲,JavaScript 只在一個線程上運行。也就是說亚脆,JavaScript 同時只能執(zhí)行一個任務(wù)做院,其他任務(wù)都必須在后面排隊等待。

注意濒持,JavaScript 只在一個線程上運行键耕,不代表 JavaScript 引擎只有一個線程。事實上柑营,JavaScript 引擎有多個線程屈雄,單個腳本只能在一個線程上運行(稱為主線程),其他線程都是在后臺配合官套。

JavaScript 之所以采用單線程酒奶,而不是多線程蚁孔,跟歷史有關(guān)系。JavaScript 從誕生起就是單線程讥蟆,原因是不想讓瀏覽器變得太復(fù)雜勒虾,因為多線程需要共享資源、且有可能修改彼此的運行結(jié)果瘸彤,對于一種網(wǎng)頁腳本語言來說修然,這就太復(fù)雜了。如果 JavaScript 同時有兩個線程质况,一個線程在網(wǎng)頁 DOM 節(jié)點上添加內(nèi)容愕宋,另一個線程刪除了這個節(jié)點,這時瀏覽器應(yīng)該以哪個線程為準结榄?是不是還要有鎖機制中贝?所以,為了避免復(fù)雜性臼朗,JavaScript 一開始就是單線程邻寿,這已經(jīng)成了這門語言的核心特征,將來也不會改變视哑。

這種模式的好處是實現(xiàn)起來比較簡單绣否,執(zhí)行環(huán)境相對單純;壞處是只要有一個任務(wù)耗時很長挡毅,后面的任務(wù)都必須排隊等著蒜撮,會拖延整個程序的執(zhí)行。常見的瀏覽器無響應(yīng)(假死)跪呈,往往就是因為某一段 JavaScript 代碼長時間運行(比如死循環(huán))段磨,導(dǎo)致整個頁面卡在這個地方,其他任務(wù)無法執(zhí)行耗绿。JavaScript 語言本身并不慢苹支,慢的是讀寫外部數(shù)據(jù),比如等待 Ajax 請求返回結(jié)果缭乘。這個時候沐序,如果對方服務(wù)器遲遲沒有響應(yīng),或者網(wǎng)絡(luò)不通暢堕绩,就會導(dǎo)致腳本的長時間停滯。

如果排隊是因為計算量大邑时,CPU 忙不過來奴紧,倒也算了,但是很多時候 CPU 是閑著的晶丘,因為 IO 操作(輸入輸出)很慢(比如 Ajax 操作從網(wǎng)絡(luò)讀取數(shù)據(jù))黍氮,不得不等著結(jié)果出來唐含,再往下執(zhí)行。JavaScript 語言的設(shè)計者意識到沫浆,這時 CPU 完全可以不管 IO 操作捷枯,掛起處于等待中的任務(wù),先運行排在后面的任務(wù)专执。等到 IO 操作返回了結(jié)果淮捆,再回過頭,把掛起的任務(wù)繼續(xù)執(zhí)行下去本股。這種機制就是 JavaScript 內(nèi)部采用的“事件循環(huán)”機制(Event Loop)攀痊。

單線程模型雖然對 JavaScript 構(gòu)成了很大的限制,但也因此使它具備了其他語言不具備的優(yōu)勢拄显。如果用得好苟径,JavaScript 程序是不會出現(xiàn)堵塞的,這就是為什么 Node 可以用很少的資源躬审,應(yīng)付大流量訪問的原因棘街。

為了利用多核 CPU 的計算能力,HTML5 提出 Web Worker 標準承边,允許 JavaScript 腳本創(chuàng)建多個線程遭殉,但是子線程完全受主線程控制,且不得操作 DOM炒刁。所以恩沽,這個新標準并沒有改變 JavaScript 單線程的本質(zhì)。

同步任務(wù)和異步任務(wù)

程序里面所有的任務(wù)翔始,可以分成兩類:同步任務(wù)(synchronous)和異步任務(wù)(asynchronous)罗心。

同步任務(wù)是那些沒有被引擎掛起、在主線程上排隊執(zhí)行的任務(wù)城瞎。只有前一個任務(wù)執(zhí)行完畢渤闷,才能執(zhí)行后一個任務(wù)。

異步任務(wù)是那些被引擎放在一邊脖镀,不進入主線程飒箭、而進入任務(wù)隊列的任務(wù)。只有引擎認為某個異步任務(wù)可以執(zhí)行了(比如 Ajax 操作從服務(wù)器得到了結(jié)果)蜒灰,該任務(wù)(采用回調(diào)函數(shù)的形式)才會進入主線程執(zhí)行弦蹂。排在異步任務(wù)后面的代碼,不用等待異步任務(wù)結(jié)束會馬上運行强窖,也就是說凸椿,異步任務(wù)不具有“堵塞”效應(yīng)。

舉例來說翅溺,Ajax 操作可以當作同步任務(wù)處理脑漫,也可以當作異步任務(wù)處理髓抑,由開發(fā)者決定。如果是同步任務(wù)优幸,主線程就等著 Ajax 操作返回結(jié)果吨拍,再往下執(zhí)行;如果是異步任務(wù)网杆,主線程在發(fā)出 Ajax 請求以后羹饰,就直接往下執(zhí)行,等到 Ajax 操作有了結(jié)果跛璧,主線程再執(zhí)行對應(yīng)的回調(diào)函數(shù)严里。

任務(wù)隊列和事件循環(huán)

JavaScript 運行時,除了一個正在運行的主線程追城,引擎還提供一個任務(wù)隊列(task queue)刹碾,里面是各種需要當前程序處理的異步任務(wù)。(實際上座柱,根據(jù)異步任務(wù)的類型迷帜,存在多個任務(wù)隊列。為了方便理解色洞,這里假設(shè)只存在一個隊列戏锹。)

首先,主線程會去執(zhí)行所有的同步任務(wù)火诸。等到同步任務(wù)全部執(zhí)行完锦针,就會去看任務(wù)隊列里面的異步任務(wù)。如果滿足條件置蜀,那么異步任務(wù)就重新進入主線程開始執(zhí)行奈搜,這時它就變成同步任務(wù)了。等到執(zhí)行完盯荤,下一個異步任務(wù)再進入主線程開始執(zhí)行馋吗。一旦任務(wù)隊列清空,程序就結(jié)束執(zhí)行秋秤。

異步任務(wù)的寫法通常是回調(diào)函數(shù)宏粤。一旦異步任務(wù)重新進入主線程,就會執(zhí)行對應(yīng)的回調(diào)函數(shù)灼卢。如果一個異步任務(wù)沒有回調(diào)函數(shù)绍哎,就不會進入任務(wù)隊列,也就是說鞋真,不會重新進入主線程蛇摸,因為沒有用回調(diào)函數(shù)指定下一步的操作。

JavaScript 引擎怎么知道異步任務(wù)有沒有結(jié)果灿巧,能不能進入主線程呢赶袄?答案就是引擎在不停地檢查,一遍又一遍抠藕,只要同步任務(wù)執(zhí)行完了饿肺,引擎就會去檢查那些掛起來的異步任務(wù),是不是可以進入主線程了盾似。這種循環(huán)檢查的機制敬辣,就叫做事件循環(huán)(Event Loop)。

異步操作的模式

下面總結(jié)一下異步操作的幾種模式零院。

回調(diào)函數(shù)

回調(diào)函數(shù)是異步操作最基本的方法溉跃。

下面是兩個函數(shù)f1f2,編程的意圖是f2必須等到f1執(zhí)行完成告抄,才能執(zhí)行撰茎。

function f1() {
  // ...
}
function f2() {
  // ...
}

f1();
f2();

上面代碼的問題在于,如果f1是異步操作打洼,f2會立即執(zhí)行龄糊,不會等到f1結(jié)束再執(zhí)行。

這時募疮,可以考慮改寫f1炫惩,把f2寫成f1的回調(diào)函數(shù)。

function f1(callback) {
  // ...
  callback();
}
function f2() {
  // ...
}

f1(f2);

回調(diào)函數(shù)的優(yōu)點是簡單阿浓、容易理解和實現(xiàn)他嚷,缺點是不利于代碼的閱讀和維護,各個部分之間高度耦合(coupling)芭毙,使得程序結(jié)構(gòu)混亂筋蓖、流程難以追蹤(尤其是多個回調(diào)函數(shù)嵌套的情況),而且每個任務(wù)只能指定一個回調(diào)函數(shù)稿蹲。

事件監(jiān)聽

另一種思路是采用事件驅(qū)動模式扭勉。異步任務(wù)的執(zhí)行不取決于代碼的順序,而取決于某個事件是否發(fā)生苛聘。

還是以f1f2為例涂炎。首先,為f1綁定一個事件(這里采用的 jQuery 的寫法)设哗。

f1.on('done', f2);

上面這行代碼的意思是唱捣,當f1發(fā)生done事件,就執(zhí)行f2网梢。然后震缭,對f1進行改寫:

function f1() {
  setTimeout(function () {
    // ...
    f1.trigger('done');
  }, 1000);
}

上面代碼中,f1.trigger('done')表示战虏,執(zhí)行完成后拣宰,立即觸發(fā)done事件党涕,從而開始執(zhí)行f2

這種方法的優(yōu)點是比較容易理解巡社,可以綁定多個事件膛堤,每個事件可以指定多個回調(diào)函數(shù),而且可以“去耦合”(decoupling)晌该,有利于實現(xiàn)模塊化肥荔。缺點是整個程序都要變成事件驅(qū)動型,運行流程會變得很不清晰朝群。閱讀代碼的時候燕耿,很難看出主流程。

發(fā)布/訂閱

事件完全可以理解成“信號”姜胖,如果存在一個“信號中心”誉帅,某個任務(wù)執(zhí)行完成,就向信號中心“發(fā)布”(publish)一個信號谭期,其他任務(wù)可以向信號中心“訂閱”(subscribe)這個信號堵第,從而知道什么時候自己可以開始執(zhí)行。這就叫做”發(fā)布/訂閱模式”(publish-subscribe pattern)隧出,又稱觀察者模式踏志。

這個模式有多種實現(xiàn),下面采用的是 Ben Alman 的Tiny Pub/Sub胀瞪,這是 jQuery 的一個插件针余。

首先,f2向信號中心jQuery訂閱done信號凄诞。

jQuery.subscribe('done', f2);

然后圆雁,f1進行如下改寫。

function f1() {
  setTimeout(function () {
    // ...
    jQuery.publish('done');
  }, 1000);
}

上面代碼中帆谍,jQuery.publish('done')的意思是伪朽,f1執(zhí)行完成后,向信號中心jQuery發(fā)布done信號汛蝙,從而引發(fā)f2的執(zhí)行烈涮。

f2完成執(zhí)行后,可以取消訂閱(unsubscribe)窖剑。

jQuery.unsubscribe('done', f2);

這種方法的性質(zhì)與“事件監(jiān)聽”類似坚洽,但是明顯優(yōu)于后者。因為可以通過查看“消息中心”西土,了解存在多少信號讶舰、每個信號有多少訂閱者,從而監(jiān)控程序的運行。

異步操作的流程控制

如果有多個異步操作跳昼,就存在一個流程控制的問題:如何確定異步操作執(zhí)行的順序般甲,以及如何保證遵守這種順序。

function async(arg, callback) {
  console.log('參數(shù)為 ' + arg +' , 1秒后返回結(jié)果');
  setTimeout(function () { callback(arg * 2); }, 1000);
}

上面代碼的async函數(shù)是一個異步任務(wù)庐舟,非常耗時欣除,每次執(zhí)行需要1秒才能完成,然后再調(diào)用回調(diào)函數(shù)挪略。

如果有六個這樣的異步任務(wù),需要全部完成后滔岳,才能執(zhí)行最后的final函數(shù)杠娱。請問應(yīng)該如何安排操作流程?

function final(value) {
  console.log('完成: ', value);
}

async(1, function (value) {
  async(2, function (value) {
    async(3, function (value) {
      async(4, function (value) {
        async(5, function (value) {
          async(6, final);
        });
      });
    });
  });
});
// 參數(shù)為 1 , 1秒后返回結(jié)果
// 參數(shù)為 2 , 1秒后返回結(jié)果
// 參數(shù)為 3 , 1秒后返回結(jié)果
// 參數(shù)為 4 , 1秒后返回結(jié)果
// 參數(shù)為 5 , 1秒后返回結(jié)果
// 參數(shù)為 6 , 1秒后返回結(jié)果
// 完成:  12

上面代碼中谱煤,六個回調(diào)函數(shù)的嵌套摊求,不僅寫起來麻煩,容易出錯刘离,而且難以維護室叉。

串行執(zhí)行

我們可以編寫一個流程控制函數(shù),讓它來控制異步任務(wù)硫惕,一個任務(wù)完成以后茧痕,再執(zhí)行另一個。這就叫串行執(zhí)行恼除。

var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];

function async(arg, callback) {
  console.log('參數(shù)為 ' + arg +' , 1秒后返回結(jié)果');
  setTimeout(function () { callback(arg * 2); }, 1000);
}

function final(value) {
  console.log('完成: ', value);
}

function series(item) {
  if(item) {
    async( item, function(result) {
      results.push(result);
      return series(items.shift());
    });
  } else {
    return final(results[results.length - 1]);
  }
}

series(items.shift());

上面代碼中踪旷,函數(shù)series就是串行函數(shù),它會依次執(zhí)行異步任務(wù)豁辉,所有任務(wù)都完成后令野,才會執(zhí)行final函數(shù)。items數(shù)組保存每一個異步任務(wù)的參數(shù)徽级,results數(shù)組保存每一個異步任務(wù)的運行結(jié)果气破。

注意,上面的寫法需要六秒餐抢,才能完成整個腳本现使。

并行執(zhí)行

流程控制函數(shù)也可以是并行執(zhí)行,即所有異步任務(wù)同時執(zhí)行弹澎,等到全部完成以后朴下,才執(zhí)行final函數(shù)。

var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];

function async(arg, callback) {
  console.log('參數(shù)為 ' + arg +' , 1秒后返回結(jié)果');
  setTimeout(function () { callback(arg * 2); }, 1000);
}

function final(value) {
  console.log('完成: ', value);
}

items.forEach(function(item) {
  async(item, function(result){
    results.push(result);
    if(results.length === items.length) {
      final(results[results.length - 1]);
    }
  })
});

上面代碼中苦蒿,forEach方法會同時發(fā)起六個異步任務(wù)殴胧,等到它們?nèi)客瓿梢院螅艜?zhí)行final函數(shù)。

相比而言团滥,上面的寫法只要一秒竿屹,就能完成整個腳本。這就是說灸姊,并行執(zhí)行的效率較高拱燃,比起串行執(zhí)行一次只能執(zhí)行一個任務(wù),較為節(jié)約時間力惯。但是問題在于如果并行的任務(wù)較多碗誉,很容易耗盡系統(tǒng)資源,拖慢運行速度父晶。因此有了第三種流程控制方式哮缺。

并行與串行的結(jié)合

所謂并行與串行的結(jié)合,就是設(shè)置一個門檻甲喝,每次最多只能并行執(zhí)行n個異步任務(wù)尝苇,這樣就避免了過分占用系統(tǒng)資源。

var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
var running = 0;
var limit = 2;

function async(arg, callback) {
  console.log('參數(shù)為 ' + arg +' , 1秒后返回結(jié)果');
  setTimeout(function () { callback(arg * 2); }, 1000);
}

function final(value) {
  console.log('完成: ', value);
}

function launcher() {
  while(running < limit && items.length > 0) {
    var item = items.shift();
    async(item, function(result) {
      results.push(result);
      running--;
      if(items.length > 0) {
        launcher();
      } else if(running == 0) {
        final(results);
      }
    });
    running++;
  }
}

launcher();

上面代碼中埠胖,最多只能同時運行兩個異步任務(wù)糠溜。變量running記錄當前正在運行的任務(wù)數(shù),只要低于門檻值直撤,就再啟動一個新的任務(wù)非竿,如果等于0,就表示所有任務(wù)都執(zhí)行完了谊惭,這時就執(zhí)行final函數(shù)汽馋。

這段代碼需要三秒完成整個腳本,處在串行執(zhí)行和并行執(zhí)行之間圈盔。通過調(diào)節(jié)limit變量豹芯,達到效率和資源的最佳平衡。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末驱敲,一起剝皮案震驚了整個濱河市铁蹈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌众眨,老刑警劉巖握牧,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異娩梨,居然都是意外死亡沿腰,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門狈定,熙熙樓的掌柜王于貴愁眉苦臉地迎上來颂龙,“玉大人习蓬,你說我怎么就攤上這事章咧∷帕保” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵蓄坏,是天一觀的道長企巢。 經(jīng)常有香客問我枫慷,道長,這世上最難降的妖魔是什么浪规? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任或听,我火速辦了婚禮,結(jié)果婚禮上罗丰,老公的妹妹穿的比我還像新娘神帅。我一直安慰自己,他們只是感情好萌抵,可當我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著元镀,像睡著了一般绍填。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上栖疑,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天讨永,我揣著相機與錄音,去河邊找鬼遇革。 笑死卿闹,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的萝快。 我是一名探鬼主播锻霎,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼揪漩!你這毒婦竟也來了旋恼?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤奄容,失蹤者是張志新(化名)和其女友劉穎冰更,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體昂勒,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡蜀细,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了戈盈。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奠衔。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出涣觉,到底是詐尸還是另有隱情痴荐,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布官册,位于F島的核電站生兆,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏膝宁。R本人自食惡果不足惜鸦难,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望员淫。 院中可真熱鬧合蔽,春花似錦、人聲如沸介返。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽圣蝎。三九已至刃宵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間徘公,已是汗流浹背牲证。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留关面,地道東北人坦袍。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像等太,于是被迫代替她去往敵國和親捂齐。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,543評論 2 349

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