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