轉(zhuǎn)載同事寫的文章,從promise的標(biāo)準(zhǔn)角度來說明其實(shí)現(xiàn)慰毅,這樣不管在看Q隘截,還是bluebird的時(shí)候,都會(huì)容易很多。
JS是如何運(yùn)行的
每當(dāng)談起JS的時(shí)候婶芭,單線程东臀,異步,回調(diào)雕擂,非阻塞啡邑,event loop這些詞匯總是會(huì)出現(xiàn)贱勃。但是JS到底是如何運(yùn)行的呢井赌,不妨看一看Philip Roberts在JSConf上講解event loop的視頻。Philip Roberts還自己動(dòng)手做了一個(gè)JS runtime的可視化程序贵扰,這個(gè)可視化程序他在演講中也有展示仇穗。
讓我們來看一段代碼吧
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
process.nextTick(function() {
console.log('nextTick');
});
setImmediate(function() {
console.log('setImmediate');
});
console.log('script end');
可以看到這段代碼中用到了setTimeout
、process.nextTick
和setImmediate
戚绕,這三個(gè)方法在JS中都是異步去執(zhí)行的纹坐,輸出結(jié)果如下
script start
script end
nextTick
setTimeout
setImmediate
為什么同樣是異步方法process.nextTick
中的回調(diào)函數(shù)就會(huì)在setTimeout
和setImmediate
中的回調(diào)函數(shù)之前執(zhí)行?這就和task和microtask的機(jī)制有關(guān)了舞丛。關(guān)于task和microtask這里有一篇文章(Tasks, microtasks, queues and schedules)講述的非常好耘子,可以觀摩一下,學(xué)習(xí)學(xué)習(xí)球切。
為了更好的理解JS是如何運(yùn)行的谷誓,可以看下圖。
在JS運(yùn)行的過程中吨凑,event loop每一次循環(huán)都會(huì)將一個(gè)task從Task Queue
中取出捍歪,task執(zhí)行的過程中會(huì)調(diào)用不同的函數(shù)并壓棧。棧中的代碼會(huì)調(diào)用一些API鸵钝,當(dāng)這些API執(zhí)行結(jié)束后會(huì)將完成的任務(wù)加入Task Queue
(具體的實(shí)現(xiàn)因API而異糙臼,有些API可能會(huì)在單獨(dú)的線程中去處理這些操作)。當(dāng)stack
中的代碼全部執(zhí)行完成時(shí)會(huì)再次從Task Queue
中取出一個(gè)新的任務(wù)來執(zhí)行恩商,這樣就開始了新的一輪loop变逃。
那么Microtask是在什么時(shí)候執(zhí)行的呢?JS會(huì)在每一輪loop結(jié)束怠堪,也就是stack
中的代碼全部執(zhí)行完畢時(shí)揽乱,去執(zhí)行Microtask Queue
中的任務(wù)。當(dāng)Microtask Queue
中的任務(wù)全部執(zhí)行完成后再從Task Queue
中取出下一個(gè)任務(wù)研叫〈敢ぃ可以理解為執(zhí)行過程為
Task1 -> Microtask ->Task2
以Task方式運(yùn)行的有setTimeOut
、setImmediate
,而已MicroTask方式運(yùn)行的有process.nextTick
嚷炉、MutationObserver
渊啰。這也就是上面的例子中process.nextTick
中回調(diào)優(yōu)先執(zhí)行的原因。因?yàn)?code>process.nextTick中回調(diào)被添加到了Microtask Queue
,而setTimeOut
和setImmediate
中的回調(diào)則被添加到了Task Queue
的末尾,他們在之后的幾輪loop中才會(huì)被執(zhí)行绘证。
這里需要提一下有些地方將Task稱為MacroTask,將MicroTask稱為Jobs隧膏。
而在一些具體的實(shí)現(xiàn)中,可能會(huì)存在多個(gè)Task Queue
嚷那,根據(jù)不同的實(shí)現(xiàn)目的不同的Task Queue
之間存在不同的優(yōu)先級(jí)(例如有些瀏覽器可能更加注重UI渲染的性能胞枕,所以將UI相關(guān)任務(wù)的Task Queue優(yōu)先級(jí)提高)。
猜測Promise的實(shí)現(xiàn)
熟悉Promise的人都知道Promise有三個(gè)狀態(tài)魏宽,pending
腐泻、resloved
和rejected
。一旦Promise的狀態(tài)發(fā)生改變就再也不會(huì)變動(dòng)队询,且Promise包含的值也不會(huì)被改變派桩。
//e.g.1
console.log('script start');
let promise = new Promise(function(resolve, reject) {
console.log('in promise');
resolve('reslove promise');
});
promise.then(function(value) {
console.log('resolve: ', value);
}, function(reason) {
console.log('reason: ', reason);
});
console.log('script end')
上面這段代碼對于經(jīng)常使用Promise的人再簡單不過了,可以看下他的輸出結(jié)果蚌斩。
script start
in promise
script end
resolve: reslove promise
從e.g.1
的輸出結(jié)果可以看到傳給then方法的回調(diào)是在最后執(zhí)行的铆惑,所以可以判斷出new Promise(function)
中的function是同步執(zhí)行的,而then(reslove,reject)
中的resolve或reject是異步執(zhí)行的送膳。
熟悉Promise的人對下面一段代碼也自然不會(huì)感到陌生员魏。
//e.g.2
promise.then((value) => {
//do some stuff
}).then((value) => {
//do some stuff
}).then((value) => {
//do some stuff
}).catch((reason) => {
//do some stuff
});
為什么Promise能寫成鏈?zhǔn)降模?code>.then之后還能接著.then
叠聋?基于這一點(diǎn)可以判斷出then方法return的是一個(gè)Promise撕阎,那么既然是Promise就一定會(huì)有狀態(tài),那么調(diào)用then之后return的這個(gè)Promise的狀態(tài)是如何確定的呢晒奕?接著看下面的栗子闻书。
//e.g.3
let promise1 = new Promise(function(resolve, reject) {
resolve('reslove promise');
});
let promise2 = promise1.then(function onReslove(value) {
console.log('1 resolve: ', value);
return 1;
}, function onReject(reason) {
console.log('1 reason: ', reason);
});
promise2.then(function onReslove(value) {
console.log('2 resolve: ', value);
}, function onReject(reason) {
console.log('2 reason: ', reason);
});
執(zhí)行結(jié)果:
1 resolve: reslove promise
2 resolve: 1
可以看到當(dāng)在onReslove
中返回一個(gè)基礎(chǔ)類型的時(shí)候promise2
的狀態(tài)變成了resolved
。
如果把上面的return 1;
改為throw new Error('error');
會(huì)是什么樣呢脑慧?輸出結(jié)果如下:
1 resolve: reslove promise
2 reason: Error: error
at onReslove (/Users/lx/Documents/projects/VsTest/PromiseExample.js:40:11)
at <anonymous>
at process._tickCallback (internal/process/next_tick.js:188:7)
at Function.Module.runMain (module.js:607:11)
at startup (bootstrap_node.js:158:16)
at bootstrap_node.js:575:3
可以看到此時(shí)promise2
的狀態(tài)變?yōu)榱?code>rejected魄眉。那么如果我在onResolve()
中return一個(gè)處于不同狀態(tài)Promise會(huì)怎么樣呢?
//e.g.4
let promise1 = new Promise(function(resolve, reject) {
resolve('reslove promise');
});
let promise2 = promise1.then(function onReslove(value) {
console.log('1 resolve: ', value);
return promiseReturn;
}, function onReject(reason) {
console.log('1 reason: ', reason);
});
promise2.then(function onReslove(value) {
console.log('2 resolve: ', value);
return 1;
}, function onReject(reason) {
console.log('2 reason: ', reason);
});
//依次使promiseReturn等于以下值:
//pending狀態(tài)的Promise,5s后變?yōu)閞esolved狀態(tài)
let promiseReturn = new Promise(function(reslove, reject) {
setTimeout(() => {
reslove(1)
}, 5000);
});
//輸出結(jié)果為:
1 resolve: reslove promise
//5s之后
2 resolve: 1
//resolved狀態(tài)的Promise
let promiseReturn = Promise.resolve(1);
//輸出結(jié)果為:
1 resolve: reslove promise
2 resolve: 1
//rejected狀態(tài)的Promise
let promiseReturn = Promise.reject(new Error('error'));
//輸出結(jié)果為:
1 resolve: reslove promise
2 reason: Error: error
at Object.<anonymous> (/Users/lx/Documents/projects/VsTest/PromiseExample.js:33:36)
at Module._compile (module.js:569:30)
通過上面的例子可以看到闷袒,當(dāng)onResolve()
return一個(gè)Promise時(shí)坑律,promise2的狀態(tài)是和return的Promise的狀態(tài)相同的。
PromiseA+標(biāo)準(zhǔn)
[圖片上傳失敗...(image-a86d48-1516949283239)]
ES標(biāo)準(zhǔn)中的Promise囊骤,Q以及bluebird都是PromiseA+標(biāo)準(zhǔn)的實(shí)現(xiàn)晃择。 PromiseA+標(biāo)準(zhǔn)主要從三部分提出了對Promise實(shí)現(xiàn)的要求,第一部分規(guī)定了Promise的狀態(tài)已經(jīng)狀態(tài)的變化也物。第二部分則指定Promise的then
方法的行為宫屠。第三部分則是說明了如何決定then
方法返回的Promise的狀態(tài),并且支持了不同PromiseA+標(biāo)準(zhǔn)實(shí)現(xiàn)的Promise之間的兼容性滑蚯。
PromiseA+標(biāo)準(zhǔn)如下(更具體的標(biāo)準(zhǔn)戳這里):
Promise的狀態(tài)
Promise必須處于pending
,resolved
,rejected
三個(gè)狀態(tài)之一
- 當(dāng)Promise處于
pending
狀態(tài)時(shí)可以轉(zhuǎn)換到resolved
或rejected
狀態(tài) - 當(dāng)Promise處于
resolved
狀態(tài)時(shí)無法再轉(zhuǎn)換到其他狀態(tài)浪蹂,并且有一個(gè)無法改變value
- 當(dāng)Promise處于
rejected
狀態(tài)時(shí)無法再轉(zhuǎn)換到其他狀態(tài)抵栈,并且有一個(gè)無法改變的reason
(reason一般為一個(gè)Error對象)
Promise的then方法
Promise的then方法接受兩個(gè)參數(shù)
promise.then(onResolved, onRejected);
onResolved
和onRejected
參數(shù)都是可選的,如果onResolved
或onRejected
不是function坤次,則忽略相應(yīng)的參數(shù)古劲。onResolved
和onRejected
都不能被調(diào)用超過一次。onResolved
和onRejected
需要通過異步的方式執(zhí)行缰猴,可以用“macro-task”或“micro-task”機(jī)制來執(zhí)行产艾。同一個(gè)Promise的
then
方法可以被調(diào)用多次,當(dāng)該P(yáng)romise狀態(tài)變?yōu)?code>resolved或rejected
狀態(tài)時(shí)滑绒,注冊在該P(yáng)romise上的回調(diào)應(yīng)該根據(jù)注冊的順序被調(diào)用闷堡。-
then
方法會(huì)返回一個(gè)Promisepromise2 = promise1.then(onResolved, onRejected);
- 如果
onResolved
或onRejected
返回一個(gè)x
,那么promise2
的狀態(tài)需要根據(jù)x
來決定(至于如何決定promise2
的狀態(tài)蹬挤,會(huì)在第三部分中說明)缚窿。 - 如果
onResolved
或onRejected
拋出一個(gè)異常e
,那么promise2
必須rejected且reason = e
棘幸。 - 如果
promise1
是resolved狀態(tài)且onResolved
不是一個(gè)function那么promise2
必須resolved焰扳,并且promise2
的value必須與promise1
相同 - 如果
promise1
是rejected狀態(tài)且onRejected
不是一個(gè)function那么promise2
必須rejected,并且promise2
的reason必須與promise1
相同
- 如果
The Promise Resolution Procedure
個(gè)人感覺這個(gè)標(biāo)題不好“生翻”误续,直面的翻譯可能反倒容易讓人誤解吨悍。可以把這個(gè)部分理解為一種操作蹋嵌,該操作需要接受兩個(gè)參數(shù)(promise, x)
育瓜,會(huì)根據(jù)x
的情況來決定promise
的狀態(tài)。
在我們的onResolved
回調(diào)中一般會(huì)return一個(gè)value(如果沒有寫return xxx,那么value就等于undefined)栽烂。這里就可以把x
當(dāng)做這個(gè)value躏仇。調(diào)用then
方法時(shí)返回的Promise的狀態(tài)就是由這個(gè)x
來決定的。
如果x
是一個(gè)thenable(帶有then方法的對象或function)腺办,那么可以假設(shè)x
和Promise的行為相似焰手。這一點(diǎn)是為了讓不同PromiseA+標(biāo)準(zhǔn)的實(shí)現(xiàn)可以兼容。
The Promise Resolution Procedure這個(gè)操作的步驟如下:
1.如果
x
和promise
是同一個(gè)對象的引用(x === promise
),那么rejectpromise
并將一個(gè)TypeError
賦值給reason-
2.如果
x
是一個(gè)Promise(x instanceof Promise
),那么promise
的狀態(tài)入下:2.1 如果
x
處于pending狀態(tài)那么promise
也處于pending狀態(tài)怀喉,直到x
狀態(tài)變?yōu)閞esolved或rejected书妻。2.2 如果
x
處于resolved狀態(tài),那么用x
的value來resolvepromise
躬拢。2.3 如果
x
處于rejected狀態(tài)躲履,那么用x
的reason來rejectpromise
-
3.如果
x
是一個(gè)對象或function3.1 如果獲取屬性
x.then
的過程中拋出異常e
,那么將e
作為reason來rejectpromise
-
3.2 如果
x.then
是一個(gè)function聊闯,那么調(diào)用x.then
傳入?yún)?shù)resolvePromise
和rejectPromise
3.2.1 如果
resolvePromise
被調(diào)用且傳入的參數(shù)為y
工猜,那么再次執(zhí)行此操作,參數(shù)為(promise, y)
3.2.2 如果
rejectPromise
被調(diào)用且傳入的參數(shù)r
菱蔬,那么將r
作為reason來rejectpromise
3.2.3 如果
resolvePromise
和rejectPromise
同時(shí)被調(diào)用篷帅,或者被調(diào)用多次,那么優(yōu)先處理第一次調(diào)用,之后的調(diào)用都應(yīng)該被忽略犹褒。3.2.4 如果調(diào)用
x.then
拋出了異常e
抵窒,若在拋出異常前resolvePromise
或rejectPromise
已經(jīng)被調(diào)用,那么忽略異常即可叠骑。若resolvePromise
或rejectPromise
沒有被調(diào)用過李皇,那么將e
作為reason來rejectpromise
3.3 如果
x.then
不是一個(gè)function,那么用x
來resolvepromise
4.如果
x
既不是對象也不是function宙枷,那么用x
來resolvepromise