摘要:單線程就意味著,所有任務需要排隊,前一個任務結束,才會執(zhí)行后一個任務。這決定了它只能是單線程,否則會帶來很復雜的同步問題。小結本身是單線程的,并沒有異步的特性。當異步函數(shù)執(zhí)行時,回調函數(shù)會被壓入這個隊列。
走在前端的大道上
本篇將自己讀過的相關 js異步 的文章中,對自己有啟發(fā)的章節(jié)片段總結在這(會對原文進行刪改),會不斷豐富提煉總結更新。
概念JS 是單線程的語言。
單線程就意味著,所有任務需要排隊,前一個任務結束,才會執(zhí)行后一個任務。如果前一個任務耗時很長,后一個任務就不得不一直等著。
優(yōu)缺點
這種模式的好處是實現(xiàn)起來比較簡單,執(zhí)行環(huán)境相對單純;壞處是只要有一個任務耗時很長,后面的任務都必須排隊等著,會拖延整個程序的執(zhí)行。常見的瀏覽器無響應(假死),往往就是因為某一段Javascript代碼長時間運行(比如死循環(huán)),導致整個頁面卡在這個地方,其他任務無法執(zhí)行。
為什么單線程
JavaScript的單線程,與它的用途有關。作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很復雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節(jié)點上添加內容,另一個線程刪除了這個節(jié)點,這時瀏覽器應該以哪個線程為準?
所以,為了避免復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特征,將來也不會改變。
為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript腳本創(chuàng)建多個線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個新標準并沒有改變JavaScript單線程的本質。
應對方法
為了解決這個問題,Javascript語言將任務的執(zhí)行模式分成兩種:同步(Synchronous)和異步(Asynchronous)。
于是,所有任務可以分成兩種,一種是同步任務(synchronous),另一種是異步任務(asynchronous)。同步任務指的是,在主線程上排隊執(zhí)行的任務,只有前一個任務執(zhí)行完畢,才能執(zhí)行后一個任務;異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務可以執(zhí)行了,該任務才會進入主線程執(zhí)行。
具體來說,異步執(zhí)行的運行機制如下。(同步執(zhí)行也是如此,因為它可以被視為沒有異步任務的異步執(zhí)行。)
(1)所有同步任務都在主線程上執(zhí)行,形成一個執(zhí)行棧(execution context stack)。 (2)主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。 (3)一旦"執(zhí)行棧"中的所有同步任務執(zhí)行完畢,系統(tǒng)就會讀取"任務隊列",看看里面有哪些事件。那些對應的異步任務,于是結束等待狀態(tài),進入執(zhí)行棧,開始執(zhí)行。 (4)主線程不斷重復上面的第三步。既是單線程又是異步難道不矛盾?
JS的單線程是指一個瀏覽器進程中只有一個JS的執(zhí)行線程,同一時刻內只會有一段代碼在執(zhí)行(你可以使用IE的標簽式瀏覽試試看效果,這時打開的多個頁面使用的都是同一個JS執(zhí)行線程,如果其中一個頁面在執(zhí)行一個運算量較大的function時,其他窗口的JS就會停止工作)。
而異步機制是瀏覽器的兩個或以上常駐線程共同完成的,例如異步請求是由兩個常駐線程:JS執(zhí)行線程 和 事件觸發(fā)線程 共同完成的,JS的 執(zhí)行線程 發(fā)起異步請求(這時瀏覽器會開一條新的 HTTP請求線程 來執(zhí)行請求,這時JS的任務已完成,繼續(xù)執(zhí)行線程隊列中剩下的其他任務),然后在未來的某一時刻 事件觸發(fā)線程 監(jiān)視到之前的發(fā)起的HTTP請求已完成,它就會把完成事件插入到JS執(zhí)行隊列的尾部等待JS處理。又例如定時觸發(fā)(settimeout和setinterval)是由瀏覽器的 定時器線程 執(zhí)行的定時計數(shù),然后在定時時間把定時處理函數(shù)的執(zhí)行請求插入到JS執(zhí)行隊列的尾端(所以用這兩個函數(shù)的時候,實際的執(zhí)行時間是大于或等于指定時間的,不保證能準確定時的)。所以,所謂的JS的單線程和異步更多的應該是屬于瀏覽器的行為,他們之間沒有沖突,更不是同一種事物,沒有什么區(qū)別不區(qū)別的。
小結:
Javascript 本身是單線程的,并沒有異步的特性。 由于 Javascript 的運用場景是瀏覽器,瀏覽器本身是典型的 GUI 工作線程,GUI 工作線程在絕大多數(shù)系統(tǒng)中都實現(xiàn)為事件處理,避免阻塞交互,因此產生了 Javascript 異步基因。此后種種都源于此。
JS 中異步操作還挺多的,常見的分以下幾種:
setTimeout (setInterval)
AJAX
Promise
async/await
setTimeoutJavaScript最基礎的異步函數(shù)是setTimeout和setInterval。setTimeout會在一定時間后執(zhí)行給定的函數(shù)。它接受一個回調函數(shù)作為第一參數(shù)和一個毫秒時間作為第二參數(shù)。
setTimeout( function() { console.log("Hello!"); }, 1000);
setTimout(setInterval)并不是立即就執(zhí)行的,這段代碼意思是,等 1s后,把這個 function 加入任務隊列中,如果任務隊列中沒有其他任務了,就執(zhí)行輸出 "Hello"。
var outerScopeVar; helloCatAsync(); console.log(outerScopeVar); function helloCatAsync() { setTimeout(function() { outerScopeVar = "hello"; }, 2000); }
執(zhí)行上面代碼,發(fā)現(xiàn) outerScopeVar 輸出是 undefined,而不是 hello。之所以這樣是因為在異步代碼中返回的一個值是不可能給同步流程中使用的,因為 console.log(outerScopeVar) 是同步代碼,執(zhí)行完后才會執(zhí)行 setTimout。
helloCatAsync(function(result) { console.log(result); }); function helloCatAsync(callback) { setTimeout( function() { callback("hello") } , 1000) }
把上面代碼改成,傳遞一個callback,console 輸出就會是 hello。
有趣的是,直到在同一程序段中所有其余的代碼執(zhí)行結束后,超時才會發(fā)生。所以如果設置了超時,同時執(zhí)行了需長時間運行的函數(shù),那么在該函數(shù)執(zhí)行完成之前,超時甚至都不會啟動。實際上,異步函數(shù),如setTimeout和setInterval,被壓入了稱之為Event Loop的隊列。
Event Loop是一個回調函數(shù)隊列。當異步函數(shù)執(zhí)行時,回調函數(shù)會被壓入這個隊列。JavaScript引擎直到異步函數(shù)執(zhí)行完成后,才會開始處理事件循環(huán)。這意味著JavaScript代碼不是多線程的,即使表現(xiàn)的行為相似。事件循環(huán)是一個先進先出(FIFO)隊列,這說明回調是按照它們被加入隊列的順序執(zhí)行的。
AJAXvar xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 ) { console.log(xhr.responseText); } else { console.log( xhr.status); } } xhr.open("GET", "url", false); xhr.send();
上面這段代碼,xhr.open 中第三個參數(shù)默認為 false 異步執(zhí)行,改為 true 時為同步執(zhí)行。
ES6 Promise語法:
new Promise( function(resolve, reject) {...});
console.dir(Promise) ? Promise() all:? all() arguments:(...) caller:(...) length:1 name:"Promise" prototype:Promise catch:? catch() constructor:? Promise() finally:? finally() then:? then() Symbol(Symbol.toStringTag):"Promise"__proto__:Object race:? race() reject:? reject() resolve:? resolve() Symbol(Symbol.species):(...) get Symbol(Symbol.species):? [Symbol.species]() __proto__:? () [[Scopes]]:Scopes[0]
這么一看就明白了,Promise是一個構造函數(shù),自己身上有all、reject、resolve這幾個眼熟的方法,原型上有then、catch等同樣很眼熟的方法。這么說用Promise new出來的對象肯定就有then、catch方法。
那就new一個玩玩吧。
var p = new Promise(function(resolve, reject){ //做一些異步操作 setTimeout(function(){ console.log("執(zhí)行完成"); resolve("隨便什么數(shù)據(jù)"); }, 2000); });
Promise的構造函數(shù)接收一個參數(shù),是函數(shù),并且傳入兩個參數(shù):resolve,reject,分別表示異步操作執(zhí)行成功后的回調函數(shù) 和 異步操作執(zhí)行失敗后的回調函數(shù)。其實這里用“成功”和“失敗”來描述并不準確,按照標準來講,resolve是將Promise的狀態(tài)置為fullfiled,reject是將Promise的狀態(tài)置為rejected,在遇到 resolve 或 reject之前,狀態(tài)一直是pending。
不過在我們開始階段可以先這么理解,后面再細究概念。
在上面的代碼中,我們執(zhí)行了一個異步操作,也就是setTimeout,2秒后,輸出“執(zhí)行完成”,并且調用resolve方法。 運行代碼,會在2秒后輸出“執(zhí)行完成”。注意!我只是new了一個對象,并沒有調用它,我們傳進去的函數(shù)就已經執(zhí)行了,這是需要注意的一個細節(jié)。所以我們用Promise的時候一般是包在一個函數(shù)中,在需要的時候去運行這個函數(shù),如:
function runAsync(){ var p = new Promise(function(resolve, reject){ //做一些異步操作 setTimeout(function(){ console.log("執(zhí)行完成"); resolve("隨便什么數(shù)據(jù)"); }, 2000); }); return p; } runAsync()
這時候你應該有兩個疑問:1.包裝這么一個函數(shù)有毛線用?2.resolve("隨便什么數(shù)據(jù)");這是干毛的? 我們繼續(xù)來講。在我們包裝好的函數(shù)最后,會return出Promise對象,也就是說,執(zhí)行這個函數(shù)我們得到了一個Promise對象。還記得Promise對象上有then、catch方法吧?這就是強大之處了,看下面的代碼:
runAsync().then(function(data){ console.log(data); //后面可以用傳過來的數(shù)據(jù)做些其他操作 //...... });
在 runAsync() 的返回上直接調用then方法,then接收一個參數(shù),是函數(shù),并且會拿到我們在runAsync 中調用resolve時傳的的參數(shù)。運行這段代碼,會在2秒后輸出“執(zhí)行完成”,緊接著輸出“隨便什么數(shù)據(jù)”。 這時候你應該有所領悟了,原來then里面的函數(shù)就跟我們平時的回調函數(shù)一個意思,能夠在runAsync這個異步任務執(zhí)行完成之后被執(zhí)行。這就是Promise的作用了,簡單來講,就是能把原來的回調寫法分離出來,在異步操作執(zhí)行完后,用鏈式調用的方式執(zhí)行回調函數(shù)。 你可能會不屑一顧,那么牛逼轟轟的Promise就這點能耐?我把回調函數(shù)封裝一下,給runAsync傳進去不也一樣嗎,就像這樣:
function runAsync(callback){ setTimeout(function(){ console.log("執(zhí)行完成"); callback("隨便什么數(shù)據(jù)"); }, 2000); } runAsync(function(data){ console.log(data); });
效果也是一樣的,還費勁用Promise干嘛。那么問題來了,有多層回調該怎么辦?如果callback也是一個異步操作,而且執(zhí)行完后也需要有相應的回調函數(shù),該怎么辦呢?總不能再定義一個callback2,然后給callback傳進去吧。而Promise的優(yōu)勢在于,可以在then方法中繼續(xù)寫Promise對象并返回,然后繼續(xù)調用then來進行回調操作。
鏈式操作的用法三個函數(shù)runAsync1、runAsync2、runAsync3
function runAsync1(){ var p = new Promise(function(resolve, reject){ //做一些異步操作 setTimeout(function(){ console.log("異步任務1執(zhí)行完成"); resolve("隨便什么數(shù)據(jù)1"); }, 1000); }); return p; } function runAsync2(){ var p = new Promise(function(resolve, reject){ //做一些異步操作 setTimeout(function(){ console.log("異步任務2執(zhí)行完成"); resolve("隨便什么數(shù)據(jù)2"); }, 2000); }); return p; } function runAsync3(){ var p = new Promise(function(resolve, reject){ //做一些異步操作 setTimeout(function(){ console.log("異步任務3執(zhí)行完成"); resolve("隨便什么數(shù)據(jù)3"); }, 2000); }); return p; }
使用Promise的正確場景是這樣的:
runAsync1() .then(function(data){ console.log(data); return runAsync2(); }) .then(function(data){ console.log(data); return runAsync3(); }) .then(function(data){ console.log(data); });
這樣能夠按順序,每隔兩秒輸出每個異步回調中的內容,在runAsync2中傳給resolve的數(shù)據(jù),能在接下來的then方法中拿到。運行結果如下:
從表面上看,Promise只是能夠簡化層層回調的寫法,而實質上,Promise的精髓是“狀態(tài)”,用維護狀態(tài)、傳遞狀態(tài)的方式來使得回調函數(shù)能夠及時調用,它比傳遞callback函數(shù)要簡單、靈活的多。
在then方法中,你也可以直接return數(shù)據(jù)而不是Promise對象,在后面的then中就可以接收到數(shù)據(jù)了,比如我們把上面的代碼修改成這樣:
runAsync1() .then(function(data){ console.log(data); return runAsync2(); }) .then(function(data){ console.log(data); return "直接返回數(shù)據(jù)"; //這里直接返回數(shù)據(jù) }) .then(function(data){ console.log(data); });
那么輸出就變成了這樣:
reject的用法到這里,你應該對“Promise是什么玩意”有了最基本的了解。那么我們接著來看看ES6的Promise還有哪些功能。我們光用了resolve,還沒用reject呢,它是做什么的呢?事實上,我們前面的例子都是只有“執(zhí)行成功”的回調,還沒有“失敗”的情況,reject的作用就是把Promise的狀態(tài)置為rejected,這樣我們在then中就能捕捉到,然后執(zhí)行“失敗”情況的回調。看下面的代碼。
function getNumber(){ var p = new Promise(function(resolve, reject){ //做一些異步操作 setTimeout(function(){ var num = Math.ceil(Math.random()*10); //生成1-10的隨機數(shù) if(num<=5){ resolve(num); } else{ reject("數(shù)字太大了"); } }, 2000); }); return p; } getNumber() .then( function(data){ console.log("resolved"); console.log(data); }, function(reason, data){ console.log("rejected"); console.log(reason); } );
getNumber函數(shù)用來異步獲取一個數(shù)字,2秒后執(zhí)行完成,如果數(shù)字小于等于5,我們認為是“成功”了,調用resolve修改Promise的狀態(tài)。否則我們認為是“失敗”了,調用reject并傳遞一個參數(shù),作為失敗的原因。 運行getNumber并且在then中傳了兩個參數(shù),then方法 可以接受兩個參數(shù),第一個對應resolve的回調,第二個對應reject的回調。所以我們能夠分別拿到他們傳過來的數(shù)據(jù)。多次運行這段代碼,你會隨機得到下面兩種結果:
rejected 1
或
rejected 數(shù)字太大了catch的用法
我們知道Promise對象除了then方法,還有一個catch方法,它是做什么用的呢?其實它 和then的第二個參數(shù)一樣,用來指定reject的回調,用法是這樣:
getNumber() .then(function(data){ console.log("resolved"); console.log(data); }) .catch(function(reason){ console.log("rejected"); console.log(reason); });
效果和寫在then的第二個參數(shù)里面一樣。不過它還有另外一個作用:在執(zhí)行resolve的回調(也就是上面then中的第一個參數(shù))時,如果拋出異常了(代碼出錯了),那么并不會報錯卡死js,而是會進到這個catch方法中。請看下面的代碼:
getNumber()
.then(function(data){
console.log("resolved"); console.log(data); console.log(somedata); //此處的somedata未定義
})
.catch(function(reason){
console.log("rejected"); console.log(reason);
});
在resolve的回調中,我們console.log(somedata);而somedata這個變量是沒有被定義的。如果我們不用Promise,代碼運行到這里就直接 在控制臺報錯了,不往下運行了。但是在這里,會得到這樣的結果:
也就是說進到catch方法里面去了,而且把錯誤原因傳到了reason參數(shù)中。即便是有錯誤的代碼也不會報錯了,這與我們的try/catch語句有相同的功能。
all的用法Promise的all方法提供了并行執(zhí)行異步操作的能力,并且在所有異步操作執(zhí)行完后才執(zhí)行回調。我們仍舊使用上面定義好的runAsync1、runAsync2、runAsync3這三個函數(shù),看下面的例子:
Promise .all([runAsync1(), runAsync2(), runAsync3()]) .then(function(results){ console.log(results); });
用Promise.all來執(zhí)行,all接收一個數(shù)組參數(shù),里面的值最終都算返回Promise對象。這樣,三個異步操作的并行執(zhí)行的,等到它們都執(zhí)行完后才會進到then里面。那么,三個異步操作返回的數(shù)據(jù)哪里去了呢?都在then里面呢,all會把所有異步操作的結果放進一個數(shù)組中傳給then,就是上面的results。所以上面代碼的輸出結果就是:
有了all,你就可以并行執(zhí)行多個異步操作,并且在一個回調中處理所有的返回數(shù)據(jù),是不是很酷?有一個場景是很適合用這個的,一些游戲類的素材比較多的應用,打開網頁時,預先加載需要用到的各種資源如圖片、flash以及各種靜態(tài)文件。所有的都加載完后,我們再進行頁面的初始化。
race的用法all方法的效果實際上是「誰跑的慢,以誰為準執(zhí)行回調」,那么相對的就有另一個方法「誰跑的快,以誰為準執(zhí)行回調」,這就是race方法,這個詞本來就是賽跑的意思。race的用法與all一樣,我們把上面runAsync1的延時改為1秒來看一下:
Promise .race([runAsync1(), runAsync2(), runAsync3()]) .then(function(results){ console.log(results); });
這三個異步操作同樣是并行執(zhí)行的。結果你應該可以猜到,1秒后runAsync1已經執(zhí)行完了,此時then里面的就執(zhí)行了。結果是這樣的:
你猜對了嗎?不完全,是吧。在then里面的回調開始執(zhí)行時,runAsync2()和runAsync3()并沒有停止,仍舊再執(zhí)行。于是再過1秒后,輸出了他們結束的標志。 這個race有什么用呢?使用場景還是很多的,比如我們可以用race給某個異步請求設置超時時間,并且在超時后執(zhí)行相應的操作,代碼如下:
//請求某個圖片資源 function requestImg(){ var p = new Promise(function(resolve, reject){ var img = new Image(); img.onload = function(){ resolve(img); } img.src = "xxxxxx"; }); return p; } //延時函數(shù),用于給請求計時 function timeout(){ var p = new Promise(function(resolve, reject){ setTimeout(function(){ reject("圖片請求超時"); }, 5000); }); return p; } Promise .race([requestImg(), timeout()]) .then(function(results){ console.log(results); }) .catch(function(reason){ console.log(reason); });
requestImg函數(shù)會異步請求一張圖片,我把地址寫為"xxxxxx",所以肯定是無法成功請求到的。timeout函數(shù)是一個延時5秒的異步操作。我們把這兩個返回Promise對象的函數(shù)放進race,于是他倆就會賽跑,如果5秒之內圖片請求成功了,那么遍進入then方法,執(zhí)行正常的流程。如果5秒鐘圖片還未成功返回,那么timeout就跑贏了,則進入catch,報出“圖片請求超時”的信息。運行結果如下:
promise 和 ajax 結合例子:function ajax(url) { return new Promise(function(resolve, reject) { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 ) { resovle(xhr.responseText); } else { reject( xhr.status); } } xhr.open("GET", url, false); xhr.send(); }); } ajax("/test.json") .then(function(data){ console.log(data); }) .cacth(function(err){ console.log(err); });jquery中的Promise
jquery中的Promise,也就是我們所知道的Deferred對象
事實上,在此之前網上有很多文章在講jquery Deferred對象了,但是總喜歡把ajax和Deferred混在一起講,容易把人搞混。when、done、promise、success、error、fail、then、resolve、reject、always這么多方法不能揉在一起講,需要把他們捋一捋,哪些是Deferred對象的方法,哪些是ajax的語法糖,我們需要心知肚明。
先講$.Deferredjquery用$.Deferred實現(xiàn)了Promise規(guī)范,$.Deferred是個什么玩意呢?還是老方法,打印出來看看,先有個直觀印象:
var def = $.Deferred(); console.log(def);
輸出如下:
520134-20160329213911379-199799420.png
$.Deferred()返回一個對象,我們可以稱之為Deferred對象,上面掛著一些熟悉的方法如:done、fail、then等。jquery就是用這個Deferred對象來注冊異步操作的回調函數(shù),修改并傳遞異步操作的狀態(tài)。
Deferred對象的基本用法如下,為了不與ajax混淆,我們依舊舉setTimeout的例子:
function runAsync(){ var def = $.Deferred(); //做一些異步操作 setTimeout(function(){ console.log("執(zhí)行完成"); def.resolve("隨便什么數(shù)據(jù)"); }, 2000); return def; } runAsync().then(function(data){ console.log(data) });
在runAsync函數(shù)中,我們首先定義了一個def對象,然后進行一個延時操作,在2秒后調用def.resolve(),最后把def作為函數(shù)的返回。調用runAsync的時候將返回def對象,然后我們就可以.then來執(zhí)行回調函數(shù)。
是不是感覺和ES6的Promise很像呢?我們來回憶一下第一篇中ES6的例子:
function runAsync(){ var p = new Promise(function(resolve, reject){ //做一些異步操作 setTimeout(function(){ console.log("執(zhí)行完成"); resolve("隨便什么數(shù)據(jù)"); }, 2000); }); return p; } runAsync()
區(qū)別在何處一看便知。由于jquery的def對象本身就有resolve方法,所以我們在創(chuàng)建def對象的時候并未像ES6這樣傳入了一個函數(shù)參數(shù),是空的。在后面可以直接def.resolve()這樣調用。
這樣也有一個弊端,因為執(zhí)行runAsync()可以拿到def對象,而def對象上又有resolve方法,那么豈不是可以在外部就修改def的狀態(tài)了?比如我把上面的代碼修改如下:
var d = runAsync(); d.then(function(data){ console.log(data) }); d.resolve("在外部結束");
現(xiàn)象會如何呢?并不會在2秒后輸出“執(zhí)行完成”,而是直接輸出“在外部結束”。因為我們在異步操作執(zhí)行完成之前,沒等他自己resolve,就在外部給resolve了。這顯然是有風險的,比如你定義的一個異步操作并指定好回調函數(shù),有可能被別人給提前結束掉,你的回調函數(shù)也就不能執(zhí)行了。
怎么辦?jquery提供了一個promise方法,就在def對象上,他可以返回一個受限的Deferred對象,所謂受限就是沒有resolve、reject等方法,無法從外部來改變他的狀態(tài),用法如下:
function runAsync(){ var def = $.Deferred(); //做一些異步操作 setTimeout(function(){ console.log("執(zhí)行完成"); def.resolve("隨便什么數(shù)據(jù)"); }, 2000); return def.promise(); //就在這里調用 }
這樣返回的對象上就沒有resolve方法了,也就無法從外部改變他的狀態(tài)了。這個promise名字起的有點奇葩,容易讓我們搞混,其實他就是一個返回受限Deferred對象的方法,與Promise規(guī)范沒有任何關系,僅僅是名字叫做promise罷了。雖然名字奇葩,但是推薦使用。
then的鏈式調用既然Deferred也是Promise規(guī)范的實現(xiàn)者,那么其他特性也必須是支持的。鏈式調用的用法如下:
var d = runAsync(); d.then(function(data){ console.log(data); return runAsync2(); }) .then(function(data){ console.log(data); return runAsync3(); }) .then(function(data){ console.log(data); });done與fail
我們知道,Promise規(guī)范中,then方法接受兩個參數(shù),分別是執(zhí)行完成和執(zhí)行失敗的回調,而jquery中進行了增強,還可以接受第三個參數(shù),就是在pending狀態(tài)時的回調,如下:
deferred.then( doneFilter [, failFilter ] [, progressFilter ] )
除此之外,jquery還增加了兩個語法糖方法,done和fail,分別用來指定執(zhí)行完成和執(zhí)行失敗的回調,也就是說這段代碼:
d.then(function(){ console.log("執(zhí)行完成"); }, function(){ console.log("執(zhí)行失敗"); });
與這段代碼是等價的:
d.done(function(){ console.log("執(zhí)行完成"); }) .fail(function(){ console.log("執(zhí)行失敗"); });always的用法
jquery的Deferred對象上還有一個always方法,不論執(zhí)行完成還是執(zhí)行失敗,always都會執(zhí)行,有點類似ajax中的complete。不贅述了。
$.when的用法jquery中,還有一個$.when方法來實現(xiàn)Promise,與ES6中的all方法功能一樣,并行執(zhí)行異步操作,在所有的異步操作執(zhí)行完后才執(zhí)行回調函數(shù)。不過$.when并沒有定義在$.Deferred中,看名字就知道,$.when,它是一個多帶帶的方法。與ES6的all的參數(shù)稍有區(qū)別,它接受的并不是數(shù)組,而是多個Deferred對象,如下:
$.when(runAsync(), runAsync2(), runAsync3()) .then(function(data1, data2, data3){ console.log("全部執(zhí)行完成"); console.log(data1, data2, data3); });
jquery中沒有像ES6中的race方法嗎?就是以跑的快的為準的那個方法。對的,jquery中沒有。
以上就是jquery中Deferred對象的常用方法了,還有一些其他的方法用的也不多,干脆就不記它了。接下來該說說ajax了。
ajax與Deferred的關系jquery的ajax返回一個受限的Deferred對象,還記得受限的Deferred對象吧,也就是沒有resolve方法和reject方法,不能從外部改變狀態(tài)。想想也是,你發(fā)一個ajax請求,別人從其他地方給你取消掉了,也是受不了的。
既然是Deferred對象,那么我們上面講到的所有特性,ajax也都是可以用的。比如鏈式調用,連續(xù)發(fā)送多個請求:
req1 = function(){ return $.ajax(/*...*/); } req2 = function(){ return $.ajax(/*...*/); } req3 = function(){ return $.ajax(/*...*/); } req1().then(req2).then(req3).done(function(){ console.log("請求發(fā)送完畢"); });
明白了ajax返回對象的實質,那我們用起來就得心應手了。
success、error與complete這三個方法或許是我們用的最多的,使用起來是這樣的:
$.ajax(/*...*/) .success(function(){/*...*/}) .error(function(){/*...*/}) .complete(function(){/*...*/})
分別表示ajax請求成功、失敗、結束的回調。這三個方法與Deferred又是什么關系呢?其實就是語法糖,success對應done,error對應fail,complete對應always,就這樣,只是為了與ajax的參數(shù)名字上保持一致而已,更方便大家記憶,看一眼源碼:
deferred.promise( jqXHR ).complete = completeDeferred.add; jqXHR.success = jqXHR.done; jqXHR.error = jqXHR.fail;
complete那一行那么寫,是為了減少重復代碼,其實就是把done和fail又調用一次,與always中的代碼一樣。deferred.promise( jqXHR )這句也能看出,ajax返回的是受限的Deferred對象。
jquery加了這么些個語法糖,雖然上手門檻更低了,但是卻造成了一定程度的混淆。一些人雖然這么寫了很久,卻一直不知道其中的原理,在面試的時候只能答出一些皮毛,這是很不好的。這也是我寫這篇文章的緣由。
jquery中Deferred對象涉及到的方法很多,本文盡量分門別類的來介紹,希望能幫大家理清思路??偨Y一下就是:$.Deferred實現(xiàn)了Promise規(guī)范,then、done、fail、always是Deferred對象的方法。
$.when是一個全局的方法,用來并行運行多個異步任務,與ES6的all是一個功能。
$.ajax返回一個Deferred對象,success、error、complete是ajax提供的語法糖,功能與Deferred對象的done、fail、always一致。
語法:
async function name([param[, param[, ... param]]]) { statements }
調用 async 函數(shù)時會返回一個 promise 對象。當這個 async 函數(shù)返回一個值時,Promise 的 resolve 方法將會處理這個值;當 async 函數(shù)拋出異常時,Promise 的 reject 方法將處理這個異常值。
async 函數(shù)中可能會有await 表達式,這將會使 async 函數(shù)暫停執(zhí)行,等待 Promise 正常解決后繼續(xù)執(zhí)行 async 函數(shù)并返回解決結果(異步)。
function resolveAfter2Seconds(x) { return new Promise(resolve => { setTimeout(() => { resolve(x); }, 2000); }); } async function add1(x) { var a = resolveAfter2Seconds(20); var b = resolveAfter2Seconds(30); return x + await a + await b; } add1(10).then(v => { console.log(v); // 2s 后打印 60, 兩個 await 是同時發(fā)生的,也就是并行的。 }); async function add2(x) { var a = await resolveAfter2Seconds(20); var b = await resolveAfter2Seconds(30); return x + a + b; } add2(10).then(v => { console.log(v); // 4s 后打印 60,按順序完成的。 });
上面代碼是 mdn 的一個例子。
說明了 async 返回的是一個 promise對象;
await 后面跟的表達式是一個 promise,執(zhí)行到 await時,函數(shù)暫停執(zhí)行,直到該 promise 返回結果,并且暫停但不會阻塞主線程。
await 的任何內容都通過 Promise.resolve() 傳遞。
await 可以是并行(同時發(fā)生)和按順序執(zhí)行的。
看下面兩段代碼:
async function series() { await wait(500); await wait(500); return "done!"; } async function parallel() { const wait1 = wait(500); const wait2 = wait(500); await wait1; await wait2; return "done!"; }
第一段代碼執(zhí)行完畢需要 1000毫秒,這段 await 代碼是按順序執(zhí)行的;第二段代碼執(zhí)行完畢只需要 500 毫秒,這段 await 代碼是并行的。
回調函數(shù)這是異步編程最基本的方法。
假定有兩個函數(shù)f1和f2,后者等待前者的執(zhí)行結果。
f1(); f2();
如果f1是一個很耗時的任務,可以考慮改寫f1,把f2寫成f1的回調函數(shù)。
function f1(callback){ setTimeout(function () { // f1的任務代碼 callback(); }, 1000); }
執(zhí)行代碼就變成下面這樣:
f1(f2);
采用這種方式,我們把同步操作變成了異步操作,f1不會堵塞程序運行,相當于先執(zhí)行程序的主要邏輯,將耗時的操作推遲執(zhí)行。
回調函數(shù)的優(yōu)點是簡單、容易理解和部署,缺點是不利于代碼的閱讀和維護,各個部分之間高度耦合(Coupling),流程會很混亂,而且每個任務只能指定一個回調函數(shù)。
事件監(jiān)聽另一種思路是采用事件驅動模式。任務的執(zhí)行不取決于代碼的順序,而取決于某個事件是否發(fā)生。
還是以f1和f2為例。首先,為f1綁定一個事件(這里采用的jQuery的寫法)。
f1.on("done", f2);
上面這行代碼的意思是,當f1發(fā)生done事件,就執(zhí)行f2。然后,對f1進行改寫:
function f1(){ setTimeout(function () { // f1的任務代碼 f1.trigger("done"); }, 1000); }
f1.trigger("done")表示,執(zhí)行完成后,立即觸發(fā)done事件,從而開始執(zhí)行f2。
這種方法的優(yōu)點是比較容易理解,可以綁定多個事件,每個事件可以指定多個回調函數(shù),而且可以"去耦合"(Decoupling),有利于實現(xiàn)模塊化。缺點是整個程序都要變成事件驅動型,運行流程會變得很不清晰。
參考文章
1.Promise
2.JavaScript 運行機制詳解:再談Event Loop
3.知乎問答--知乎用戶
4.知乎問答--響馬
5.知乎問答--passenger
6.大白話講解Promise(一)
7.搞懂jquery中的Promise
8.JavaScript異步編程
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://www.ezyhdfw.cn/yun/90435.html
摘要:為啥做這個花了萬玩區(qū)塊鏈游戲,我覺得智能合約還是有點靠譜的要從這篇文章說起,之前幫拿數(shù)據(jù)的時候,就在想。 為啥做這個? 花了600萬玩區(qū)塊鏈游戲,我覺得智能合約還是有點靠譜的 要從這篇文章說起,之前幫nick拿數(shù)據(jù)的時候,就在想。既然 eth的所有數(shù)據(jù)都是公開的,那我是不是可以把所有的鏈上游戲的交易數(shù)據(jù)都拿下來,然后看看, 針對所有游戲來說 有多少人玩了區(qū)塊鏈游戲? 有多少人賺了錢?...
摘要:模塊化是隨著前端技術的發(fā)展,前端代碼爆炸式增長后,工程化所采取的必然措施。目前模塊化的思想分為和。特別指出,事件不等同于異步,回調也不等同于異步。將會討論安全的類型檢測惰性載入函數(shù)凍結對象定時器等話題。 Vue.js 前后端同構方案之準備篇——代碼優(yōu)化 目前 Vue.js 的火爆不亞于當初的 React,本人對寫代碼有潔癖,代碼也是藝術。此篇是準備篇,工欲善其事,必先利其器。我們先在代...
摘要:我們將登錄按鈕上綁上事件,點擊登錄之后向服務端提交賬號和密碼進行驗證。所以前端和后端權限的劃分是不太一致。側邊欄最后一個涉及到權限的地方就是側邊欄,不過在前 完整項目地址:vue-element-admin 系列文章: 手摸手,帶你用vue擼后臺 系列一(基礎篇) 手摸手,帶你用vue擼后臺 系列二(登錄權限篇) 手摸手,帶你用vue擼后臺 系列三 (實戰(zhàn)篇) 手摸手,帶你用vu...
摘要:從最開始的到封裝后的都在試圖解決異步編程過程中的問題。為了讓編程更美好,我們就需要引入來降低異步編程的復雜性。異步編程入門的全稱是前端經典面試題從輸入到頁面加載發(fā)生了什么這是一篇開發(fā)的科普類文章,涉及到優(yōu)化等多個方面。 TypeScript 入門教程 從 JavaScript 程序員的角度總結思考,循序漸進的理解 TypeScript。 網絡基礎知識之 HTTP 協(xié)議 詳細介紹 HTT...
本文涵蓋 面試題的引入 對事件循環(huán)面試題執(zhí)行順序的一些疑問 通過面試題對微任務、事件循環(huán)、定時器等對深入理解 結論總結 面試題 面試題如下,大家可以先試著寫一下輸出結果,然后再看我下面的詳細講解,看看會不會有什么出入,如果把整個順序弄清楚 Node.js 的執(zhí)行順序應該就沒問題了。 async function async1(){ console.log(async1 start) ...
閱讀 2633·2021-09-06 15:02
閱讀 3307·2021-09-02 10:18
閱讀 2907·2019-08-30 15:44
閱讀 759·2019-08-30 15:43
閱讀 2020·2019-08-30 14:08
閱讀 2820·2019-08-30 13:16
閱讀 1483·2019-08-26 13:52
閱讀 985·2019-08-26 12:21