摘要:寫在前面這一章的順序對于未接觸過使用過的童鞋而言略抽象了,前邊幾章主要為了說明和之前的異步方式相比有什么優勢和它能解決什么問題,后邊才詳解的設計和各種場景下如何使用。建議先了解和簡單使用過后再閱讀,效果更佳。
寫在前面:
Promise這一章的順序對于未接觸過使用過Promise的童鞋而言略抽象了,前邊幾章主要為了說明Promise和之前的異步方式相比有什么優勢和它能解決什么問題,后邊才詳解Promise的API設計和各種場景下如何使用Promise。
建議先了解和簡單使用過Promise后再閱讀,效果更佳。
正文
3.1 什么是Promise之前的方式:
利用回調函數封裝程序中的continuation
回調交給第三方
第三方調用回調
實現正確功能
Promise方式:
第三方提供了解其任務何時結束的能力
Promise的異步特性是基于任務的(圖示如下)
一種處理異步的思路:為了統一現在和將來,把它們都變成將來,即所有操作都成了異步的
書中關于Promise是個啥的觀點:
一種封裝和組合未來值的易于復用的機制
一種在異步任務中作為兩個或更多步驟的流程控制機制,時序上的this-then-that —— 關注點分離
Promise設計的重要基礎
Promise一定是異步執行的,即使是立即完成的Promise(類似 new Promise((resolve)=>{ resolve(42) })),也無法被同步觀察到
一旦Promise決議,它就永遠保持在這個狀態,變成了不變值(immuatable value),這是設計中最基礎和最重要的因素
Promise至多只能有一個決議值(一個!一個!一個!)
引申:
Promise的決議結果可以給多方多次查看
安全、可靠
3.2 Promise的檢測基于thenable的鴨子類型
if( p !== null && ( typeof p === "object" || typeof p === "function" ) && typeof p.then === "function" ) { // 假定這是一個thenable } else { // 不是thenable }
這種方式顯然是有些問題的,但是目前通用的方式
3.3 Promise如何解決信任問題信任問題見 異步篇
3.3.1 調用過早避免Zalgo這類副作用:一個任務有時同步完成,有時異步完成,可能導致競態條件
Promise從定義上保證了不會存在這種問題:參考3.1 設計基礎 — 即使是立即完成的Promise,也無法被同步觀察到
3.3.2 調用過晚Note: 調用過晚強調的是調用順序?
Promise創建對象調用resolve(..)或reject(..)時,這個Promise的then注冊的觀察回調就會自動調度(注意是被調度而不是執行) —— 在下一個異步時機點上依次被調用執行,它們相互之間是不會互相影響或延誤的
3.3.3 回調未調用Promise一旦決議則一定會通知決議(傳入then的完成回調或拒絕回調調用),即使是Javascript運行錯誤也會調用拒絕回調
如果某個Promise一直不決議呢?使用競態的高級抽象機制:
// 超時工具 function timeoutPromise(delay){ return new Promise( (resolve, reject) => { setTimeout( function () { reject("Timeout!"); }, delay); } ) } // 設置某個Promise foo()超時 Promise.race( [ foo(), timeoutPromise(3000) ] ) .then( function () { // foo(..)及時完成 }, function (err) { // foo(..)被拒絕或者超時 // 通過查看err確定錯誤情況 } );3.3.4 調用次數過少或過多
如果創建Promise的代碼試圖多次調用resolve(..)或reject(..),或者兩者都調用,Promise只會接受第一次決議,后續調用都會被忽略
3.3.5 未能傳遞參數/環境值Promise至多只能有一個決議值
如果使用多個參數調用resolve(..)或reject(..),第一個參數之后的所有參數都會被忽略
Promise其實也是傳入回調函數,故函數中照樣能根據作用域規則訪問到對應的環境數據
3.3.6 吞掉錯誤或異常這里說的錯誤或異常可能出現在兩個過程:
Promise創建過程或其決議確認之前的任何時間點上(注:書中原文查看其決議結果過程中任何時間點,個人認為可能翻譯得有點問題,應該要強調是其決議之前)
Promise決議確認后在查看結果時(then(..)注冊的回調中)出現了js異常錯誤
這兩種錯誤都不會被丟棄,但針對它們的處理方式有所不同:
針對1:
該Promise會被立即拒絕,但注意這個異常也被變成了異步行為
let p = new Promise ( function(resolve, reject){ foo.bar(); // foo undefined 將拋出錯誤 Promise=>reject resolve( 42 ); // 不會執行到這里 }); p.then( function fulfilled(){ // 不會執行到這里 }, function rejected(err){ // err是一個TypeError異常 } )
針對2:
這個時候當前Promise已經決議,其決議結果是個不可變值
then(..)調用返回的下一個Promise被拒絕
let q = new Promise ( function(resolve, reject){ resolve( 42 ); }) q.then( function fulfilled(){ foo.bar(); // foo undefined 將拋出錯誤 導致then返回的Promise被reject }, function rejected(err){ // 不會執行到這里 } ).then( function fulfilled(){ // 不會執行到這里 }, function rejected(err){ // err是一個TypeError異常 } )3.3.7 構建可信任的Promise
Promise.resolve(..) 規范化傳入的值:
傳入一個非Promise、非thenable的立即值, 會得到一個用該值填充的Promise
傳入一個真正的Promise,會返回同一個Promise
傳入一個非Promise的thenable值,會試圖展開這個值,持續到提取出一個具體的非類Promise的最終值
具體看例子(傳入Promise的情況略)
// 傳入一個立即值 let p = Promise.resolve(42); p.then( res => { console.log("Promise.resolve(42).then:",res); }) let p1 = Promise.resolve({}); p1.then( res => { console.log("Promise.resolve({}).then:",res); }) // 傳入一個 thenable 嘗試展開 let p2 = Promise.resolve({ then: function(cb) { cb(42)} }); p2.then( res => { console.log("Promise.resolve(thenable).then:", res); }, err => { console.log("Promise.resolve(thenable).then:", err); }) // 注意 這種情況其實也是立即值!!! let p3 = Promise.resolve( setTimeout(()=>{ return "inside a continuation" },1000) ); // settimeout函數返回當前定時器引用=>耶 立即值 p3.then( res => { console.log("Promise.resolve(看起來是個異步).then:", res); })3.4 Promise鏈式流
Promise不僅僅是一個單步執行this-then-that的操作機制,這只是它的構成部件,實際上Promise是可以連接到一起使用表示一系列異步步驟:
每次對Promise調用then(..),它都會創建并返回一個新的Promise,我們可以將其鏈接起來;(并不局限于要求then中返回一個Promise)
不管從then(..)調用的完成回調(第一個參數)返回的值是什么,它都會被自動設置為被鏈接Promise(上一點中的)的完成(resolve)(一定要理解這句話)
再仔細看看第二點,結合上文 3.3.7 Promise.resolve(..)的能力,這是Promise鏈式流在每一步都能有異步能力的關鍵!
栗子:
// 返回立即值 let p = Promise.resolve(21); p .then( function(v) { console.log(v); // 21 // 返回立即值 return v * 2; }) // 這里是鏈接的Promise .then ( function(v) { console.log(v); // 42 });
// 返回Promise并引入異步 let p = Promise.resolve(21); p .then ( function(v) { // 返回一個異步Promise return new Promise( (resolve, reject) => { setTimeout(() => { resolve(v*2); }, 1000); }); }) .then ( function(v) { // 前一步延遲1s后執行 console.log(v); })
Promise鏈不僅僅是一個表達多步異步序列的流程控制,還可以從一個步驟到下一個步驟的消息通道
3.5 錯誤處理幾種錯誤處理方式:
try...catch結構不能應用于異步模式
function foo() { setTimeout(() => { baz.bar(); // 錯誤代碼 }, 100); } try{ foo(); // 之后將拋出全局錯誤 } catch (err) { // 不會走到這里 }
foo()中有自己的異步完成函數,其中任何異步錯誤都無法捕捉到
node.js api或庫中常見的err-first模式
function foo(cb) { setTimeout(() => { try { var x = baz.bar(); // 錯誤代碼 cb(null, x); } catch (err) { cb(err); } }, 100); } foo( function(err, val) { if(err) { console.error(err); // 報錯惹 } else { console.log(val); } })
分離回調模式(split-callback)
一個回調用于完成情況,一個回調用于拒絕情況
Promise采用的就是這種方式
先參考 3.3.6 再進行詳細討論:
Promise決議前、決議后產生的錯誤處理方式有所不同
錯誤的使用Promise API產生的錯誤會阻礙正常Promise對象的構造,這種情況下會立即拋出異常(這種情況應該死都不要出現 0 0)
由于Promise鏈式特點,其鏈上的最后一步,不管是什么,總是存在著在未被查看的Promise中出現未捕獲錯誤的可能性
即理論上來說:總有可能有錯誤未被捕獲,而出現全局報錯
P.S. 這也是個人認為使用Promise最頭疼的一點
3.5.2 處理未捕獲的情況關于如何解決3.5.1提出問題的一些思路
增加done(..)作為鏈式調用的終點,在其中可以查看未捕獲的錯誤,并且不會創建和返回新的Promise
依靠瀏覽器 追蹤Promise對象在被垃圾回收時是否有拒絕(未捕獲的錯誤),獲得其報告 (什么功能?@TODO),可是如果Promise未被垃圾回收呢?
3.5.2 成功的坑該小節討論的是從作者角度提出一種避免在使用Promise時在開發者未注意的情況下出現未捕獲錯誤而報出全局錯誤的方案
具體請看:
{ let p = Promise.reject(21); // 將觸發全局報錯 Uncaught (in promise) 21 let p1 = Promise.reject(21).then ( // 拒絕前,注冊了一個錯誤處理函數 (res) => { // 不會走到這里 }, (err) => { console.log(`注冊了一個錯誤處理函數:${err}`); } ) Promise.prototype.defer = function (){ // 作者提出的一個API // 簡單實現就是單純的返回這個Promise本身 return this; } let p2 = Promise.reject(21).defer(); // p2的結果在將來會被查看,現在暫時不要報全局錯誤 let foo = Promise.resolve(21); foo .then (function(v) { return p2; // 這里查看p2的結果 }, function (err) { // 不會走到這里 }) .catch (function(v) { console.log(v); // p2的結果 }) }3.6 Promise模式
基于Promise構建的異步抽象模式
3.6.1 Promise.all([ .. ])類似門(gate)這種機制:需要等待兩個或更多并行/并發的任務都完成才能繼續,它們的完成順序并不重要,但必須都要完成,門才能打開并讓流程控制繼續
Promise.all([ .. ])的參數接收一個數組:
數組中的每個值都會交給Promise.resolve(..) 過濾以保證傳入值是一個真正的Promise (Promise.resolve(..)的作用參考 3.3.7 構建可信任的Promise)
數組為空,主promise就會立即完成
返回一個Promise:
傳入的所有promise完成,該promise標記完成,返回消息是一個由所有傳入promise的完成消息組成的數組,與調用API時傳入的順序一致(與完成順序無關)
如果傳入的promise中有任何一個被拒絕的話,該promise會立即被拒絕,并丟棄來自其他所有promise的全部結果(其他promise還是會執行),返回錯誤消息是被拒絕的那個promise的錯誤消息(注意,promise一旦決議結果不會變更,故僅有第一個被拒絕的promise錯誤消息會被主promise返回)
每個promise都必須關聯一個拒絕/錯誤處理函數,特別是從Promise.all([ ... ])返回的那一個
3.6.2 Promise.race([ ... ])類似門閂(shuan),競態:一旦有任何一個Promise決議為完成,就標記為完成;一旦有任何一個Promise決議為拒絕,它就會拒絕
Promise.race([ ... ])的參數接收一個數組:
被Promise.resolve(...)過濾那是當然的
傳入立即值沒有任何意義,肯定是第一個立即值取勝
如果傳入一個空數組,會導致該Promise永遠不會決議!千萬不要這么做
返回一個Promise:
和Promise.all([ ... ])不同,返回消息不是一個數組,因為只能接收一個promise的完成消息
關于這兩個API需要注意
在all和race中存在著被忽略或丟棄的promise,如果這些promise中保存著重要的數據或資源或者開發者需要記錄這些promise失敗的事實,又該怎么辦呢?
finally API就是基于這種情況提出的:Promise需要一個finally(...)回調注冊,這個回調在Promise決議后總是會被調用,并允許執行任何必要的清理工作
注:書中提到finally還未被規范支持,而在18年1月已經正式加入到提案中了,可參考 https://github.com/tc39/propo... 和 https://github.com/tc39/propo...
書中還提到了一種觀察模式(基于同一個Promise決議可以被多次查看),具體可以看栗子
let foo = new Promise((resolve, reject) => { setTimeout(() => { resolve(21); }, 301); }); let timeout = function(time) { return new Promise((resolve, reject) => { setTimeout(() => { resolve("timeout"); }, time); }) } // foo會被默默忽略 Promise.race( [ foo, timeout(300) ]) .then( (res) => { console.log(`Promise.race: ${res}`); }) .finally( (res) => { console.log(`Promise.race: ${res}`); // finally回調是不會提供任何參數的,詳情可看 https://github.com/tc39/proposal-promise-finally }) // 觀察者模式 if(!Promise.observe){ Promise.observe = function(pr, cb){ // 觀察pr的決議 pr.then( function fulfilled (msg){ // 完成時 Promise.resolve(msg).then(cb); }, function reject (msg){ // 拒絕時 傳遞錯誤消息 但注意觀察者promise是resolve的 Promise.resolve(msg).then(cb); } ); // 返回最初的promise return pr; } } // 還是上一個超時的例子 Promise.race( [ Promise.observe( foo, function cleanup (msg){ console.log(`Promise.observe: ${msg}`); // foo即使沒有在超時之前完成 也可以獲取其決議情況 } ) .then ])3.6.3 all([ .. ])和race([ .. ])的變體
略
@TODO 自行實現 Promise.any finally map等擴展API
實現一個異步的map(..)工具
接收一個數組的值(可以是Promise或其他值)
接收一個在每個值上運行的一個函數
返回一個Promise,其完成值是一個數組,該數組保存任務執行之后的異步完成值(保持映射順序)
這里也主要看栗子
if(!Promise.map) { Promise.map = function(vals, cb) { // 等待所有map的promise決議的新的promise return Promise.all( // 對vals使用map將每個值轉出promise,值數組->Promise數組 vals.map( function(val){ // 將val值替換成調用cb函數后決議的新的promise return new Promise( function(resolve){ // resolve reject傳入到cb函數中 cb(val, resolve); }) }) ) } } // 使用Promise.map var p1 = Promise.resolve(21); var p2 = Promise.resolve(30); var p3 = Promise.reject("opps"); Promise.map( [p1,p2,p3], function(pr, resolve){ Promise.resolve(pr) .then( val => { resolve( val*2 ); }, resolve // 注意:不能發出拒絕信號,如果發出會導致Promise.map被拒絕,其他map結果也會被丟棄 ) }) .then( (vals) => { console.log(vals); })
TODO:
Promise API 概述詳解多帶帶成篇
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/94606.html
摘要:異步請求線程在在連接后是通過瀏覽器新開一個線程請求將檢測到狀態變更時,如果設置有回調函數,異步線程就產生狀態變更事件,將這個回調再放入事件循環隊列中。 基礎:瀏覽器 -- 多進程,每個tab頁獨立一個瀏覽器渲染進程(瀏覽器內核) 每個瀏覽器渲染進程是多線程的,主要包括:GUI渲染線程 JS引擎線程 也稱為JS內核,負責處理Javascript腳本程序。(例如V8引擎) JS引擎線程負...
摘要:這時候控制臺看到的是對象的快照,然而點開看詳情的話是這段代碼在運行的時候,瀏覽器可能會認為需要把控制臺延遲到后臺,這種情況下,等到瀏覽器控制臺輸出對象內容時,可能已經運行,因此會在點開的時候顯示,這是的異步化造成的。 本書屬于基礎類書籍,會有比較多的基礎知識,所以這里僅記錄平常不怎么容易注意到的知識點,不會全記,供大家和自己翻閱; 上中下三本的讀書筆記: 《你不知道的JavaScri...
摘要:注此讀書筆記只記錄本人原先不太理解的內容經過閱讀你不知道的后的理解。作用域及閉包基礎,代碼運行的幕后工作者引擎及編譯器。 注:此讀書筆記只記錄本人原先不太理解的內容經過閱讀《你不知道的JS》后的理解。 作用域及閉包基礎,JS代碼運行的幕后工作者:引擎及編譯器。引擎負責JS程序的編譯及執行,編譯器負責詞法分析和代碼生成。那么作用域就像一個容器,引擎及編譯器都從這里提取東西。 ...
摘要:閉包在循環中的應用延遲函數的回調會在循環結束時才執行事實上,當定時器運行時即使沒給迭代中執行的是多有的回調函數依然是在循環結束后才會被執行,因此會每次輸出一個出來。 閉包在循環中的應用 延遲函數的回調會在循環結束時才執行;事實上,當定時器運行時即使沒給迭代中執行的是 setTime(..., 0),多有的回調函數依然是在循環結束后才會被執行,因此會每次輸出一個6出來。 for(var...
摘要:從最開始的到封裝后的都在試圖解決異步編程過程中的問題。為了讓編程更美好,我們就需要引入來降低異步編程的復雜性。異步編程入門的全稱是前端經典面試題從輸入到頁面加載發生了什么這是一篇開發的科普類文章,涉及到優化等多個方面。 TypeScript 入門教程 從 JavaScript 程序員的角度總結思考,循序漸進的理解 TypeScript。 網絡基礎知識之 HTTP 協議 詳細介紹 HTT...
閱讀 2067·2021-10-12 10:12
閱讀 788·2021-09-24 09:47
閱讀 1187·2021-08-19 11:12
閱讀 3462·2019-08-29 13:06
閱讀 681·2019-08-26 11:43
閱讀 2563·2019-08-23 17:20
閱讀 1146·2019-08-23 16:52
閱讀 2594·2019-08-23 14:27