面試的時候常常會問到Promise的使用;有的面試官再深刻一點,會繼續問是否瞭解Promise的實現方式,或者有沒有閱讀過Promise的源碼;今天咱們就來看一下,Promise在內部是如何實現來鏈式調用的。javascript
所謂Promise,簡單說就是一個容器,裏面保存着某個將來纔會結束的事件(一般是一個異步操做)的結果。從語法上說,Promise 是一個對象,從它能夠獲取異步操做的消息。Promise
提供統一的API
,各類異步操做均可以用一樣的方法進行處理。html
Promise出現以前都是經過回調函數來實現,回調函數自己沒有問題,可是嵌套層級過深,很容易掉進回調地獄
。前端
const fs = require('fs');
fs.readFile('1.txt', (err,data) => {
fs.readFile('2.txt', (err,data) => {
fs.readFile('3.txt', (err,data) => {
//可能還有後續代碼
});
});
});
複製代碼
若是每次讀取文件後還要進行邏輯的判斷或者異常的處理,那麼整個回調函數就會很是複雜且難以維護。Promise的出現正是爲了解決這個痛點,咱們能夠把上面的回調嵌套用Promise改寫一下:java
const readFile = function(fileName){
return new Promise((resolve, reject)=>{
fs.readFile(fileName, (err, data)=>{
if(err){
reject(err)
} else {
resolve(data)
}
})
})
}
readFile('1.txt')
.then(data => {
return readFile('2.txt');
}).then(data => {
return readFile('3.txt');
}).then(data => {
//...
});
複製代碼
promise最先是在commonjs社區提出來的,當時提出了不少規範。比較接受的是promise/A規範。可是promise/A規範比較簡單,後來人們在這個基礎上,提出了promise/A+規範,也就是實際上的業內推行的規範;es6也是採用的這種規範,可是es6在此規範上還加入了Promise.all、Promise.race、Promise.catch、Promise.resolve、Promise.reject等方法。es6
咱們能夠經過腳原本測試咱們寫的Promise是否符合promise/A+的規範。將咱們實現的Promise加入如下代碼:面試
Promise.defer = Promise.deferred = function () {
let dfd = {};
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
}
複製代碼
而後經過module.exports導出,安裝測試的腳本:npm
npm install -g promises-aplus-tests
複製代碼
在實現Promise的目錄執行如下命令:數組
promises-aplus-tests promise.js
複製代碼
接下來,腳本會對照着promise/A+的規範,對咱們的腳原本一條一條地進行測試。更多規範可查看規範promise
咱們先回顧一下,咱們平時都是怎麼使用Promise的:異步
var p = new Promise(function(resolve, reject){
console.log('執行')
setTimeout(function(){
resolve(2)
}, 1000)
})
p.then(function(res){
console.log('suc',res)
},function(err){
console.log('err',err)
})
複製代碼
首先看出來,Promise是經過構造函數實例化一個對象,而後經過實例對象上的then方法,來處理異步返回的結果。同時,promise/A+規範規定了:
promise 是一個擁有 then 方法的對象或函數,其行爲符合本規範;
一個 Promise 的當前狀態必須爲如下三種狀態中的一種:等待態(Pending)、執行態(Fulfilled)和拒絕態(Rejected)。
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
function Promise(executor) {
var _this = this
this.state = PENDING; //狀態
this.value = undefined; //成功結果
this.reason = undefined; //失敗緣由
function resolve(value) {}
function reject(reason) {}
}
Promise.prototype.then = function (onFulfilled, onRejected) {
};
module.exports = Promise;
複製代碼
當咱們實例化Promise時,構造函數會立刻調用傳入的執行函數executor,咱們能夠試一下:
let p = new Promise((resolve, reject) => {
console.log('執行了');
});
複製代碼
所以在Promise中構造函數立馬執行,同時將resolve函數和reject函數做爲參數傳入:
function Promise(executor) {
var _this = this
this.state = PENDING; //狀態
this.value = undefined; //成功結果
this.reason = undefined; //失敗緣由
function resolve(value) {}
function reject(reason) {}
executor(resolve, reject)
}
複製代碼
可是executor也會可能存在異常,所以經過try/catch來捕獲一下異常狀況:
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
複製代碼
promise/A+規範中規定,當Promise對象已經由等待態(Pending)改變爲執行態(Fulfilled)或者拒絕態(Rejected)後,就不能再次更改狀態,且終值也不可改變。
所以咱們在回調函數resolve和reject中判斷,只能是pending狀態的時候才能更改狀態:
function resolve(value) {
if(_this.state === PENDING){
_this.state = FULFILLED
_this.value = value
}
}
function reject(reason) {
if(_this.state === PENDING){
_this.state = REJECTED
_this.reason = reason
}
}
複製代碼
咱們更改狀態的同時,將回調函數中成功的結果或者失敗的緣由都保存在對應的屬性中,方便之後來獲取。
當Promise的狀態改變以後,無論成功仍是失敗,都會觸發then回調函數。所以,then的實現也很簡單,就是根據狀態的不一樣,來調用不一樣處理終值的函數。
Promise.prototype.then = function (onFulfilled, onRejected) {
if(this.state === FULFILLED){
typeof onFulfilled === 'function' && onFulfilled(this.value)
}
if(this.state === REJECTED){
typeof onRejected === 'function' && onRejected(this.reason)
}
};
複製代碼
在規範中也說了,onFulfilled和onRejected是可選的,所以咱們對兩個值進行一下類型的判斷:
onFulfilled 和 onRejected 都是可選參數。若是 onFulfilled 不是函數,其必須被忽略。若是 onRejected 不是函數,其必須被忽略
代碼寫到這裏,貌似該有的實現方式都有了,咱們來寫個demo測試一下:
var myP = new Promise(function(resolve, reject){
console.log('執行')
setTimeout(function(){
reject(3)
}, 1000)
});
myP.then(function(res){
console.log(res)
},function(err){
console.log(err)
});
複製代碼
然鵝,很遺憾,運行起來咱們發現只打印了構造函數中的執行
,下面的then函數根本都沒有執行。咱們整理一下代碼的運行流暢:
當then裏面函數運行時,resolve因爲是異步執行的,尚未來得及修改state,此時仍是PENDING狀態;所以咱們須要對異步的狀況作一下處理。
那麼如何讓咱們的Promise來支持異步呢?咱們能夠參考發佈訂閱模式,在執行then方法的時候,若是當前仍是PENDING狀態,就把回調函數寄存到一個數組中,當狀態發生改變時,去數組中取出回調函數;所以咱們先在Promise中定義一下變量:
function Promise(executor) {
this.onFulfilled = [];//成功的回調
this.onRejected = []; //失敗的回調
}
複製代碼
這樣,當then執行時,若是仍是PENDING狀態,咱們不是立刻去執行回調函數,而是將其存儲起來:
Promise.prototype.then = function (onFulfilled, onRejected) {
if(this.state === FULFILLED){
typeof onFulfilled === 'function' && onFulfilled(this.value)
}
if(this.state === REJECTED){
typeof onRejected === 'function' && onRejected(this.reason)
}
if(this.state === PENDING){
typeof onFulfilled === 'function' && this.onFulfilled.push(onFulfilled)
typeof onRejected === 'function' && this.onRejected.push(onRejected)
}
};
複製代碼
存儲起來後,當resolve或者reject異步執行的時候就能夠來調用了:
function resolve(value) {
if(_this.state === PENDING){
_this.state = FULFILLED
_this.value = value
_this.onFulfilled.forEach(fn => fn(value))
}
}
function reject(reason) {
if(_this.state === PENDING){
_this.state = REJECTED
_this.reason = reason
_this.onRejected.forEach(fn => fn(reason))
}
}
複製代碼
有童鞋可能會提出疑問了,爲何這邊onFulfilled和onRejected要存在數組中,直接用一個變量接收不是也能夠麼?下面看一個例子:
var p = new Promise((resolve, reject)=>{
setTimeout(()=>{
resolve(4)
}, 0)
})
p.then((res)=>{
//4 res
console.log(res, 'res')
})
p.then((res1)=>{
//4 res1
console.log(res1, 'res1')
})
複製代碼
咱們分別調用了兩次then,若是是一個變量的話,最後確定只會運行後一個then,把以前的覆蓋了,若是是數組的話,兩個then都能正常運行。
至此,咱們運行demo,就能如願以償的看到運行結果了;一個四十行左右的簡單Promise墊片就此完成了。這裏貼一下完整的代碼:
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
function Promise(executor) {
var _this = this
this.state = PENDING; //狀態
this.value = undefined; //成功結果
this.reason = undefined; //失敗緣由
this.onFulfilled = [];//成功的回調
this.onRejected = []; //失敗的回調
function resolve(value) {
if(_this.state === PENDING){
_this.state = FULFILLED
_this.value = value
_this.onFulfilled.forEach(fn => fn(value))
}
}
function reject(reason) {
if(_this.state === PENDING){
_this.state = REJECTED
_this.reason = reason
_this.onRejected.forEach(fn => fn(reason))
}
}
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
Promise.prototype.then = function (onFulfilled, onRejected) {
if(this.state === FULFILLED){
typeof onFulfilled === 'function' && onFulfilled(this.value)
}
if(this.state === REJECTED){
typeof onRejected === 'function' && onRejected(this.reason)
}
if(this.state === PENDING){
typeof onFulfilled === 'function' && this.onFulfilled.push(onFulfilled)
typeof onRejected === 'function' && this.onRejected.push(onRejected)
}
};
複製代碼
相信上面的Promise墊片應該很容易理解,下面鏈式調用纔是Promise的難點和核心點;咱們對照promise/A+規範,一步一步地來實現,咱們先來看一下規範是如何來定義的:
then 方法必須返回一個 promise 對象
promise2 = promise1.then(onFulfilled, onRejected);
也就是說,每一個then方法都要返回一個新的Promise對象,這樣咱們的then方法才能不斷的鏈式調用;所以上面的簡單墊片中then方法就不適用了,由於它什麼都沒有返回,咱們對其進行簡單的改寫,不論then進行什麼操做,都返回一個新的Promise對象:
Promise.prototype.then = function (onFulfilled, onRejected) {
let promise2 = new Promise((resolve, reject)=>{
})
return promise2
}
複製代碼
咱們繼續看then的執行過程:
[[Resolve]](promise2, x)
首先第一點,咱們知道onFulfilled和onRejected執行以後都會有一個返回值x,對返回值x處理就須要用到Promise解決過程,這個咱們下面再說;第二點須要對onFulfilled和onRejected進行異常處理,沒什麼好說的;第三和第四點,說的實際上是一個問題,若是onFulfilled和onRejected兩個參數沒有傳,則繼續往下傳(值的傳遞特性);舉個例子:
var p = new Promise(function(resolve, reject){
setTimeout(function(){
resolve(3)
}, 1000)
});
p.then(1,1)
.then('','')
.then()
.then(function(res){
//3
console.log(res)
})
複製代碼
這裏無論onFulfilled和onRejected傳什麼值,只要不是函數,就繼續向下傳入,直到有函數進行接收;所以咱們對then方法進行以下完善:
//_this是promise1的實例對象
var _this = this
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }
var promise2 = new Promise((resolve, reject)=>{
if(_this.state === FULFILLED){
let x = onFulfilled(_this.value)
resolvePromise(promise2, x, resolve, reject)
} else if(_this.state === REJECTED){
let x = onRejected(_this.reason)
resolvePromise(promise2, x ,resolve, reject)
} else if(_this.state === PENDING){
_this.onFulfilled.push(()=>{
let x = onFulfilled(_this.value)
resolvePromise(promise2, x, resolve, reject)
})
_this.onRejected.push(()=>{
let x = onRejected(_this.reason)
resolvePromise(promise2, x ,resolve, reject)
})
}
})
複製代碼
咱們發現函數中有一個resolvePromise,就是上面說的Promise解決過程,它是對新的promise2和上一個執行結果 x 的處理,因爲具備複用性,咱們把它抽成一個單獨的函數,這也是上面規範中定義的第一點。
因爲then的回調是異步執行的,所以咱們須要把onFulfilled和onRejected執行放到異步中去執行,同時作一下錯誤的處理:
//其餘代碼略
if(_this.state === FULFILLED){
setTimeout(()=>{
try {
let x = onFulfilled(_this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
})
} else if(_this.state === REJECTED){
setTimeout(()=>{
try {
let x = onRejected(_this.reason)
resolvePromise(promise2, x ,resolve, reject)
} catch (error) {
reject(error)
}
})
} else if(_this.state === PENDING){
_this.onFulfilled.push(()=>{
setTimeout(()=>{
try {
let x = onFulfilled(_this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
})
})
_this.onRejected.push(()=>{
setTimeout(()=>{
try {
let x = onRejected(_this.reason)
resolvePromise(promise2, x ,resolve, reject)
} catch (error) {
reject(error)
}
})
})
}
複製代碼
Promise 解決過程是一個抽象的操做,其需輸入一個 promise 和一個值,咱們表示爲
[[Resolve]](promise, x)
,若是 x 有 then 方法且看上去像一個 Promise ,解決程序即嘗試使 promise 接受 x 的狀態;不然其用 x 的值來執行 promise 。
這段話比較抽象,通俗一點的來講就是promise的解決過程須要傳入一個新的promise和一個值x,若是傳入的x是一個thenable的對象(具備then方法),就接受x的狀態:
//promise2:新的Promise對象
//x:上一個then的返回值
//resolve:promise2的resolve
//reject:promise2的reject
function resolvePromise(promise2, x, resolve, reject) {
}
複製代碼
定義好函數後,來看具體的操做說明:
首先第一點,若是x和promise相等,這是一種什麼狀況呢,就是至關於把本身返回出去了:
var p = new Promise(function(resolve, reject){
resolve(3)
});
//Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>
var p2 = p.then(function(){
return p2
})
複製代碼
這樣會陷入一個死循環中,所以咱們首先要把這種狀況給排除掉:
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
reject(new TypeError('Chaining cycle'));
}
}
複製代碼
接下來就是對不一樣狀況的判斷了,首先咱們把 x 爲對象或者函數的狀況給判斷出來:
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
reject(new TypeError('Chaining cycle'));
}
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
//函數或對象
} else {
//普通值
resolve(x)
}
}
複製代碼
若是 x 爲對象或函數,就把 x.then 賦值給 then好理解,可是第二點取then有可能會報錯是爲何呢?這是由於須要考慮到全部出錯的狀況(防小人不防君子),若是有人實現Promise對象的時候使用Object.defineProperty()惡意拋錯,致使程序崩潰,就像這樣:
var Promise = {};
Object.defineProperty(Promise, 'then', {
get: function(){
throw Error('error')
}
})
//Uncaught Error: error
Promise.then
複製代碼
所以,咱們取then的時候也須要try/catch:
//其餘代碼略
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
//函數或對象
try {
let then = x.then
} catch(e){
reject(e)
}
}
複製代碼
取出then後,回到3.3,判斷若是是一個函數,就將 x 做爲函數的做用域 this 調用,同時傳入兩個回調函數做爲參數。
//其餘代碼略
try {
let then = x.then
if(typeof then === 'function'){
then.call(x, (y)=>{
resolve(y)
}, (r) =>{
reject(r)
})
} else {
resolve(x)
}
} catch(e){
reject(e)
}
複製代碼
這樣,咱們的鏈式調用就能順利的調用起來了;可是還有一種特殊的狀況,若是resolve的y值仍是一個Promise對象,這時就應該繼續執行,好比下面的例子:
var p1 = new Promise((resolve, reject)=>{
resolve('p1')
})
p1.then((res)=>{
return new Promise((resolve, reject)=>{
resolve(new Promise((resolve, reject)=>{
resolve('p2')
}))
})
})
.then((res1)=>{
//Promise {state: "fulfilled", value: "p2"}
console.log(res1)
})
複製代碼
這時候第二個then打印出來的是一個promise對象;咱們應該繼續遞歸調用resolvePromise(參考規範3.3.1),所以,最終resolvePromise的完整代碼以下:
function resolvePromise(promise2, x, resolve, reject){
if(promise2 === x){
reject(new TypeError('Chaining cycle'))
}
if(x && typeof x === 'object' || typeof x === 'function'){
let used;
try {
let then = x.then
if(typeof then === 'function'){
then.call(x, (y)=>{
if (used) return;
used = true
resolvePromise(promise2, y, resolve, reject)
}, (r) =>{
if (used) return;
used = true
reject(r)
})
} else {
if (used) return;
used = true
resolve(x)
}
} catch(e){
if (used) return;
used = true
reject(e)
}
} else {
resolve(x)
}
}
複製代碼
到這裏,咱們的Promise也可以完整的實現鏈式調用了;而後把代碼用promises-aplus-tests測試一下,完美的經過了872項測試。
promise/A規範(英文) promise/A+規範(英文) promise/A+規範(中文)
更多前端資料請關注公衆號【前端壹讀】
。