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, reject),可以直接指定失敗的回撥(支援接收上一個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.reject()
可以生成一個失敗的Promise
Promise.reject(`出錯了`)等同於new Promise((resolve, reject) => reject(`出錯了`))
上述用法不夠詳細,下面的程式碼會更容易理解
2. Why Promise?
以jquery的ajax為例(@1.5.0版本以前,後來jquery也引入了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: 錯誤
複製程式碼
我們實現一下,思路很簡單,在執行器執行時進行try 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已經誕生很久了,如果你還不瞭解它,那你已經很落後了,抓緊時間上車。程式世界一日千里,作為程式設計師,要主動擁抱變化。
歡迎加我的個人微信深入交流