摘要:,異步編程的流行解決方案,相比于古老的回調函數等方式,它更科學,更優雅。它來自民間,后被官方招安。實現方法,方法和狀態機制根據使用方法我們可以知道,是一個需要接受一個執行器的構造函數,執行器提供兩個方法,內部有狀態機制,原型鏈上有方法。
Promise,js異步編程的流行解決方案,相比于古老的回調函數等方式,它更科學,更優雅。它來自民間,后被官方招安。 本文將從介紹用法開始,一步步了解Promise,探究源碼,最終根據官方規范手寫一個Promise。 讓我們先擁抱ta,再扒光ta!
我想在你身上,做春天在櫻桃樹身上做的事情。——巴勃羅·聶魯達
1. How Promise?
創建Promise
首先來看看promise的用法,從名字可以看出它是個構造函數,所以我們得new它,得到一個Promise實例p,我們打印p看看
let p = new Promise console.log(p) // TypeError: Promise resolver undefined is not a function
參數
報錯信息告訴我們,Promise需要一些參數,這里需要一個函數(我們叫它執行器)作為參數,該函數有兩個參數————resolve和reject,這兩個參數也是函數(由js引擎提供),我們可以在Promise內部調用,當異步操作成功時,調用resolve,否則reject。
let p =new Promise(function(resolve, reject){ if(/* 異步操作成功 */){ resolve(data) }else{ reject(err) } })
state
現在我們需要知道一個重要概念,Promise是有“狀態”的,分別是pending(等待態)、fulfilled(成功態)、rejected(失敗態),pending可以轉換為fulfilled或rejected,但fulfilled和rejected不可相互轉化。
resolve/reject 方法
resolve方法可以將pending轉為fulfilled,reject方法可以將pending轉為rejected。
then方法
通過給Promise示例上的then方法傳遞兩個函數作為參數,可以提供改變狀態時的回調,第一個函數是成功的回調,第二個則是失敗的回調。
p.then(function(data){ // resolve方法會將參數傳進成功的回調 console.log(data) }, function(err){ // reject方法會將失敗的信息傳進失敗的回調 console.log(err) })
舉個栗子
let p = new Promise(function(resolve, reject){ setTimeout(function(){ let num = Math.random() if (num > 0.5) { resolve(num) }else{ reject(num) } }, 1000) }) p.then(function(num){ console.log("大于0.5的數字:", num) },function(num){ console.log("小于等于0.5的數字", num) }) // 運行第一次:小于等于0.5的數字 0.166162996031475 // 運行第二次:大于0.5的數字: 0.6591451548308984 ...
在Promise執行器中我們進行了一次異步操作,并在我們覺得合適的時候調用成功或失敗的回調函數,并拿到了想要的數據以進行下一步操作
鏈式調用
除此之外,每一個then方法都會返回一個新的Promise實例(不是原來那個),讓then方法支持鏈式調用,并可以通過返回值將參數傳遞給下一個then
p.then(function(num){ return num },function(num){ return num }).then(function(num){ console.log("大于0.5的數字:", num) },function(num){ console.log("小于等于0.5的數字", num) })
catch方法
catch方法等同于.then(null, rejection),可以直接指定失敗的回調(支持接收上一個then發生的錯誤)
Promise.all()
這可能是個很有用的方法,它可以統一處理多個Promise
Promise.all能將多個Promise實例包裝成一個Promise實例
let Promise1 = new Promise(function(resolve, reject){}) let Promise2 = new Promise(function(resolve, reject){}) let Promise3 = new Promise(function(resolve, reject){}) let p = Promise.all([Promise1, Promise2, Promise3]) p.then(funciton(){ // 三個都成功則成功 }, function(){ // 只要有失敗,則失敗 })
這個組合后的Promise實例和普通實例一樣,有三種狀態,這里有組成它的幾個小Promise的狀態決定
:
1、當Promise1, Promise2, Promise3的狀態都為成功態,則p為成功態;
2、當Promise1, Promise2, Promise3中有任意一個為失敗態,則p為失敗態;
Promise.race()
與all方法類似,也可以講多個Promise實例包裝成一個新的Promise實例
不同的是,all時大Promise的狀態由多個小Promise共同決定,而race時由第一個轉變狀態的小Promise的狀態決定,第一個是成功態,則轉成功態,第一個失敗態,則轉失敗態
Promise.resolve()
可以生成一個成功的Promise
Promise.resolve("成功")等同于new Promise(function(resolve){resolve("成功")})
Promise.resolve()
可以生成一個失敗的Promise
Promise.reject("出錯了")等同于new Promise((resolve, reject) => reject("出錯了"))
上述用法不夠詳細,下面的代碼會更容易理解 2. Why Promise?以jquery的ajax為例(@1.5.0版本以前,后來jqery也引入了Promise的概念),看看從前我們是如何解決異步問題的。
$.get("url", {data: data}, function(result){ console.log("成功", result)// 成功的回調,result為異步拿到的數據 });
看起來還可以?
想象一個場景,當我們需要發送多個異步請求,而請求之間相互關聯相互依賴,沒有請求1就不會有請求2,沒有請求2就不會有請求3........
這時我們需要這樣寫
$.get("url", {data: data}, function(result1){ $.get("url", {data: result1}, function(result2){ $.get("url", {data: result2}, function(result3){ $.get("url", {data: result3}, function(result4){ ...... $.get("url", {data: resultn}, function(resultn+1){ console.log("成功") } } } } });
這樣的話,我們就掉入了傳說中的回調地獄,萬劫不復,不能自拔。
這種代碼,難以維護和調試,一旦出現bug,牽一發而動全身。
下面我們看看Promise是如何解決的,我們以node中的fs訪問文件舉例
先創建三個相互依賴的txt文件
1.txt的內容:
2.txt
2.txt的內容:
3.txt
3.txt的內容:
完成
js代碼:
let readFile = require("fs").readFile; // 加載node內置模塊fs 利用readFile方法異步訪問文件 function getFile(url){ // 創建一個讀取文件方法 return new Promise(function(resolve, reject){ // 返回一個Promise對象 readFile(url, "utf8", function(err,data){ // 讀取文件 resolve(data) // 調用成功的方法 }) }) } getFile("1.txt").then(function(data){ // then方法進行鏈式調用 console.log(data) // 2.txt return getFile(data) //拿到了第一次的內容用來請求第二次 }).then(function(data){ console.log(data) // 3.txt return getFile(data) //拿到了第二次的內容用來請求第三次 }).then(function(data){ console.log(data) // 完成 })
(這里我們先不必搞懂代碼,下面會介紹具體用法)
看起來多了幾行代碼[尷尬],但我們通過創建一個讀取函數返回一個Promise對象,再利用Promise自帶的.then方法,將嵌套的異步代碼弄得看起來像同步一樣,這樣的話,出現問題可以輕易的調試和修改。
3. What Promise?接下來是本文的重頭戲,根據PromiseA+(Promise的官方標準)動手實現一個180行左右代碼的promise,功能可實現多數(then catch all race resolve reject),這里會將的比較詳細,一步一步理清思路。
實現resolve、reject方法,then方法和狀態機制
根據使用方法我們可以知道,Promise是一個需要接受一個執行器的構造函數,執行器提供兩個方法,內部有狀態機制,原型鏈上有then方法。
開始擼:
// myPromise function Promise(executor){ //executor是一個執行器(函數) let _this = this // 先緩存this以免后面指針混亂 _this.status = "pending" // 默認狀態為等待態 _this.value = undefined // 成功時要傳遞給成功回調的數據,默認undefined _this.reason = undefined // 失敗時要傳遞給失敗回調的原因,默認undefined function resolve(value) { // 內置一個resolve方法,接收成功狀態數據 // 上面說了,只有pending可以轉為其他狀態,所以這里要判斷一下 if (_this.status === "pending") { _this.status = "resolved" // 當調用resolve時要將狀態改為成功態 _this.value = value // 保存成功時傳進來的數據 } } function reject(reason) { // 內置一個reject方法,失敗狀態時接收原因 if (_this.status === "pending") { // 和resolve同理 _this.status = "rejected" // 轉為失敗態 _this.reason = reason // 保存失敗原因 } } executor(resolve, reject) // 執行執行器函數,并將兩個方法傳入 } // then方法接收兩個參數,分別是成功和失敗的回調,這里我們命名為onFulfilled和onRjected Promise.prototype.then = function(onFulfilled, onRjected){ let _this = this; // 依然緩存this if(_this.status === "resolved"){ // 判斷當前Promise的狀態 onFulfilled(_this.value) // 如果是成功態,當然是要執行用戶傳遞的成功回調,并把數據傳進去 } if(_this.status === "rejected"){ // 同理 onRjected(_this.reason) } } module.exports = Promise // 導出模塊,否則別的文件沒法使用
注意:上面代碼的命名不是隨便起的,像onFulfilled和onRjected,是嚴格按照Promise/A+規范走的,不信你看圖
這樣我們就實現了第一步,可以創建Promise實例并使用then方法了,測試一下
let Promise = require("./myPromise") // 引入模塊 let p = new Promise(function(resolve, reject){ resolve("test") }) p.then(function(data){ console.log("成功", data) },function(err){ console.log("失敗", err) }) // 成功 test
再試試reject
let Promise = require("./myPromise") // 引入模塊 let p = new Promise(function(resolve, reject){ reject("test") }) p.then(function(data){ console.log("成功", data) },function(err){ console.log("失敗", err) }) // 失敗 test
看起來不錯,但回調函數是立即執行的,無法進行異步操作,比如這樣是不行的
let p = new Promise(function(resolve, reject){ setTimeout(function(){ resolve(100) }, 1000) }) p.then(function(data){ console.log("成功", data) },function(err){ console.log("失敗", err) }) // 不會輸出任何代碼
原因是我們在then函數中只對成功態和失敗態進行了判斷,而實例被new時,執行器中的代碼會立即執行,但setTimeout中的代碼將稍后執行,也就是說,then方法執行時,Promise的狀態沒有被改變依然是pending態,所以我們要對pending態也做判斷,而由于代碼可能是異步的,那么我們就要想辦法把回調函數進行緩存,并且,then方法是可以多次使用的,所以要能存多個回調,那么這里我們用一個數組。
實現異步
在實例上掛兩個參數
_this.onResolvedCallbacks = []; // 存放then成功的回調 _this.onRejectedCallbacks = []; // 存放then失敗的回調
then方法加一個pending時的判斷
if(_this.status === "pending"){ // 每一次then時,如果是等待態,就把回調函數push進數組中,什么時候改變狀態什么時候再執行 _this.onResolvedCallbacks.push(function(){ // 這里用一個函數包起來,是為了后面加入新的邏輯進去 onFulfilled(_this.value) }) _this.onRejectedCallbacks.push(function(){ // 同理 onRjected(_this.reason) }) }
下一步要分別在resolve和reject方法里加入執行數組中存放的函數的方法,修改一下上面的resolve和reject方法
function resolve(value) { if (_this.status === "pending") { _this.status = "resolved" _this.value = value _this.onResolvedCallbacks.forEach(function(fn){ // 當成功的函數被調用時,之前緩存的回調函數會被一一調用 fn() }) } } function reject(reason) { if (_this.status === "pending") { _this.status = "rejected" _this.reason = reason _this.onRejectedCallbacks.forEach(function(fn){// 當失敗的函數被調用時,之前緩存的回調函數會被一一調用 fn() }) } }
現在可以執行異步任務了,也可以多次then了,一個窮人版Promise就完成了,
處理錯誤
上面的代碼雖然能用,但經不起考驗,真正的Promise如果在實例中拋出錯誤,應該走reject:
new Promise(function(resolve, reject){ throw new Error("錯誤") }).then(function(){ },function(err){ console.log("錯誤:", err) }) // 錯誤: Error: 錯誤
我們實現一下,思路很簡單,在執行器執行時進行thy catch
try{ executor(resolve, reject) }catch(e){ // 如果捕獲發生異常,直接調失敗,并把參數穿進去 reject(e) }
實現then的鏈式調用(難點)
上面說過了,then可以鏈式調用,也是這一點讓Promise十分好用,當然這部分源碼也比較復雜
我們知道jquery實現鏈式調用是return了一個this,但Promise不行,為什么不行?
正宗的Promise是這樣的套路:
let p1 = new Promise(function(resolve, reject){ resolve() }) let p2 = p1.then(function(data){ //這是p1的成功回調,此時p1是成功態 throw new Error("錯誤") // 如果這里拋出錯誤,p2應是失敗態 }) p2.then(function(){ },function(err){ console.log(err) }) // Error: 錯誤
如果返回的是this,那么p2跟p1相同,固狀態也相同,但上面說了,Promise的成功態和失敗態不能相互轉換,那就不會得到p1成功而p2失敗的效果,而實際上是可能發生這種情況的。
所以Promise的then方法實現鏈式調用的原理是:返回一個新的Promise
在then方法中先定義一個新的Promise,取名為promise2(官方規定的),然后在三種狀態下分別用promise2包裝一下,在調用onFulfilled時用一個變量x(規定的)接收返回值,trycatch一下代碼,沒錯就調resolve傳入x,有錯就調reject傳入錯誤,最后再把promise2給return出去,就可以進行鏈式調用了,,,,但是!
// 改動then let promise2; if (_this.status === "resolved") { promise2 = new Promise(function (resolve, reject) { // 可以湊合用,但是是有很多問題的 try { let x = onFulfilled(_this.value) resolve(x) } catch (e) { reject(e) } }) } if (_this.status === "rejected") { promise2 = new Promise(function (resolve, reject) { // 可以湊合用,但是是有很多問題的 try { let x = onRjected(_this.reason) resolve(x) } catch (e) { reject(e) } }) } if(_this.status === "pending"){ promise2 = new Promise(function (resolve, rejec _this.onResolvedCallbacks.push(function(){ // 可以湊合用,但是是有很多問題的 try { let x = onFulfilled(_this.value) resolve(x) } catch (e) { reject(e) } }) _this.onRejectedCallbacks.push(function(){ // 可以湊合用,但是是有很多問題的 try { let x = onRjected(_this.reason) resolve(x) } catch (e) { reject(e) } }) }) } return promise2
這里我先解釋一下x的作用再說為什么不行,x是用來接收上一次then的返回值,比如這樣
let p = new Promise(function(resolve, reject){ resolve(data) }) p.then(function(data){ return xxx // 這里返回一個值 }, function(){ }).then(function(data){ console.log // 這里會接收到xxx }, function(){ }) // 以上代碼中第一次then的返回值就是源碼內第一次調用onRjected的返回值,可以用一個x來接收
接下來說問題,上面這樣看起來是符合邏輯的,并且也確實可以鏈式調用并接受到,但我們在寫庫,庫就要經得起考驗,把容錯性提到最高,要接受使用者各種新(cao)奇(dan)操作,所謂有容nai大。可能性如下:
1、前一次then返回一個普通值,字符串數組對象這些東西,都沒問題,只需傳給下一個then,剛才的方法就夠用。
2、前一次then返回的是一個Promise,是正常的操作,也是Promise提供的語法糖,我們要想辦法判斷到底返回的是啥。
3、前一次then返回的是一個Promise,其中有異步操作,也是理所當然的,那我們就要等待他的狀態改變,再進行下面的處理。
4、前一次then返回的是自己本身這個Promise
var p1 = p.then(function(){// 這里得用var,let由于作用域的原因會報錯undefined return p1 })
5、前一次then返回的是一個別人自己隨便寫的Promise,這個Promise可能是個有then的普通對象,比如{then:"哈哈哈"},也有可能在then里故意拋錯(這種蛋疼的操作我們也要考慮進去)。比如他這樣寫
let promise = {} Object.defineProperty(promise,"then",{ value: function(){ throw new Error("報錯氣死你") } }) // 如果返回這東西,我們再去調then方法就肯定會報錯了
6、調resolve的時候再傳一個Promise下去,我們還得處理這個Promise。
p.then(function(data) { return new Promise(function(resolve, reject) { resolve(new Promise(function(resolve,reject){ resolve(1111) })) }) })
7、可能既調resolve又調reject,得忽略后一個。
8、光then,里面啥也不寫。
。。
稍等,我先吐一會。。。
好了咱們調整心情繼續擼,其實這一系列的問題,很多都是相關的,只要根據規范,都可以順利解決,接上面的代碼,先干三件事
1、問題7是最好解決的,如果沒傳resolve和reject,我們就給他一個。
2、官方規范規定了一件事
簡單說就是為免在測試中出問題onFulfilled和onRejected要異步執行,我們就讓他異步執行
3、問題1-7,我們可以采取統一的覺得方案,定義一個函數來判斷和處理這一系列的情況,官方給出了一個叫做resolvePromise的函數
再看then方法
Promise.prototype.then = function (onFulfilled, onRjected) { //成功和失敗默認不傳給一個函數,解決了問題8 onFulfilled = typeof onFulfilled === "function" ? onFulfilled : function (value) { return value; } onRjected = typeof onRjected === "function" ? onRjected : function (err) { throw err; } let _this = this; let promise2; //返回的promise if (_this.status === "resolved") { promise2 = new Promise(function (resolve, reject) { // 當成功或者失敗執行時有異常那么返回的promise應該處于失敗狀態 setTimeout(function () {// 根據規范讓那倆家伙異步執行 try { let x = onFulfilled(_this.value);//這里解釋過了 // 寫一個方法統一處理問題1-7 resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }) }) } if (_this.status === "rejected") { promise2 = new Promise(function (resolve, reject) { setTimeout(function () { try { let x = onRjected(_this.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }) }) } if (_this.status === "pending") { promise2 = new Promise(function (resolve, reject) { _this.onResolvedCallbacks.push(function () { setTimeout(function () { try { let x = onFulfilled(_this.value); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e) } }) }); _this.onRejectedCallbacks.push(function () { setTimeout(function () { try { let x = onRjected(_this.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }) }); }) } return promise2; }
接下來看看resolvePromise該怎么寫
function resolvePromise(promise2, x, resolve, reject) { // 接受四個參數,新Promise、返回值,成功和失敗的回調 // 有可能這里返回的x是別人的promise // 盡可能允許其他亂寫 if (promise2 === x) { //這里應該報一個類型錯誤,來解決問題4 return reject(new TypeError("循環引用了")) } // 看x是不是一個promise,promise應該是一個對象 let called; // 表示是否調用過成功或者失敗,用來解決問題7 //下面判斷上一次then返回的是普通值還是函數,來解決問題1、2 if (x !== null && (typeof x === "object" || typeof x === "function")) { // 可能是promise {},看這個對象中是否有then方法,如果有then我就認為他是promise了 try { let then = x.then;// 保存一下x的then方法 if (typeof then === "function") { // 成功 //這里的y也是官方規范,如果還是promise,可以當下一次的x使用 //用call方法修改指針為x,否則this指向window then.call(x, function (y) { if (called) return //如果調用過就return掉 called = true // y可能還是一個promise,在去解析直到返回的是一個普通值 resolvePromise(promise2, y, resolve, reject)//遞歸調用,解決了問題6 }, function (err) { //失敗 if (called) return called = true reject(err); }) } else { resolve(x) } } catch (e) { if (called) return called = true; reject(e); } } else { // 說明是一個普通值1 resolve(x); // 表示成功了 } }
測試一下
PromiseA+提供了測試庫promises-aplus-tests,github上明確講解了使用方法
公開一個適配器接口:
Promise.deferred = function () { let dfd = {}; dfd.promise = new Promise(function (resolve, reject) { dfd.resolve = resolve; dfd.reject = reject; }); return dfd }
用命令行: promises-aplus-tests myPromise.js
經過一系列測試得到結果
872 passing (18s)
證明了我們的promise是完全符合規范的!
其他方法
除了最重要的then方法,Promise還有很多方法,但都不難,這里一次性介紹一遍
// 捕獲錯誤的方法,在原型上有catch方法,返回一個沒有resolve的then結果即可 Promise.prototype.catch = function (callback) { return this.then(null, callback) } // 解析全部方法,接收一個Promise數組promises,返回新的Promise,遍歷數組,都完成再resolve Promise.all = function (promises) { //promises是一個promise的數組 return new Promise(function (resolve, reject) { let arr = []; //arr是最終返回值的結果 let i = 0; // 表示成功了多少次 function processData(index, y) { arr[index] = y; if (++i === promises.length) { resolve(arr); } } for (let i = 0; i < promises.length; i++) { promises[i].then(function (y) { processData(i, y) }, reject) } }) } // 只要有一個promise成功了 就算成功。如果第一個失敗了就失敗了 Promise.race = function (promises) { return new Promise(function (resolve, reject) { for (var i = 0; i < promises.length; i++) { promises[i].then(resolve,reject) } }) } // 生成一個成功的promise Promise.resolve = function(value){ return new Promise(function(resolve,reject){ resolve(value); }) } // 生成一個失敗的promise Promise.reject = function(reason){ return new Promise(function(resolve,reject){ reject(reason); }) }
結語:Promise是異步的較好的解決方案之一,通過對源碼的解析,對Promise甚至js異步都有了深刻的理解。Promise已經誕生很久了,如果你還不了解它,那你已經很落后了,抓緊時間上車。程序世界一日千里,作為程序員,要主動擁抱變化。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/93734.html
摘要:定期召開會議,會議由會員公司的代表與特邀專家出席。新版本將會包含每年截止時間之前完成的所有特性。它引入了一個新的構造函數和具有輔助函數的命名空間對象。 導言:ECMAScript的演化不會停止,但是我們完全沒必要害怕。除了ES6這個史無前例的版本帶來了海量的信息和知識點以外,之后每年一發的版本都僅僅帶有少量的增量更新,一年更新的東西花半個小時就能搞懂了,完全沒必要畏懼。本文將帶您花大約...
摘要:就算改變已經發生了,即使再對對象添加回調函數,也會立即得到這個結果。方法接收個參數,第一個參數是狀態的回調函數,第二個參數可選是狀態的回調函數。簡單來講,就是能把原來的回調寫法分離出來,在異步操作執行完后,用鏈式調用的方式執行回調函數。 在ECMAScript 6標準中,Promise被正式列為規范,Promise,字面意思就是許諾,承諾,嘿,聽著是不是很浪漫的說?我們來探究一下這個浪...
摘要:做了什么一個用于爬取上妹子圖片的爬蟲。于是觀察瀏覽器正常瀏覽行為。在請求頭中設置和。解決該問題斷線繼續下載圖片下載個文件時,經常斷線。應該是網站的飯爬蟲機制起了作用,暫時無法解決。于是在保存圖片時會先判斷圖片是否存在。 做了什么 一個用于爬取www.nvshens.com上妹子圖片的爬蟲。如有侵權,馬上關閉showImg(https://segmentfault.com/img/bVR...
摘要:在這個教程中,我們將利用的和包來進行數據清洗。在很多情況下,使用唯一的值作為索引值識別數據字段是非常有幫助的。清洗數據字段到現在為止,我們移除了不必要的列并改變了我們的索引變得更有意義。 作者:xiaoyu微信公眾號:Python數據科學知乎:Python數據分析師 數據科學家花了大量的時間清洗數據集,并將這些數據轉換為他們可以處理的格式。事實上,很多數據科學家聲稱開始獲取和清洗數據...
閱讀 1096·2021-11-24 10:24
閱讀 2584·2021-11-22 13:54
閱讀 992·2021-09-24 09:55
閱讀 3592·2019-08-30 15:54
閱讀 1311·2019-08-30 15:44
閱讀 1089·2019-08-30 14:23
閱讀 3195·2019-08-29 13:45
閱讀 1267·2019-08-29 11:19