- 前言
- API
- Promise特點
- 狀態跟隨
- V8中的async await和Promise
- 實現一個Promise
- 參考
前言
作為一個前端開發,使用了Promise一年多了,一直以來都停留在API的呼叫階段,沒有很好的去深入。剛好最近閱讀了V8團隊的一篇如何實現更快的async await,藉著這個機會整理了Promise的相關理解。文中如有錯誤,請輕噴~
API
Promise是社群中對於非同步的一種解決方案,相對於回撥函式和事件機制更直觀和容易理解。ES6 將其寫進了語言標準,統一了用法,提供了原生的Promise物件。
這裡只對API的一些特點做記錄,如果需要詳細教程,推薦阮老師的Promise物件一文
new Promise
–建立一個promise例項
Promise.prototype.then(resolve, reject)
–then方法返回一個新的Promise例項
Promise.prototype.catch(error)
–.then(null, rejection)或.then(undefined, rejection)的別名,用於指定發生錯誤時的回撥函式。
–錯誤會一直傳遞,直到被catch,如果沒有catch,則沒有任何反應
–catch返回一個新的Promise例項
Promise.prototype.finally()
–指定不管 Promise 物件最後狀態如何,都會執行的操作。
–實現如下:
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
Promise.all([promise Array])
–將多個 Promise 例項,包裝成一個新的 Promise 例項
–所有子promise執行完成後,才執行all的resolve,引數為所有子promise返回的陣列
–某個子promise出錯時,執行all的reject,引數為第一個被reject的例項的返回值
–某個子promise自己catch時,不會傳遞reject給all,因為catch重新返回一個promise例項
Promise.race([promise Array])
–將多個 Promise 例項,包裝成一個新的 Promise 例項。
–子promise有一個例項率先改變狀態,race的狀態就跟著改變。那個率先改變的 Promise 例項的返回值,就傳遞給race的回撥函式。
Promise.resolve()
–將現有物件轉為 Promise 物件
–引數是promise例項, 原封不動的返回
–引數是一個thenable物件 將這個物件轉為 Promise 物件,狀態為resolved
–引數是一個原始值 返回一個新的 Promise 物件,狀態為resolved
–不帶有任何引數 返回一個resolved狀態的 Promise 物件。
–等價於如下程式碼
Promise.resolve(`foo`)
// 等價於
new Promise(resolve => resolve(`foo`))
Promise.reject()
–返回一個新的 Promise 例項,該例項的狀態為rejected
–Promise.reject()方法的引數,會原封不動地作為reject的理由,變成後續方法的引數。
Promise特點
很多文章都是把resolve當成fulfilled,本文也是,但本文還有另外一個resolved,指的是該Promise已經被處理,注意兩者的區別
1. 物件具有三個狀態,分別是pending(進行中)、fulfilled(resolve)(已成功)、reject(已失敗),並且物件的狀態不受外界改變,只能從pending到fulfilled或者pending到reject。
2. 一旦狀態被改變,就不會再變,任何時候都能得到這個結果,與事件回撥不同,事件回撥在事件過去後無法再呼叫函式。
3. 一個promise一旦resolved,再次resolve/reject將失效。即只能resolved一次。
4. 值穿透,傳給then或者catch的引數為非函式時,會發生穿透(下面有示例程式碼)
5. 無法取消,Promise一旦執行,無法取消。
6. 如果不設定回撥函式,Promise內部丟擲的錯誤,不會反應到外部
7. 處於pending時,無法感知promise的狀態(剛剛開始還是即將完成)。
值穿透程式碼:
new Promise(resolve=>resolve(8))
.then()
.then()
.then(function foo(value) {
console.log(value) // 8
})
狀態追隨
狀態追隨的概念和下面的v8處理asyac await相關聯
狀態跟隨就是指將一個promise(代指A)當成另外一個promise(代指B)的resolve引數,即B的狀態會追隨A。
如下程式碼所示:
const promiseA = new Promise((resolve) => {
setTimeout(() => {
resolve(`ccc`)
}, 3000)
})
const promiseB = new Promise(res => {
res(promiseA)
})
promiseB.then((arg) => {
console.log(arg) // print `ccc` after 3000ms
})
按理說,promiseB應該是已經處於resolve的狀態, 但是依然要3000ms後才列印出我們想要的值, 這難免讓人困惑
在ES的標準文件中有這麼一句話可以幫助我們理解:
A resolved promise may be pending, fulfilled or rejected.
就是說一個已經處理的promise,他的狀態有可能是pending, fulfilled 或者 rejected。 這與我們前面學的不一樣啊, resolved了的promise不應該是處於結果狀態嗎?這確實有點反直覺,結合上面的例子看,當處於狀態跟隨時,即使promiseB立即被resolved了,但是因為他追隨了promiseA的狀態,而A的狀態則是pending,所以才說處於resolved的promiseB的狀態是pending。
再看另外一個例子:
const promiseA = new Promise((resolve) => {
setTimeout(() => {
resolve(`ccc`)
}, 3000)
})
const promiseB = new Promise(res => {
setTimeout(() => {
res(promiseA)
}, 5000)
})
promiseB.then((arg) => {
console.log(arg) // print `ccc` after 5000ms
})
其實理解了上面的話,這一段的程式碼也比較容易理解,只是因為自己之前進了牛角尖,所以特意記錄下來:
- 3s後 promiseA狀態變成resolve
- 5s後 promiseB被resolved, 追隨promiseA的狀態
- 因為promiseA的狀態為resolve, 所以列印 ccc
V8中的async await和Promise
在進入正題之前,我們可以先看下面這段程式碼:
const p = Promise.resolve();
(async () => {
await p;
console.log("after:await");
})();
p.then(() => {
console.log("tick:a");
}).then(() => {
console.log("tick:b");
});
V8團隊的部落格中, 說到這段程式碼的執行結果有兩種:
Node8(錯誤的):
tick a
tick b
after: await
Node10(正確的):
after await
tick a
tick b
ok, 問題來了, 為啥是這個樣子?
先從V8對於await的處理說起, 這裡引用一張官方部落格的圖來說明Node8 await的虛擬碼:
Node8 await
對於上面的例子程式碼翻譯過來就(該程式碼引用自V8是怎麼實現更快的async await)是:
const p = Promise.resolve();
(() => {
const implicit_promise = new Promise(resolve => {
const promise = new Promise(res => res(p));
promise.then(() => {
console.log("after:await");
resolve();
});
});
return implicit_promise;
})();
p.then(() => {
console.log("tick:a");
}).then(() => {
console.log("tick:b");
});
很明顯,內部那一句 new Promise(res => res(p)); 程式碼就是一個狀態跟隨,即promise追隨p的狀態,那這跟上面的結果又有什麼關係?
在繼續深入之前, 我們還需要了解一些概念:
task和microtask
JavaScript 中有 task 和 microtask 的概念。 Task 處理 I/O 和計時器等事件,一次執行一個。 Microtask 為 async/await 和 promise 實現延遲執行,並在每個任務結束時執行。 總是等到 microtasks 佇列被清空,事件迴圈執行才會返回。
如官方提供的一張圖:
EnqueueJob、PromiseResolveThenableJob和PromiseReactionJob
EnquequeJob: 存放兩種型別的任務, 即PromiseResolveThenableJob和PromiseReactionJob, 並且都是屬於microtask型別的任務
PromiseReactionJob: 可以通俗的理解為promise中的回撥函式
PromiseResolveThenableJob(promiseToResolve, thenable, then): 建立和 promiseToResolve 關聯的 resolve function 和 reject function。以 then 為呼叫函式,thenable 為this,resolve function和reject function 為引數呼叫返回。(下面利用程式碼講解)
狀態跟隨的內部
再以之前的程式碼為例子
const promiseA = new Promise((resolve) => {
resolve(`ccc`)
})
const promiseB = new Promise(res => {
res(promiseA)
})
當promiseB被resolved的時候, 也就是將一個promise(代指A)當成另外一個promise(代指B)的resolve引數,會向EnquequeJob插入一個PromiseResolveThenableJob任務,PromiseResolveThenableJob大概做了如下的事情:
() => {
promiseA.then(
resolvePromiseB,
rejectPromiseB
);
}
並且當resolvePromiseB執行後, promiseB的狀態才變成resolve,也就是B追隨A的狀態
Node 8中的流程
1. p處於resolve狀態,promise呼叫then被resolved,同時向microtask插入任務PromiseResolveThenableJob
2. p.then被呼叫, 向microtask插入任務tickA
3. 執行PromiseResolveThenableJob, 向microtask插入任務resolvePromise(如上面的promiseA.then(...))
4. 執行tickA(即輸出tick: a),返回一個promise, 向microtask插入任務tickB
5. 因為microtask的任務還沒執行完, 繼續
6. 執行resolvePromise, 此時promise終於變成resolve, 向microtask插入任務`after await`
7. 執行tickB(即輸出tick: b)
8. 執行`after await`(即輸出`after await`)
Node 10 await
老規矩, 先補一張虛擬碼圖:
翻譯過來就是醬紫:
const p = Promise.resolve();
(() => {
const implicit_promise = new Promise(resolve => {
const promise = Promise.resolve(p)
promise.then(() => {
console.log("after:await");
resolve();
});
});
return implicit_promise;
})();
p.then(() => {
console.log("tick:a");
}).then(() => {
console.log("tick:b");
});
因為p是一個promise, 然後Promise.resolve會直接將P返回,也就是
p === promise // true
因為直接返回了p,所以省去了中間兩個microtask任務,並且輸出的順序也變得正常,也就是V8所說的更快的async await
實現一個Promise
先實現一個基礎的函式
function Promise(cb) {
const that = this
that.value = undefined // Promise的值
that.status = `pending` // Promise的狀態
that.resolveArray = [] // resolve函式集合
that.rejectArray = [] // reject函式集合
function resolve(value) {
if (value instanceof Promise) {
return value.then(resolve, reject)
}
setTimeout(function() {
if (that.status === `pending`) { // 處於pending狀態 迴圈呼叫
that.value = value
that.status = `resolve`
for(let i = 0; i < that.resolveArray.length; i++) {
that.resolveArray[i](value)
}
}
})
}
function reject(reason) {
if (reason instanceof Promise) {
return reason.then(resolve, reject)
}
setTimeout(function() {
if (that.status === `pending`) { // 處於pending狀態 迴圈呼叫
that.value = reason
that.status = `reject`
for(let i = 0; i < that.rejectArray.length; i++) {
that.rejectArray[i](reason)
}
}
})
}
try {
cb(resolve, reject)
} catch (e) {
reject(e)
}
}
Promise.prototype.then = function(onResolve, onReject) {
var that = this
var promise2 // 返回的Promise
onResolve = typeof onResolve === `function` ? onResolve : function(v) { return v } //如果不是函式 則處理穿透值
onReject = typeof onReject === `function` ? onReject : function(v) { return v } //如果不是函式 則處理穿透值
if (that.status === `resolve`) {
return promise2 = new Promise(function(resolve, reject) {
setTimeout(function() {
try {
const x = onResolve(that.value)
if (x instanceof Promise) { // 如果onResolved的返回值是一個Promise物件,直接取它的結果做為promise2的結果
x.then(resolve, reject)
} else {
resolve(x)
}
} catch (e) {
reject(e)
}
})
})
}
if (that.status === `reject`) {
return promise2 = new Promise(function(resolve, reject) {
setTimeout(function() {
try {
const x = onResolve(that.value)
if (x instanceof Promise) { // 如果onResolved的返回值是一個Promise物件,直接取它的結果做為promise2的結果
x.then(resolve, reject)
} else {
reject(x)
}
} catch (e) {
reject(e)
}
})
})
}
if (that.status === `pending`) {
return promise2 = new Promise(function(resolve, reject) {
that.resolveArray.push(function(value) {
try {
var x = onResolve(value)
if (x instanceof Promise) {
x.then(resolve, reject)
}
} catch (e) {
reject(e)
}
})
that.rejectArray.push(function(reason) {
try {
var x = onReject(reason)
if (x instanceof Promise) {
x.then(resolve, reject)
}
} catch (e) {
reject(e)
}
})
})
}
}
Promise.prototype.catch = function(onReject) {
return this.then(null, onReject)
}
參考
v8是怎麼實現更快的 await ?深入理解 await 的執行機制
V8中更快的非同步函式和promise
剖析Promise內部結構,一步一步實現一個完整的、能通過所有Test case的Promise類
PromiseA+
ES6入門
深入Promise