理解javascript異步編程
一、異步機(jī)制
JavaScript的執(zhí)行環(huán)境是單線程的,單線程的好處是執(zhí)行環(huán)境簡(jiǎn)單,不用去考慮諸如資源同步,死鎖等多線程阻塞式編程等所需要面對(duì)的惱人的問題。但帶來的壞處是當(dāng)一個(gè)任務(wù)執(zhí)行時(shí)間較長(zhǎng)時(shí),后面的任務(wù)會(huì)等待很長(zhǎng)時(shí)間。在瀏覽器端就會(huì)出現(xiàn)瀏覽器假死,鼠標(biāo)無法響應(yīng)等情況。所以在瀏覽器端,耗時(shí)很長(zhǎng)的操作都應(yīng)該異步執(zhí)行,避免瀏覽器失去響應(yīng)。所謂異步執(zhí)行,不同于同步執(zhí)行(程序的執(zhí)行順序與任務(wù)的排列順序是一致的、同步的),每一個(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ù)的排列順序是不一致的、異步的。既然Javascript是單線程的,那它又如何能夠異步的執(zhí)行呢?
二、Javascript線程模型和事件驅(qū)動(dòng)
JavaScript有一個(gè)基于事件循環(huán)的并發(fā)模式。這個(gè)模式與C語言和java有很大不同。
運(yùn)行時(shí)的概念
棧
函數(shù)調(diào)用形成堆棧幀。
function f(b){ var a = 12; return a+b+35; } function g(x){ var m = 4; return f(m*x); } g(21);
當(dāng)調(diào)用函數(shù)g時(shí),創(chuàng)建第一個(gè)包含g參數(shù)和局部變量的幀。當(dāng)g函數(shù)調(diào)用f函數(shù)時(shí),創(chuàng)建包含f參數(shù)和局部變量第二個(gè)堆棧幀并推到第一個(gè)堆棧幀的頂部。當(dāng)f返回時(shí),頂部的堆棧幀元素被彈出(只留下g調(diào)用)。當(dāng)g函數(shù)返回時(shí),堆棧為空。
堆
堆是一個(gè)大型的非結(jié)構(gòu)化區(qū)域,對(duì)象被分配到堆中。
隊(duì)列
一個(gè)javascript運(yùn)行環(huán)境包含一個(gè)信息隊(duì)列,這個(gè)隊(duì)列是一系列將被執(zhí)行的信息列表。每一個(gè)消息被關(guān)聯(lián)到一個(gè)函數(shù)上。當(dāng)堆棧為空時(shí),從消息隊(duì)列中取出一個(gè)消息并進(jìn)行處理。該處理包含調(diào)用相關(guān)的函數(shù)(以及因此產(chǎn)生一個(gè)初始化的堆棧幀)。當(dāng)堆棧再次為空時(shí),消息處理結(jié)束。
事件循環(huán)
事件循環(huán)的名字源于它的實(shí)現(xiàn),經(jīng)常像下面這樣:
while(queue.waitForMessage()){ queue.processNextMessage(); }
queue.waitForMessage同步等待一個(gè)消息。
1、運(yùn)行到完成
每個(gè)消息完全處理之后,其它消息才會(huì)被處理。這樣的好處就是當(dāng)一個(gè)函數(shù)不能被提前,只能等其他函數(shù)執(zhí)行完畢(并且可以修改數(shù)據(jù)的函數(shù)操作)。這不同于C,例如,如果一個(gè)函數(shù)在一個(gè)線程運(yùn)行時(shí),它可以停在任何點(diǎn)運(yùn)行在另一個(gè)線程一些其他的代碼。這種模式的缺點(diǎn)是,如果一個(gè)消息時(shí)間過長(zhǎng)完成,Web應(yīng)用程序無法處理像點(diǎn)擊或滾動(dòng)的用戶交互。該瀏覽器可緩解此與“腳本花費(fèi)的時(shí)間太長(zhǎng)運(yùn)行”對(duì)話框。一個(gè)很好的做法,遵循的是使信息處理短,如果可能削減一個(gè)消息到幾條消息。
2、添加消息
在網(wǎng)頁瀏覽器中,事件可以在任何時(shí)候添加,一個(gè)事件發(fā)生并伴隨事件監(jiān)聽綁定到事件上。如果沒有事件監(jiān)聽,則事件丟失。就像點(diǎn)擊一個(gè)元素,元素上綁定點(diǎn)擊事件。調(diào)用setTimeout時(shí),當(dāng)函數(shù)的第二個(gè)參數(shù)時(shí)間被傳遞進(jìn)去,將添加一個(gè)消息到隊(duì)列中。如果在隊(duì)列中沒有其他消息,該消息被立即處理;然而,如果有消息,則setTimeout的信息將必須等待其它消息以進(jìn)行處理。由于這個(gè)原因,第二個(gè)參數(shù)是最小的時(shí)間,而不是一個(gè)保證時(shí)間。
3、幾個(gè)運(yùn)行環(huán)境之間的通信
一個(gè)web worker或跨域iframe都有自己的堆棧,堆,和消息隊(duì)列。兩個(gè)不同的運(yùn)行環(huán)境只能通過postMessage的方法發(fā)送消息進(jìn)行通信。這種方法增加了一個(gè)消息到其他運(yùn)行時(shí),如果后者監(jiān)聽消息事件。
從不阻塞
事件循環(huán)模型是javascript的一個(gè)很有意思的屬性,不像其它語言,它從不阻塞。假定瀏覽器中有一個(gè)專門用于事件調(diào)度的實(shí)例(該實(shí)例可以是一個(gè)線程,我們可以稱之為事件分發(fā)線程event dispatch thread),該實(shí)例的工作就是一個(gè)不結(jié)束的循環(huán),從事件隊(duì)列中取出事件,處理所有很事件關(guān)聯(lián)的回調(diào)函數(shù)(event handler)。注意回調(diào)函數(shù)是在Javascript的主線程中運(yùn)行的,而非事件分發(fā)線程中,以保證事件處理不會(huì)發(fā)生阻塞。通過事件和回調(diào)的I/O操作是一個(gè)典型的表現(xiàn),所以當(dāng)應(yīng)用等待索引型數(shù)據(jù)庫(kù)查詢返回或XHR請(qǐng)求返回時(shí),它仍然可以處理其他事情比如用戶輸入。
三、回調(diào)
回調(diào)是javascript的基礎(chǔ),函數(shù)被作為參數(shù)進(jìn)行傳遞。像下面:
f1(); f2(); f3();
如果f1中執(zhí)行了大量的耗時(shí)操作,而且f2需要在f1之后執(zhí)行。則程序可以改為回調(diào)的形式。如下:
function f1(callback){ setTimeout(function () { // f1的大量耗時(shí)任務(wù)代碼并的到三個(gè)結(jié)果i,l,you. console.log("this is function1"); var i = "i", l = "love", y = "you"; if (callback && typeof(callback) === "function") { callback(i,l,y); } }, 50); } function f2(a, b, c) { alert(a + " " + b + " " + c); console.log("this is function2"); } function f3(){console.log("this is function3");} f1(f2); f3();
運(yùn)行結(jié)果:
this is function3 this is function1 i love you this is function2
采用這種方式,我們把同步操作變成了異步操作,f1不會(huì)堵塞程序運(yùn)行,相當(dāng)于先執(zhí)行程序的主要邏輯,將耗時(shí)的操作推遲執(zhí)行。
回調(diào)函數(shù)的優(yōu)點(diǎn)是簡(jiǎn)單,輕量級(jí)(不需要額外的庫(kù))。缺點(diǎn)是各個(gè)部分之間高度耦合(Coupling),流程會(huì)很混亂,而且每個(gè)任務(wù)只能指定一個(gè)回調(diào)函數(shù)。某個(gè)操作需要經(jīng)過多個(gè)非阻塞的IO操作,每一個(gè)結(jié)果都是通過回調(diào),產(chǎn)生意大利面條式(spaghetti)的代碼。
operation1(function(err, result) { operation2(function(err, result) { operation3(function(err, result) { operation4(function(err, result) { operation5(function(err, result) { // do something useful }) }) }) }) })
四、事件監(jiān)聽
另一種思路是采用事件驅(qū)動(dòng)模式。任務(wù)的執(zhí)行不取決于代碼的順序,而取決于某個(gè)事件是否發(fā)生。
// plain, non-jQuery version of hooking up an event handler var clickity = document.getElementById("clickity"); clickity.addEventListener("click", function (e) { //console log, since it's like ALL real world scenarios, amirite? console.log("Alas, someone is pressing my buttons…"); }); // the obligatory jQuery version $("#clickity").on("click", function (e) { console.log("Alas, someone is pressing my buttons…"); });
也可以自定義事件進(jìn)行監(jiān)聽,關(guān)于自定義事件,屬于另外一部分的內(nèi)容。這種方法的優(yōu)點(diǎn)是比較容易理解,可以綁定多個(gè)事件,每個(gè)事件可以指定多個(gè)回調(diào)函數(shù),而且可以"去耦合"(Decoupling),有利于實(shí)現(xiàn)模塊化。缺點(diǎn)是整個(gè)程序都要變成事件驅(qū)動(dòng)型,運(yùn)行流程會(huì)變得很不清晰。
五、觀察者模式
我們假定,存在一個(gè)"信號(hào)中心",某個(gè)任務(wù)執(zhí)行完成,就向信號(hào)中心"發(fā)布"(publish)一個(gè)信號(hào),其他任務(wù)可以向信號(hào)中心"訂閱"(subscribe)這個(gè)信號(hào),從而知道什么時(shí)候自己可以開始執(zhí)行。這就叫做"發(fā)布/訂閱模式"(publish-subscribe pattern),又稱"觀察者模式"(observer pattern)。
var pubsub = (function(){ var q = {} topics = {}, subUid = -1; //發(fā)布消息 q.publish = function(topic, args) { if(!topics[topic]) {return;} var subs = topics[topic], len = subs.length; while(len--) { subs[len].func(topic, args); } return this; }; //訂閱事件 q.subscribe = function(topic, func) { topics[topic] = topics[topic] ? topics[topic] : []; var token = (++subUid).toString(); topics[topic].push({ token : token, func : func }); return token; }; return q; //取消訂閱就不寫了,遍歷topics,然后通過保存前面返回token,刪除指定元素 })(); //觸發(fā)的事件 var f2 = function(topics, data) { console.log("logging:" + topics + ":" + data); console.log("this is function2"); } function f1(){ setTimeout(function () { // f1的任務(wù)代碼 console.log("this is function1"); //發(fā)布消息'done' pubsub .publish('done', 'hello world'); }, 1000); } pubsub.subscribe('done', f2); f1();
上面代碼的運(yùn)行結(jié)果為:
this is function1 logging:done:hello world this is function2
觀察者模式的實(shí)現(xiàn)方法有很多種,也可以直接借用第三方庫(kù)。這種方法的性質(zhì)與"事件監(jiān)聽"類似(觀察者模式和自定義事件非常相似),但是明顯優(yōu)于后者。觀察者模式和事件監(jiān)聽一樣具有良好的去耦性,并且有一個(gè)消息中心,通過對(duì)消息中心的處理,可以良好地監(jiān)控程序運(yùn)行。
六、Promises對(duì)象
Promises的概念是由CommonJS小組的成員在 Promises/A規(guī)范 中提出來的。Promises被逐漸用作一種管理異步操作回調(diào)的方法,但出于它們的設(shè)計(jì),它們遠(yuǎn)比那個(gè)有用得多。Promise允許我們以同步的方式寫代碼,同時(shí)給予我們代碼的異步執(zhí)行。
function f1(){ var def = $.Deferred(); setTimeout(function () { // f1的任務(wù)代碼 console.log("this is f1"); def.resolve(); }, 500); return def.promise(); } function f2(){ console.log("this is f2"); } f1().then(f2);
上面代碼的運(yùn)行結(jié)果為:
this is f1 this is f2
上面引用的是jquery對(duì)Promises/A的實(shí)現(xiàn),jquery中還有一系列方法,具體可參考:Deferred Object.關(guān)于Promises,強(qiáng)烈建議讀一下You're Missing the Point of Promises.還有很多第三方庫(kù)實(shí)現(xiàn)了Promises,如:Q、Bluebird、 mmDeferred 等。Promise(中文:承諾)其實(shí)為一個(gè)有限狀態(tài)機(jī),共有三種狀態(tài):pending(執(zhí)行中)、fulfilled(執(zhí)行成功)和rejected(執(zhí)行失敗)。其中pending為初始狀態(tài),fulfilled和rejected為結(jié)束狀態(tài)(結(jié)束狀態(tài)表示promise的生命周期已結(jié)束)。狀態(tài)轉(zhuǎn)換關(guān)系為:pending->fulfilled,pending->rejected。隨著狀態(tài)的轉(zhuǎn)換將觸發(fā)各種事件(如執(zhí)行成功事件、執(zhí)行失敗事件等)。 下節(jié)具體講述狀態(tài)機(jī)實(shí)現(xiàn)js異步編程。
七、狀態(tài)機(jī)
Promises的本質(zhì)實(shí)際就是通過狀態(tài)機(jī)來實(shí)現(xiàn)的,把異步操作與對(duì)象的狀態(tài)改變掛鉤,當(dāng)異步操作結(jié)束的時(shí)候,發(fā)生相應(yīng)的狀態(tài)改變,由此再觸發(fā)其他操作。這要比回調(diào)函數(shù)、事件監(jiān)聽、發(fā)布/訂閱等解決方案,在邏輯上更合理,更易于降低代碼的復(fù)雜度。關(guān)于Promises可參考:JS魔法堂:剖析源碼理解Promises/A規(guī)范 。
八、ES6對(duì)異步的支持
這是一個(gè)新的技術(shù),成為2015年的ECMAScript(ES6)標(biāo)準(zhǔn)的一部分。該技術(shù)的規(guī)范已經(jīng)完成,但實(shí)施情況在不同的瀏覽器不同,在瀏覽器中的支持情況如下。
var f1 = new Promise(function(resolve, reject) { setTimeout(function () { // f1的任務(wù)代碼 console.log("this is f1"); resolve("Success"); }, 500); }); function f2(val){ console.log(val + ":" + "this is f2"); } function f3(){ console.log("this is f3") } f1.then(f2); f3();
以上代碼在Chrome 版本43中的運(yùn)行結(jié)果為:
this is f3 this is f1 Success:this is f2
以上就是針對(duì)javascript異步編程的了解學(xué)習(xí),之后還有相關(guān)文章進(jìn)行分享,不要錯(cuò)過哦。
相關(guān)文章
js獲取網(wǎng)頁可見區(qū)域、正文以及屏幕分辨率的高度
這篇文章主要介紹了js獲取網(wǎng)頁的各種高度,例如可見區(qū)域、正文以及屏幕分辨率的高度,需要的朋友可以參考下2014-05-05ie6下png圖片背景不透明的解決辦法使用js實(shí)現(xiàn)
我們時(shí)常在使用png圖片的時(shí)候,在ie6下發(fā)生背景不透明的問題,解決的方法實(shí)在是太多了,下面給大家介紹下一個(gè)js解決的方式,感興趣的朋友可以了解下的2013-01-01JS實(shí)現(xiàn)的base64加密解密完整實(shí)例
這篇文章主要介紹了JS實(shí)現(xiàn)的base64加密解密,以完整實(shí)例形式分析了JavaScript基于base64編碼實(shí)現(xiàn)加密與解密的具體步驟與相關(guān)技巧,并附帶了相關(guān)的加密解密在線工具地址供大家參考,需要的朋友可以參考下2016-04-04純JS實(shí)現(xiàn)根據(jù)CSS的class選擇DOM
這篇文章主要介紹了純JS實(shí)現(xiàn)根據(jù)CSS的class選擇DOM,需要的朋友可以參考下2014-03-03JavaScript實(shí)現(xiàn)重力下落與彈性效果的方法分析
這篇文章主要介紹了JavaScript實(shí)現(xiàn)重力下落與彈性效果的方法,結(jié)合實(shí)例形式分析了javascript重力下落及彈性效果的原理與具體實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-12-12jquery獲取URL中參數(shù)解決中文亂碼問題的兩種方法
從A頁面通過url傳參到B頁面時(shí),獲取URL中參數(shù)出現(xiàn)中文亂碼問題,解析url參數(shù)的正確方法如下,感興趣的朋友可以參考下2013-12-12javascript下使用Promise封裝FileReader
這篇文章主要介紹了javascript下使用Promise封裝FileReader,需要的朋友可以參考下2016-02-02