JS異步編程Promise對(duì)象詳解
1、單線程模型
單線程模型指的是,JavaScript 只在一個(gè)線程上運(yùn)行。也就是說(shuō),JavaScript 同時(shí)只能執(zhí)行一個(gè)任務(wù),其他任務(wù)都必須在后面排隊(duì)等待。注意,JavaScript 只在一個(gè)線程上運(yùn)行,不代表 JavaScript 引擎只有一個(gè)線程。事實(shí)上,JavaScript 引擎有多個(gè)線程,單個(gè)腳本只能在一個(gè)線程上運(yùn)行(稱為主線程),其他線程都是在后臺(tái)配合。
JavaScript 之所以采用單線程,而不是多線程,跟歷史有關(guān)系。JavaScript 從誕生起就是單線程,原因是不想讓瀏覽器變得太復(fù)雜,因?yàn)槎嗑€程需要共享資源、且有可能修改彼此的運(yùn)行結(jié)果,對(duì)于一種網(wǎng)頁(yè)腳本語(yǔ)言來(lái)說(shuō),這就太復(fù)雜了。
如果 JavaScript 同時(shí)有兩個(gè)線程,一個(gè)線程在網(wǎng)頁(yè) DOM 節(jié)點(diǎn)上添加內(nèi)容,另一個(gè)線程刪除了這個(gè)節(jié)點(diǎn),這時(shí)瀏覽器應(yīng)該以哪個(gè)線程為準(zhǔn)?是不是還要有鎖機(jī)制?
所以,為了避免復(fù)雜性,JavaScript 一開始就是單線程,這已經(jīng)成了這門語(yǔ)言的核心特征,將來(lái)也不會(huì)改變。
2、同步任務(wù)和異步任務(wù)
程序里面所有的任務(wù),可以分成兩類:同步任務(wù)(synchronous)和異步任務(wù)(asynchronous)。
同步任務(wù)是那些沒(méi)有被引擎掛起、在主線程上排隊(duì)執(zhí)行的任務(wù)。只有前一個(gè)任務(wù)執(zhí)行完畢,才能執(zhí)行后一個(gè)任務(wù)。
異步任務(wù)是那些被引擎放在一邊,不進(jìn)入主線程、而進(jìn)入任務(wù)隊(duì)列的任務(wù)。只有引擎認(rèn)為某個(gè)異步任務(wù)可以執(zhí)行了(比如 Ajax 操作從服務(wù)器得到了結(jié)果),該任務(wù)(采用回調(diào)函數(shù)的形式)才會(huì)進(jìn)入主線程執(zhí)行。排在異步任務(wù)后面的代碼,不用等待異步任務(wù)結(jié)束會(huì)馬上運(yùn)行,也就是說(shuō),異步任務(wù)不具有“堵塞”效應(yīng)。
舉例來(lái)說(shuō),Ajax 操作可以當(dāng)作同步任務(wù)處理,也可以當(dāng)作異步任務(wù)處理,由開發(fā)者決定。如果是同步任務(wù),主線程就等著 Ajax 操作返回結(jié)果,再往下執(zhí)行;如果是異步任務(wù),主線程在發(fā)出 Ajax 請(qǐng)求以后,就直接往下執(zhí)行,等到 Ajax 操作有了結(jié)果,主線程再執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù)。
3、任務(wù)隊(duì)列和事件循環(huán)
JavaScript 運(yùn)行時(shí),除了一個(gè)正在運(yùn)行的主線程,引擎還提供一個(gè)任務(wù)隊(duì)列(task queue),里面是各種需要當(dāng)前程序處理的異步任務(wù)。(實(shí)際上,根據(jù)異步任務(wù)的類型,存在多個(gè)任務(wù)隊(duì)列。為了方便理解,這里假設(shè)只存在一個(gè)隊(duì)列。)首先,主線程會(huì)去執(zhí)行所有的同步任務(wù)。等到同步任務(wù)全部執(zhí)行完,就會(huì)去看任務(wù)隊(duì)列里面的異步任務(wù)。
如果滿足條件,那么異步任務(wù)就重新進(jìn)入主線程開始執(zhí)行,這時(shí)它就變成同步任務(wù)了。等到執(zhí)行完,下一個(gè)異步任務(wù)再進(jìn)入主線程開始執(zhí)行。一旦任務(wù)隊(duì)列清空,程序就結(jié)束執(zhí)行。異步任務(wù)的寫法通常是回調(diào)函數(shù)。一旦異步任務(wù)重新進(jìn)入主線程,就會(huì)執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù)。
如果一個(gè)異步任務(wù)沒(méi)有回調(diào)函數(shù),就不會(huì)進(jìn)入任務(wù)隊(duì)列,也就是說(shuō),不會(huì)重新進(jìn)入主線程,因?yàn)闆](méi)有用回調(diào)函數(shù)指定下一步的操作。JavaScript 引擎怎么知道異步任務(wù)有沒(méi)有結(jié)果,能不能進(jìn)入主線程呢?答案就是引擎在不停地檢查,一遍又一遍,只要同步任務(wù)執(zhí)行完了,引擎就會(huì)去檢查那些掛起來(lái)的異步任務(wù),是不是可以進(jìn)入主線程了。這種循環(huán)檢查的機(jī)制,就叫做事件循環(huán)(Event Loop)。
維基百科的定義是:“事件循環(huán)是一個(gè)程序結(jié)構(gòu),用于等待和發(fā)送消息和事件(a programming construct that waits for and dispatches events or messages in a program)”。
4、異步操作的模式
4.1回調(diào)函數(shù)
把f2
寫成f1
的回調(diào)函數(shù)。
function f1(callback) { // ... callback(); } function f2() { // ... } f1(f2);
回調(diào)函數(shù)的優(yōu)點(diǎn)是簡(jiǎn)單、容易理解和實(shí)現(xiàn),缺點(diǎn)是不利于代碼的閱讀和維護(hù),各個(gè)部分之間高度耦合(coupling),使得程序結(jié)構(gòu)混亂、流程難以追蹤(尤其是多個(gè)回調(diào)函數(shù)嵌套的情況),而且每個(gè)任務(wù)只能指定一個(gè)回調(diào)函數(shù)。
4.2事件監(jiān)聽
f1.on('done', f2); function f1() { setTimeout(function () { // ... 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ì)變得很不清晰。閱讀代碼的時(shí)候,很難看出主流程。
4.3 發(fā)布/訂閱
事件完全可以理解成“信號(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ā)布/訂閱模式”(publish-subscribe pattern),又稱“觀察者模式”(observer pattern)。
f2
向信號(hào)中心jQuery
訂閱done
信號(hào)。
jQuery.subscribe('done', f2); function f1() { setTimeout(function () { // ... jQuery.publish('done'); }, 1000); }
上面代碼中,jQuery.publish('done')
的意思是,f1
執(zhí)行完成后,向信號(hào)中心jQuery
發(fā)布done
信號(hào),從而引發(fā)f2
的執(zhí)行。
f2
完成執(zhí)行后,可以取消訂閱(unsubscribe)。
jQuery.unsubscribe('done', f2);
這種方法的性質(zhì)與“事件監(jiān)聽”類似,但是明顯優(yōu)于后者。因?yàn)榭梢酝ㄟ^(guò)查看“消息中心”,了解存在多少信號(hào)、每個(gè)信號(hào)有多少訂閱者,從而監(jiān)控程序的運(yùn)行。
5、Promise 對(duì)象的狀態(tài)
Promise 對(duì)象通過(guò)自身的狀態(tài),來(lái)控制異步操作。Promise 實(shí)例具有三種狀態(tài)。
- 異步操作未完成(pending)
- 異步操作成功(fulfilled)
- 異步操作失敗(rejected)
上面三種狀態(tài)里面,fulfilled
和rejected
合在一起稱為resolved
(已定型)。
這三種的狀態(tài)的變化途徑只有兩種。
- 從“未完成”到“成功”
- 從“未完成”到“失敗”
一旦狀態(tài)發(fā)生變化,就凝固了,不會(huì)再有新的狀態(tài)變化。這也是 Promise 這個(gè)名字的由來(lái),它的英語(yǔ)意思是“承諾”,一旦承諾成效,就不得再改變了。這也意味著,Promise 實(shí)例的狀態(tài)變化只可能發(fā)生一次。
因此,Promise 的最終結(jié)果只有兩種。
- 異步操作成功,Promise 實(shí)例傳回一個(gè)值(value),狀態(tài)變?yōu)閒ulfilled。
- 異步操作失敗,Promise 實(shí)例拋出一個(gè)錯(cuò)誤(error),狀態(tài)變?yōu)閞ejected。
6、Promise 構(gòu)造函數(shù)
JavaScript 提供原生的Promise
構(gòu)造函數(shù),用來(lái)生成 Promise 實(shí)例。
var promise = new Promise(function (resolve, reject) { // ... if (/* 異步操作成功 */){ resolve(value); } else { /* 異步操作失敗 */ reject(new Error()); } });
上面代碼中,Promise
構(gòu)造函數(shù)接受一個(gè)函數(shù)作為參數(shù),該函數(shù)的兩個(gè)參數(shù)分別是resolve
和reject
。它們是兩個(gè)函數(shù),由 JavaScript 引擎提供,不用自己實(shí)現(xiàn)。
resolve
函數(shù)的作用是,將Promise
實(shí)例的狀態(tài)從“未完成”變?yōu)?ldquo;成功”(即從pending
變?yōu)?code>fulfilled),在異步操作成功時(shí)調(diào)用,并將異步操作的結(jié)果,作為參數(shù)傳遞出去。reject
函數(shù)的作用是,將Promise
實(shí)例的狀態(tài)從“未完成”變?yōu)?ldquo;失敗”(即從pending
變?yōu)?code>rejected),在異步操作失敗時(shí)調(diào)用,并將異步操作報(bào)出的錯(cuò)誤,作為參數(shù)傳遞出去。
下面是一個(gè)例子。
function timeout(ms) { return new Promise((resolve, reject) => { setTimeout(resolve, ms, 'done'); }); } timeout(100)
上面代碼中,timeout(100)
返回一個(gè) Promise 實(shí)例。100毫秒以后,該實(shí)例的狀態(tài)會(huì)變?yōu)?code>fulfilled。
7、then() 用法辨析
Promise 的用法,簡(jiǎn)單說(shuō)就是一句話:使用then
方法添加回調(diào)函數(shù)。但是,不同的寫法有一些細(xì)微的差別,請(qǐng)看下面四種寫法,它們的差別在哪里?
// 寫法一 f1().then(function () { return f2(); }); // 寫法二 f1().then(function () { f2(); }); // 寫法三 f1().then(f2()); // 寫法四 f1().then(f2);
為了便于講解,下面這四種寫法都再用then
方法接一個(gè)回調(diào)函數(shù)f3
。寫法一的f3
回調(diào)函數(shù)的參數(shù),是f2
函數(shù)的運(yùn)行結(jié)果。
f1().then(function () { return f2(); }).then(f3);
寫法二的f3
回調(diào)函數(shù)的參數(shù)是undefined
。
f1().then(function () { f2(); return; }).then(f3);
寫法三的f3
回調(diào)函數(shù)的參數(shù),是f2
函數(shù)返回的函數(shù)的運(yùn)行結(jié)果。
f1().then(f2()) .then(f3);
寫法四與寫法一只有一個(gè)差別,那就是f2
會(huì)接收到f1()
返回的結(jié)果。
f1().then(f2) .then(f3);
8、Promise 優(yōu)缺點(diǎn)
優(yōu)點(diǎn):讓回調(diào)函數(shù)變成了規(guī)范的鏈?zhǔn)綄懛?,程序流程可以看得很清楚。它有一整套接口,可以?shí)現(xiàn)許多強(qiáng)大的功能,比如同時(shí)執(zhí)行多個(gè)異步操作,等到它們的狀態(tài)都改變以后,再執(zhí)行一個(gè)回調(diào)函數(shù);再比如,為多個(gè)回調(diào)函數(shù)中拋出的錯(cuò)誤,統(tǒng)一指定處理方法等等。
而且,Promise 還有一個(gè)傳統(tǒng)寫法沒(méi)有的好處:它的狀態(tài)一旦改變,無(wú)論何時(shí)查詢,都能得到這個(gè)狀態(tài)。這意味著,無(wú)論何時(shí)為 Promise 實(shí)例添加回調(diào)函數(shù),該函數(shù)都能正確執(zhí)行。所以,你不用擔(dān)心是否錯(cuò)過(guò)了某個(gè)事件或信號(hào)。如果是傳統(tǒng)寫法,通過(guò)監(jiān)聽事件來(lái)執(zhí)行回調(diào)函數(shù),一旦錯(cuò)過(guò)了事件,再添加回調(diào)函數(shù)是不會(huì)執(zhí)行的。
缺點(diǎn):編寫的難度比傳統(tǒng)寫法高,而且閱讀代碼也不是一眼可以看懂。你只會(huì)看到一堆then
,必須自己在then
的回調(diào)函數(shù)里面理清邏輯。
9、微任務(wù)
Promise 的回調(diào)函數(shù)屬于異步任務(wù),會(huì)在同步任務(wù)之后執(zhí)行。
new Promise(function (resolve, reject) { resolve(1); }).then(console.log); console.log(2); // 2 // 1
上面代碼會(huì)先輸出2,再輸出1。因?yàn)?code>console.log(2)是同步任務(wù),而then
的回調(diào)函數(shù)屬于異步任務(wù),一定晚于同步任務(wù)執(zhí)行。
但是,Promise 的回調(diào)函數(shù)不是正常的異步任務(wù),而是微任務(wù)(microtask)。它們的區(qū)別在于,正常任務(wù)追加到下一輪事件循環(huán),微任務(wù)追加到本輪事件循環(huán)。這意味著,微任務(wù)的執(zhí)行時(shí)間一定早于正常任務(wù)。
setTimeout(function() { console.log(1); }, 0); new Promise(function (resolve, reject) { resolve(2); }).then(console.log); console.log(3); // 3 // 2 // 1
上面代碼的輸出結(jié)果是321
。這說(shuō)明then
的回調(diào)函數(shù)的執(zhí)行時(shí)間,早于setTimeout(fn, 0)
。因?yàn)?code>then是本輪事件循環(huán)執(zhí)行,setTimeout(fn, 0)
在下一輪事件循環(huán)開始時(shí)執(zhí)行。
到此這篇關(guān)于Promise異步編程模式的文章就介紹到這了。希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
比較不錯(cuò)的函數(shù)式JavaScript編程指南教程
你是否知道JavaScript其實(shí)也是一個(gè)函數(shù)式編程語(yǔ)言呢?本指南將教你如何利用JavaScript的函數(shù)式特性。2008-05-05JS實(shí)現(xiàn)兼容各種瀏覽器的高級(jí)拖動(dòng)方法完整實(shí)例【測(cè)試可用】
這篇文章主要介紹了JS實(shí)現(xiàn)兼容各種瀏覽器的高級(jí)拖動(dòng)方法,以完整實(shí)例形式分析了JS實(shí)現(xiàn)響應(yīng)鼠標(biāo)事件動(dòng)態(tài)修改頁(yè)面元素的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2016-06-06微信域名檢測(cè)接口調(diào)用演示步驟(含PHP、Python)
這篇文章主要介紹了微信域名檢測(cè)接口調(diào)用演示步驟(含PHP、Python),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12如何解決js函數(shù)防抖、節(jié)流出現(xiàn)的問(wèn)題
這篇文章主要介紹了如何解決js函數(shù)防抖、節(jié)流出現(xiàn)的問(wèn)題。SyntheticEvent對(duì)象是通過(guò)合并得到的。 這意味著在事件回調(diào)被調(diào)用后,SyntheticEvent 對(duì)象將被重用并且所有屬性都將被取消。 因此,您無(wú)法以異步方式訪問(wèn)該事件。,需要的朋友可以參考下2019-06-06js實(shí)現(xiàn)簡(jiǎn)單的購(gòu)物車有圖有代碼
這篇文章主要介紹了用js實(shí)現(xiàn)的簡(jiǎn)單購(gòu)物車,配有截圖,適合初學(xué)者2014-05-05js實(shí)現(xiàn)隨屏幕滾動(dòng)的帶緩沖效果的右下角廣告代碼
這篇文章主要介紹了js實(shí)現(xiàn)隨屏幕滾動(dòng)的帶緩沖效果的右下角廣告代碼,涉及javascript基于數(shù)學(xué)運(yùn)算及定時(shí)函數(shù)動(dòng)態(tài)操作頁(yè)面元素的實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-09-09Javascript中判斷一個(gè)值是否為undefined的方法詳解
這篇文章給大家詳細(xì)介紹了在Javascript中如何判斷一個(gè)值是否為undefined,對(duì)大家的日常工作和學(xué)習(xí)很有幫助,下面來(lái)一起看看吧。2016-09-09