Promise的實(shí)現(xiàn)與標(biāo)準(zhǔn)

轉(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');

可以看到這段代碼中用到了setTimeoutprocess.nextTicksetImmediate戚绕,這三個(gè)方法在JS中都是異步去執(zhí)行的纹坐,輸出結(jié)果如下

script start
script end
nextTick
setTimeout
setImmediate

為什么同樣是異步方法process.nextTick中的回調(diào)函數(shù)就會(huì)在setTimeoutsetImmediate中的回調(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)行的谷誓,可以看下圖。


image.png

在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)行的有setTimeOutsetImmediate,而已MicroTask方式運(yùn)行的有process.nextTick嚷炉、MutationObserver渊啰。這也就是上面的例子中process.nextTick中回調(diào)優(yōu)先執(zhí)行的原因。因?yàn)?code>process.nextTick中回調(diào)被添加到了Microtask Queue,而setTimeOutsetImmediate中的回調(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腐泻、reslovedrejected。一旦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)換到resolvedrejected狀態(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);
  • onResolvedonRejected參數(shù)都是可選的,如果onResolvedonRejected不是function坤次,則忽略相應(yīng)的參數(shù)古劲。onResolvedonRejected都不能被調(diào)用超過一次。

  • onResolvedonRejected需要通過異步的方式執(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è)Promise

    promise2 = promise1.then(onResolved, onRejected);
    
    1. 如果onResolvedonRejected返回一個(gè)x,那么promise2的狀態(tài)需要根據(jù)x來決定(至于如何決定promise2的狀態(tài)蹬挤,會(huì)在第三部分中說明)缚窿。
    2. 如果onResolvedonRejected拋出一個(gè)異常e,那么promise2必須rejected且reason = e棘幸。
    3. 如果promise1是resolved狀態(tài)且onResolved不是一個(gè)function那么promise2必須resolved焰扳,并且promise2的value必須與promise1相同
    4. 如果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.如果xpromise是同一個(gè)對象的引用(x === promise),那么reject promise并將一個(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來resolve promise躬拢。

    • 2.3 如果x處于rejected狀態(tài)躲履,那么用x的reason來reject promise

  • 3.如果x是一個(gè)對象或function

    • 3.1 如果獲取屬性x.then的過程中拋出異常e,那么將e作為reason來reject promise

    • 3.2 如果x.then是一個(gè)function聊闯,那么調(diào)用x.then傳入?yún)?shù)resolvePromiserejectPromise

      • 3.2.1 如果resolvePromise被調(diào)用且傳入的參數(shù)為y工猜,那么再次執(zhí)行此操作,參數(shù)為(promise, y)

      • 3.2.2 如果rejectPromise被調(diào)用且傳入的參數(shù)r菱蔬,那么將r作為reason來reject promise

      • 3.2.3 如果resolvePromiserejectPromise同時(shí)被調(diào)用篷帅,或者被調(diào)用多次,那么優(yōu)先處理第一次調(diào)用,之后的調(diào)用都應(yīng)該被忽略犹褒。

      • 3.2.4 如果調(diào)用x.then拋出了異常e抵窒,若在拋出異常前resolvePromiserejectPromise已經(jīng)被調(diào)用,那么忽略異常即可叠骑。若resolvePromiserejectPromise沒有被調(diào)用過李皇,那么將e作為reason來reject promise

    • 3.3 如果x.then不是一個(gè)function,那么用x來resolve promise

  • 4.如果x既不是對象也不是function宙枷,那么用x來resolve promise

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末掉房,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子慰丛,更是在濱河造成了極大的恐慌卓囚,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件诅病,死亡現(xiàn)場離奇詭異哪亿,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)贤笆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門蝇棉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人芥永,你說我怎么就攤上這事篡殷。” “怎么了埋涧?”我有些...
    開封第一講書人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵板辽,是天一觀的道長。 經(jīng)常有香客問我棘催,道長劲弦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任巧鸭,我火速辦了婚禮瓶您,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘纲仍。我一直安慰自己呀袱,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開白布郑叠。 她就那樣靜靜地躺著夜赵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪乡革。 梳的紋絲不亂的頭發(fā)上寇僧,一...
    開封第一講書人閱讀 51,462評(píng)論 1 302
  • 那天摊腋,我揣著相機(jī)與錄音,去河邊找鬼嘁傀。 笑死兴蒸,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的细办。 我是一名探鬼主播橙凳,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼笑撞!你這毒婦竟也來了岛啸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬榮一對情侶失蹤茴肥,失蹤者是張志新(化名)和其女友劉穎义起,沒想到半個(gè)月后已烤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體裙盾,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡缴守,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年诱渤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了仍秤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片畴博。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡吮成,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出郁稍,到底是詐尸還是另有隱情,我是刑警寧澤胜宇,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布耀怜,位于F島的核電站,受9級(jí)特大地震影響桐愉,放射性物質(zhì)發(fā)生泄漏财破。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一从诲、第九天 我趴在偏房一處隱蔽的房頂上張望左痢。 院中可真熱鬧,春花似錦系洛、人聲如沸俊性。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽定页。三九已至,卻和暖如春绽诚,著一層夾襖步出監(jiān)牢的瞬間典徊,已是汗流浹背杭煎。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留卒落,地道東北人羡铲。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像儡毕,于是被迫代替她去往敵國和親犀勒。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容