我以為我很懂Promise肄方,直到我開始實(shí)現(xiàn)Promise/A+規(guī)范

我一度以為自己很懂Promise,直到前段時(shí)間嘗試去實(shí)現(xiàn)Promise/A+規(guī)范時(shí)蹬癌,才發(fā)現(xiàn)自己對(duì)Promise的理解還過于淺薄权她。在我按照Promise/A+規(guī)范去寫具體代碼實(shí)現(xiàn)的過程中,我經(jīng)歷了從“很懂”到“陌生”逝薪,再到“領(lǐng)會(huì)”的過山車式的認(rèn)知轉(zhuǎn)變隅要,對(duì)Promise有了更深刻的認(rèn)識(shí)!

TL;DR:鑒于很多人不想看長文董济,這里直接給出我寫的Promise/A+規(guī)范的Javascript實(shí)現(xiàn)步清。

promises-tests測試用例是全部通過的。

image

Promise源于現(xiàn)實(shí)世界

Promise直譯過來就是承諾虏肾,最新的紅寶書已經(jīng)將其翻譯為期約廓啊。當(dāng)然,這都不重要封豪,程序員之間只要一個(gè)眼神就懂了谴轮。

你懂的

許下承諾

作為打工人,我們不可避免地會(huì)接到各種餅吹埠,比如口頭吹捧的餅第步、升值加薪的餅、股權(quán)激勵(lì)的餅......

有些餅馬上就兌現(xiàn)了藻雌,比如口頭褒獎(jiǎng)雌续,因?yàn)樗旧頉]有給企業(yè)帶來什么成本;有些餅卻關(guān)乎企業(yè)實(shí)際利益胯杭,它們可能未來可期驯杜,也可能猴年馬月,或是無疾而終做个,又或者直接宣告畫餅失敗鸽心。

畫餅這個(gè)動(dòng)作滚局,于Javascript而言,就是創(chuàng)建一個(gè)Promise實(shí)例:

const bing = new Promise((resolve, reject) => {
  // 祝各位的餅都能圓滿成功
  if ('畫餅成功') {
    resolve('大家happy')
  } else {
    reject('有難同當(dāng)')
  }
})

Promise跟這些餅很像顽频,分為三種狀態(tài):

  • pending: 餅已畫好藤肢,坐等實(shí)現(xiàn)糯景。
  • fulfilled: 餅真的實(shí)現(xiàn)了蟀淮,走上人生巔峰策治。
  • rejected: 不好意思通惫,畫餅失敗茂翔,emmm...

訂閱承諾

有人畫餅,自然有人接餅讽膏。所謂“接餅”檩电,就是對(duì)于這張餅的可能性做下設(shè)想拄丰。如果餅真的實(shí)現(xiàn)了府树,鄙人將別墅靠海;如果餅失敗了料按,本打工仔以淚洗面奄侠。

image

轉(zhuǎn)換成Promise中的概念,這是一種訂閱的模式载矿,成功和失敗的情況我們都要訂閱垄潮,并作出反應(yīng)。訂閱是通過then闷盔,catch等方法實(shí)現(xiàn)的弯洗。

// 通過then方法進(jìn)行訂閱
bing.then(
  // 對(duì)畫餅成功的情況作出反應(yīng)
  success => {
    console.log('別墅靠海')
  },
  // 對(duì)畫餅失敗的情況作出反應(yīng)
  fail => {
    console.log('以淚洗面...')
  }
)

鏈?zhǔn)絺鞑?/h2>

眾所周知,老板可以給高層或領(lǐng)導(dǎo)們畫餅逢勾,而領(lǐng)導(dǎo)們拿著老板畫的餅牡整,也必須給底下員工繼續(xù)畫餅,讓打工人們雞血不停溺拱,這樣大家的餅才都有可能兌現(xiàn)逃贝。

這種自上而下發(fā)餅的行為與Promise的鏈?zhǔn)秸{(diào)用在思路上不謀而合谣辞。

bossBing.then(
  success => {
    // leader接過boss的餅,繼續(xù)往下面發(fā)餅
    return leaderBing
  }
).then(
  success => {
    console.log('leader畫的餅真的實(shí)現(xiàn)了沐扳,別墅靠海')
  },
  fail => {
    console.log('leader畫的餅炸了泥从,以淚洗面...')
  }
)

總體來說,Promise與現(xiàn)實(shí)世界的承諾還是挺相似的沪摄。

image

而Promise在具體實(shí)現(xiàn)上還有很多細(xì)節(jié)躯嫉,比如異步處理的細(xì)節(jié),Resolution算法杨拐,等等和敬,這些在后面都會(huì)講到。下面我會(huì)從自己對(duì)Promise的第一印象講起戏阅,繼而過渡到對(duì)宏任務(wù)與微任務(wù)的認(rèn)識(shí)昼弟,最終揭開Promise/A+規(guī)范的神秘面紗。

初識(shí)Promise

還記得最早接觸Promise的時(shí)候奕筐,我感覺能把a(bǔ)jax過程封裝起來就挺“厲害”了。那個(gè)時(shí)候?qū)romise的印象大概就是:優(yōu)雅的異步封裝离赫,不再需要寫高耦合的callback渊胸。

這里臨時(shí)手?jǐn)]一個(gè)簡單的ajax封裝作為示例說明:

function isObject(val) {
  return Object.prototype.toString.call(val) === '[object Object]';
}

function serialize(params) {
    let result = '';
    if (isObject(params)) {
      Object.keys(params).forEach((key) => {
        let val = encodeURIComponent(params[key]);
        result += `${key}=${val}&`;
      });
    }
    return result;
}

const defaultHeaders = {
  "Content-Type": "application/x-www-form-urlencoded"
}

// ajax簡單封裝
function request(options) {
  return new Promise((resolve, reject) => {
    const { method, url, params, headers } = options
    const xhr = new XMLHttpRequest();
    if (method === 'GET' || method === 'DELETE') {
      // GET和DELETE一般用querystring傳參
      const requestURL = url + '?' + serialize(params)
      xhr.open(method, requestURL, true);
    } else {
      xhr.open(method, url, true);
    }
    // 設(shè)置請(qǐng)求頭
    const mergedHeaders = Object.assign({}, defaultHeaders, headers)
    Object.keys(mergedHeaders).forEach(key => {
      xhr.setRequestHeader(key, mergedHeaders[key]);
    })
    // 狀態(tài)監(jiān)聽
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          resolve(xhr.response)
        } else {
          reject(xhr.status)
        }
      }
    }
    xhr.onerror = function(e) {
      reject(e)
    }
    // 處理body數(shù)據(jù)疫稿,發(fā)送請(qǐng)求
    const data = method === 'POST' || method === 'PUT' ? serialize(params) : null
    xhr.send(data);
  })
}

const options = {
  method: 'GET',
  url: '/user/page',
  params: {
    pageNo: 1,
    pageSize: 10
  }
}
// 通過Promise的形式調(diào)用接口
request(options).then(res => {
  // 請(qǐng)求成功
}, fail => {
  // 請(qǐng)求失敗
})

以上代碼封裝了ajax的主要過程,而其他很多細(xì)節(jié)和各種場景覆蓋就不是幾十行代碼能說完的。不過我們可以看到,Promise封裝的核心就是:

  • 封裝一個(gè)函數(shù),將包含異步過程的代碼包裹在構(gòu)造Promise的executor中,所封裝的函數(shù)最后需要return這個(gè)Promise實(shí)例驾茴。
  • Promise有三種狀態(tài)峡捡,Pending, Fulfilled, Rejected。而resolve(), reject()是狀態(tài)轉(zhuǎn)移的觸發(fā)器摔刁。
  • 確定狀態(tài)轉(zhuǎn)移的條件,在本例中猎贴,我們認(rèn)為ajax響應(yīng)且狀態(tài)碼為200時(shí)装蓬,請(qǐng)求成功(執(zhí)行resolve())蹂随,否則請(qǐng)求失敿ㄖ浴(執(zhí)行reject())宪肖。

ps: 實(shí)際業(yè)務(wù)中,除了判斷HTTP狀態(tài)碼蜕衡,我們還會(huì)另外判斷內(nèi)部錯(cuò)誤碼(業(yè)務(wù)系統(tǒng)中前后端約定的狀態(tài)code)壤短。

實(shí)際上現(xiàn)在有了axios這類的解決方案,我們也不會(huì)輕易選擇自行封裝ajax慨仿,不鼓勵(lì)重復(fù)造這種基礎(chǔ)且重要的輪子久脯,更別說有些場景我們往往難以考慮周全。當(dāng)然镰吆,在時(shí)間允許的情況下帘撰,可以學(xué)習(xí)其源碼實(shí)現(xiàn)。

宏任務(wù)與微任務(wù)

要理解Promise/A+規(guī)范万皿,必須先溯本求源摧找,Promise與微任務(wù)息息相關(guān),所以我們有必要先對(duì)宏任務(wù)和微任務(wù)有個(gè)基本認(rèn)識(shí)牢硅。

在很長一段時(shí)間里蹬耘,我都沒有太多去關(guān)注宏任務(wù)(Task)與微任務(wù)(Microtask)。甚至有一段時(shí)間减余,我覺得setTimeout(fn, 0)在操作動(dòng)態(tài)生成的DOM元素時(shí)非常好用综苔,然而并不知道其背后的原理,實(shí)質(zhì)上這跟Task聯(lián)系緊密位岔。

var button = document.createElement('button');
button.innerText = '新增輸入框'
document.body.append(button)

button.onmousedown = function() {
  var input = document.createElement('input');
  document.body.appendChild(input);
  setTimeout(function() {
    input.focus();
  }, 0)
}

如果不使用setTimeout 0如筛,focus()會(huì)沒有效果。

那么赃承,什么是宏任務(wù)和微任務(wù)呢妙黍?我們慢慢來揭開答案。

現(xiàn)代瀏覽器采用多進(jìn)程架構(gòu)瞧剖,這一點(diǎn)可以參考Inside look at modern web browser。而和我們前端關(guān)系最緊密的就是其中的Renderer Process,Javascript便是運(yùn)行在Renderer Process的Main Thread中抓于。

image

Renderer: Controls anything inside of the tab where a website is displayed.

渲染進(jìn)程控制了展示在Tab頁中的網(wǎng)頁的一切事情做粤。可以理解為渲染進(jìn)程就是專門為具體的某個(gè)網(wǎng)頁服務(wù)的捉撮。

我們知道怕品,Javascript可以直接與界面交互。假想一下巾遭,如果Javascript采用多線程策略肉康,各個(gè)線程都能操作DOM,那最終的界面呈現(xiàn)到底以誰為準(zhǔn)呢灼舍?這顯然是存在矛盾的吼和。因此,Javascript選擇使用單線程模型的一個(gè)重要原因就是:為了保證用戶界面的強(qiáng)一致性骑素。

為了保證界面交互的連貫性和平滑度炫乓,Main Thread中,Javascript的執(zhí)行和頁面的渲染會(huì)交替執(zhí)行(出于性能考慮献丑,某些情況下末捣,瀏覽器判斷不需要執(zhí)行界面渲染,會(huì)略過渲染的步驟)创橄。目前大多數(shù)設(shè)備的屏幕刷新率為60次/秒箩做,1幀大約是16.67ms,在這1幀的周期內(nèi)妥畏,既要完成Javascript的執(zhí)行卒茬,還要完成界面的渲染(if necessary),利用人眼的殘影效應(yīng)咖熟,讓用戶覺得界面交互是非常流暢的圃酵。

用一張圖看看1幀的基本過程,引用自https://aerotwist.com/blog/the-anatomy-of-a-frame/

PS:requestIdleCallback是空閑回調(diào)馍管,在1幀的末尾郭赐,如果還有時(shí)間富余,就會(huì)調(diào)用requestIdleCallback确沸。注意不要在requestIdleCallback中修改DOM捌锭,或者讀取布局信息導(dǎo)致觸發(fā)Forced Synchronized Layout,否則會(huì)引發(fā)性能和體驗(yàn)問題罗捎。具體見Using requestIdleCallback观谦。

我們知道,一個(gè)網(wǎng)頁中的Render Process只有一個(gè)Main Thread桨菜,本質(zhì)上來說豁状,Javascript的任務(wù)在執(zhí)行階段都是按順序執(zhí)行捉偏,但是JS引擎在解析Javascript代碼時(shí),會(huì)把代碼分為同步任務(wù)和異步任務(wù)泻红。同步任務(wù)直接進(jìn)入Main Thread執(zhí)行夭禽;異步任務(wù)進(jìn)入任務(wù)隊(duì)列,并關(guān)聯(lián)著一個(gè)異步回調(diào)谊路。

在一個(gè)web app中讹躯,我們會(huì)寫一些Javascript代碼或者引用一些腳本,用作應(yīng)用的初始化工作缠劝。在這些初始代碼中潮梯,會(huì)按照順序執(zhí)行其中的同步代碼。而在這些同步代碼執(zhí)行的過程中惨恭,會(huì)陸陸續(xù)續(xù)監(jiān)聽一些事件或者注冊(cè)一些異步API(網(wǎng)絡(luò)相關(guān)秉馏,IO相關(guān),等等...)的回調(diào)喉恋,這些事件處理程序和回調(diào)就是異步任務(wù)沃饶,異步任務(wù)會(huì)進(jìn)入任務(wù)隊(duì)列,并且在接下來的Event Loop中被處理轻黑。

異步任務(wù)又分為TaskMicrotask糊肤,各自有單獨(dú)的數(shù)據(jù)結(jié)構(gòu)和內(nèi)存來維護(hù)贯莺。

用一個(gè)簡單的例子來感受下:

var a = 1;
console.log('a:', a)
var b = 2;
console.log('b:', b)
setTimeout(function task1(){
  console.log('task1:', 5)
  Promise.resolve(6).then(function microtask2(res){
    console.log('microtask2:', res)
  })
}, 0)
Promise.resolve(4).then(function microtask1(res){
  console.log('microtask1:', res)
})
var b = 3;
console.log('c:', c)

以上代碼執(zhí)行后啦膜,依次在控制臺(tái)輸出:

a: 1
b: 2
c: 3
microtask1: 4
task1: 5
microtask2: 6

仔細(xì)一看也沒什么難的家肯,但是這背后發(fā)生的細(xì)節(jié)挽绩,還是有必要探究下。我們不妨先問自己幾個(gè)問題刹枉,一起來看下吧幻工。

Task和Microtask都有哪些煞茫?

  • Tasks:
    • setTimeout
    • setInterval
    • MessageChannel
    • I/0(文件态罪,網(wǎng)絡(luò))相關(guān)API
    • DOM事件監(jiān)聽:瀏覽器環(huán)境
    • setImmediate:Node環(huán)境噩茄,IE好像也支持(見caniuse數(shù)據(jù))
  • Microtasks:
    • requestAnimationFrame:瀏覽器環(huán)境
    • MutationObserver:瀏覽器環(huán)境
    • Promise.prototype.then, Promise.prototype.catch, Promise.prototype.finally
    • process.nextTick:Node環(huán)境
    • queueMicrotask

requestAnimationFrame是不是微任務(wù)?

requestAnimationFrame簡稱rAF复颈,經(jīng)常被我們用來做動(dòng)畫效果绩聘,因?yàn)槠浠卣{(diào)函數(shù)執(zhí)行頻率與瀏覽器屏幕刷新頻率保持一致,也就是我們通常說的它能實(shí)現(xiàn)60FPS的效果耗啦。在rAF被大范圍應(yīng)用前凿菩,我們經(jīng)常使用setTimeout來處理動(dòng)畫。但是setTimeout在主線程繁忙時(shí)帜讲,不一定能及時(shí)地被調(diào)度衅谷,從而出現(xiàn)卡頓現(xiàn)象。

那么rAF屬于宏任務(wù)或者微任務(wù)嗎似将?其實(shí)很多網(wǎng)站都沒有給出定義获黔,包括MDN上也描述得非常簡單蚀苛。

我們不妨自己問問自己,rAF是宏任務(wù)嗎肢执?我想了一下枉阵,顯然不是译红,rAF可以用來代替定時(shí)器動(dòng)畫预茄,怎么能和定時(shí)器任務(wù)一樣被Event Loop調(diào)度呢?

我又問了問自己侦厚,rAF是微任務(wù)嗎耻陕?rAF的調(diào)用時(shí)機(jī)是在下一次瀏覽器重繪之前,這看起來和微任務(wù)的調(diào)用時(shí)機(jī)差不多刨沦,曾讓我一度認(rèn)為rAF是微任務(wù)诗宣,而實(shí)際上rAF也不是微任務(wù)。為什么這么說呢想诅?請(qǐng)運(yùn)行下這段代碼召庞。

function recursionRaf() {
    requestAnimationFrame(() => {
        console.log('raf回調(diào)')
        recursionRaf()
    })
}
recursionRaf();

你會(huì)發(fā)現(xiàn),在無限遞歸的情況下来破,rAF回調(diào)正常執(zhí)行篮灼,瀏覽器也可正常交互,沒有出現(xiàn)阻塞的現(xiàn)象徘禁。

遞歸rAF并沒有阻塞

而如果rAF是微任務(wù)的話诅诱,則不會(huì)有這種待遇。不信你可以翻到后面一節(jié)內(nèi)容「如果Microtask執(zhí)行時(shí)又創(chuàng)建了Microtask送朱,怎么處理娘荡?」。

所以驶沼,rAF的任務(wù)級(jí)別是很高的炮沐,擁有單獨(dú)的隊(duì)列維護(hù)。在瀏覽器1幀的周期內(nèi)回怜,rAF與Javascript執(zhí)行大年,瀏覽器重繪是同一個(gè)Level的。(其實(shí)鹉戚,大家在前面那張「解剖1幀」的圖中也能看出來了鲜戒。)

Task和Microtask各有1個(gè)隊(duì)列?

最初抹凳,我認(rèn)為既然瀏覽器區(qū)分了Task和Microtask遏餐,那就只要各自安排一個(gè)隊(duì)列存儲(chǔ)任務(wù)即可。事實(shí)上赢底,Task根據(jù)task source的不同失都,安排了獨(dú)立的隊(duì)列柏蘑。比如Dom事件屬于Task,但是Dom事件有很多種類型粹庞,為了方便user agent細(xì)分Task并精細(xì)化地安排各種不同類型Task的處理優(yōu)先級(jí)咳焚,甚至做一些優(yōu)化工作,必須有一個(gè)task source來區(qū)分庞溜。同理革半,Microtask也有自己的microtask task source。

具體解釋見HTML標(biāo)準(zhǔn)中的一段話:

Essentially, task sources are used within standards to separate logically-different types of tasks, which a user agent might wish to distinguish between. Task queues *are used by user agents to coalesce task sources within a given event loop流码。

Task和Microtask的消費(fèi)機(jī)制是怎樣的又官?

An event loop has one or more task queues. A task queue is a set of tasks.

javascript是事件驅(qū)動(dòng)的,所以Event Loop是異步任務(wù)調(diào)度的核心漫试。雖然我們一直說任務(wù)隊(duì)列六敬,但是Tasks在數(shù)據(jù)結(jié)構(gòu)上不是隊(duì)列(Queue),而是集合(Set)驾荣。在每一輪Event Loop中外构,會(huì)取出第一個(gè)runnable的Task(第一個(gè)可執(zhí)行的Task,并不一定是順序上的第一個(gè)Task)進(jìn)入Main Thread執(zhí)行播掷,然后再檢查Microtask隊(duì)列并執(zhí)行隊(duì)列中所有Microtask审编。

說再多,都不如一張圖直觀叮趴,請(qǐng)看割笙!

event loop

Task和Microtask什么時(shí)候進(jìn)入相應(yīng)隊(duì)列?

回過頭來看,我們一直在提這個(gè)概念“異步任務(wù)進(jìn)入隊(duì)列”眯亦,那么就有個(gè)疑問伤溉,Task和Microtask到底是什么時(shí)候進(jìn)入相應(yīng)的隊(duì)列?我們重新來捋捋妻率。異步任務(wù)有注冊(cè)乱顾,進(jìn)隊(duì)列回調(diào)被執(zhí)行這三個(gè)關(guān)鍵行為宫静。注冊(cè)很好理解走净,代表這個(gè)任務(wù)被創(chuàng)建了;而回調(diào)被執(zhí)行則代表著這個(gè)任務(wù)已經(jīng)被主線程撈起并執(zhí)行了孤里。但是伏伯,在進(jìn)隊(duì)列這一行為上,宏任務(wù)和微任務(wù)的表現(xiàn)是不一樣的捌袜。

宏任務(wù)進(jìn)隊(duì)列

對(duì)于Task而言说搅,任務(wù)注冊(cè)時(shí)就會(huì)進(jìn)入隊(duì)列,只是任務(wù)的狀態(tài)還不是runnable虏等,不具備被Event Loop撈起的條件弄唧。

我們先用Dom事件為例舉個(gè)例子适肠。

document.body.addEventListener('click', function(e) {
    console.log('被點(diǎn)擊了', e)
})

當(dāng)addEventListener這行代碼被執(zhí)行時(shí),任務(wù)就注冊(cè)了候引,代表有一個(gè)用戶點(diǎn)擊事件相關(guān)的Task進(jìn)入任務(wù)隊(duì)列侯养。那么這個(gè)宏任務(wù)什么時(shí)候才變成runnable呢?當(dāng)然是用戶點(diǎn)擊發(fā)生并且信號(hào)傳遞到瀏覽器Render Process的Main Thread后澄干,此時(shí)宏任務(wù)變成runnable狀態(tài)逛揩,才可以被Event Loop撈起,進(jìn)入Main Thread執(zhí)行傻寂。

這里再舉個(gè)例子息尺,順便解釋下為什么setTimeout 0會(huì)有延遲携兵。

setTimeout(function() {
    console.log('我是setTimeout注冊(cè)的宏任務(wù)')
}, 0)

執(zhí)行setTimeout這行代碼時(shí)疾掰,相應(yīng)的宏任務(wù)就被注冊(cè)了,并且Main Thread會(huì)告知定時(shí)器線程徐紧,“你定時(shí)0毫秒后給我一個(gè)消息”静檬。定時(shí)器線程收到消息,發(fā)現(xiàn)只要等待0毫秒并级,立馬就給Main Thread一個(gè)消息拂檩,“我這邊已經(jīng)過了0毫秒了”。Main Thread收到這個(gè)回復(fù)消息后嘲碧,就把相應(yīng)宏任務(wù)的狀態(tài)置為runnable稻励,這個(gè)宏任務(wù)就可以被Event Loop撈起了。

可以看到愈涩,經(jīng)過這樣一個(gè)線程間通信的過程望抽,即便是延時(shí)0毫秒的定時(shí)器,其回調(diào)也并不是在真正意義上的0毫秒之后執(zhí)行履婉,因?yàn)橥ㄐ胚^程就需要耗費(fèi)時(shí)間煤篙。網(wǎng)上有個(gè)觀點(diǎn)說setTimeout 0的響應(yīng)時(shí)間最少是4ms,其實(shí)也是有依據(jù)的毁腿,不過也是有條件的辑奈。

HTML Living Standard: If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.

對(duì)于這種說法,我覺得自己有個(gè)概念就行已烤,不同瀏覽器在實(shí)現(xiàn)規(guī)范的細(xì)節(jié)上肯定不一樣鸠窗,具體通信過程也不詳,是不是4ms也不好說胯究,關(guān)鍵是你有沒有搞清楚這背后經(jīng)歷了什么稍计。

微任務(wù)進(jìn)隊(duì)列

前面我們提到一個(gè)觀點(diǎn),執(zhí)行完一個(gè)Task后唐片,如果Microtask隊(duì)列不為空丙猬,會(huì)把Microtask隊(duì)列中所有的Microtask都取出來執(zhí)行涨颜。我認(rèn)為,Microtask不是在注冊(cè)時(shí)就進(jìn)入Microtask隊(duì)列茧球,因?yàn)镋vent Loop處理Microtask隊(duì)列時(shí)庭瑰,并不會(huì)判斷Microtask的狀態(tài)。反過來想抢埋,如果Microtask在注冊(cè)時(shí)就進(jìn)入Microtask隊(duì)列弹灭,就會(huì)存在Microtask還未變?yōu)?strong>runnable狀態(tài)就被執(zhí)行的情況,這顯然是不合理的揪垄。我的觀點(diǎn)是穷吮,Microtask在變?yōu)?strong>runnable狀態(tài)時(shí)才進(jìn)入Microtask隊(duì)列。

那么我們來分析下Microtask什么時(shí)候變成runnable狀態(tài)饥努,首先來看看Promise捡鱼。

var promise1 = new Promise((resolve, reject) => {
    resolve(1);
})
promise1.then(res => {
    console.log('promise1微任務(wù)被執(zhí)行了')
})

讀者們,我的第一個(gè)問題是酷愧,Promise的微任務(wù)什么時(shí)候被注冊(cè)驾诈?new Promise的時(shí)候?還是什么時(shí)候溶浴?不妨來猜一猜乍迄!

image

答案是.then被執(zhí)行的時(shí)候。(當(dāng)然士败,還有.catch的情況闯两,這里只是就這個(gè)例子說)。

那么Promise微任務(wù)的狀態(tài)什么時(shí)候變成runnable呢谅将?相信不少讀者已經(jīng)有了頭緒了漾狼,沒錯(cuò),就是Promise狀態(tài)發(fā)生轉(zhuǎn)移的時(shí)候戏自,在本例中也就是resolve(1)被執(zhí)行的時(shí)候邦投,Promise狀態(tài)由pending轉(zhuǎn)移為fulfilled。在resolve(1)執(zhí)行后擅笔,這個(gè)Promise微任務(wù)就進(jìn)入Microtask隊(duì)列了志衣,并且將在本次Event Loop中被執(zhí)行。

基于這個(gè)例子猛们,我們?cè)賮砑由钕码y度念脯。

var promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(1);
    }, 0);
});
promise1.then(res => {
    console.log('promise1微任務(wù)被執(zhí)行了');
});

在這個(gè)例子中,Promise微任務(wù)的注冊(cè)進(jìn)隊(duì)列并不在同一次Event Loop弯淘。怎么說呢绿店?在第一個(gè)Event Loop中,通過.then注冊(cè)了微任務(wù),但是我們可以發(fā)現(xiàn)假勿,new Promise時(shí)借嗽,執(zhí)行了一個(gè)setTimeout,這是相當(dāng)于注冊(cè)了一個(gè)宏任務(wù)转培。而resolve(1)必須在宏任務(wù)被執(zhí)行時(shí)才會(huì)執(zhí)行恶导。很明顯,兩者中間隔了至少一次Event Loop浸须。

如果能分析Promise微任務(wù)的過程惨寿,你自然就知道怎么分析ObserverMutation微任務(wù)的過程了,這里不再贅述删窒。

如果Microtask執(zhí)行時(shí)又創(chuàng)建了Microtask裂垦,怎么處理?

我們知道肌索,一次Event Loop最多只執(zhí)行一個(gè)runnable的Task蕉拢,但是會(huì)執(zhí)行Microtask隊(duì)列中的所有Microtask。如果在執(zhí)行Microtask時(shí)驶社,又創(chuàng)建了新的Microtask企量,這個(gè)新的Microtask是在下次Event Loop中被執(zhí)行嗎?答案是否定的亡电。微任務(wù)可以添加新的微任務(wù)到隊(duì)列中,并在下一個(gè)任務(wù)開始執(zhí)行之前且當(dāng)前Event Loop結(jié)束之前執(zhí)行完所有的微任務(wù)硅瞧。請(qǐng)注意不要遞歸地創(chuàng)建微任務(wù)份乒,否則會(huì)陷入死循環(huán)。

下面就是一個(gè)糟糕的示例腕唧。

// bad case
function recursionMicrotask() {
    Promise.resolve().then(() => {
        recursionMicrotask()
    })
}
recursionMicrotask();

請(qǐng)不要輕易嘗試或辖,否則頁面會(huì)卡死哦!(因?yàn)镸icrotask占著Main Thread不釋放枣接,瀏覽器渲染都沒辦法進(jìn)行了)

image

為什么要區(qū)分Task和Microtask颂暇?

這是一個(gè)非常重要的問題。為什么不在執(zhí)行完Task后但惶,直接進(jìn)行瀏覽器渲染這一步驟耳鸯,而要再加上執(zhí)行Microtask這一步呢?其實(shí)在前面的問題中已經(jīng)解答過了膀曾。一次Event Loop只會(huì)消費(fèi)一個(gè)宏任務(wù)县爬,而微任務(wù)隊(duì)列在被消費(fèi)時(shí)有“繼續(xù)上車”的機(jī)制,這就讓開發(fā)者有了更多的想象力添谊,對(duì)代碼的控制力會(huì)更強(qiáng)财喳。

做幾道題熱熱身?

在沖擊Promise/A+規(guī)范前,不妨先用幾個(gè)習(xí)題來測試下自己對(duì)Promise的理解程度耳高。

基本操作

function mutationCallback(mutationRecords, observer) {
    console.log('mt1')
}

const observer = new MutationObserver(mutationCallback)
observer.observe(document.body, { attributes: true })

Promise.resolve().then(() => {
    console.log('mt2')
    setTimeout(() => {
        console.log('t1')
    }, 0)
    document.body.setAttribute('test', "a")
}).then(() => {
    console.log('mt3')
})

setTimeout(() => {
    console.log('t2')
}, 0)

這道題就不分析了扎瓶,答案:mt2 mt1 mt3 t2 t1

瀏覽器不講武德?

Promise.resolve().then(() => {
    console.log(0);
    return Promise.resolve(4);
}).then((res) => {
    console.log(res)
})

Promise.resolve().then(() => {
    console.log(1);
}).then(() => {
    console.log(2);
}).then(() => {
    console.log(3);
}).then(() => {
    console.log(5);
}).then(() =>{
    console.log(6);
})

這道題據(jù)說是字節(jié)內(nèi)部流出的一道題泌枪,說實(shí)話我剛看到的時(shí)候也是一頭霧水栗弟。經(jīng)過我在Chrome測試,得到的答案確實(shí)很有規(guī)律工闺,就是:0 1 2 3 4 5 6乍赫。

先輸出0,再輸出1陆蟆,我還能理解雷厂,為什么輸出2和3后又突然跳到4呢,瀏覽器你不講武德暗蟆改鲫!

emm...我被戴上了痛苦面具!

image

那么這背后的執(zhí)行順序到底是怎樣的呢林束?仔細(xì)分析下像棘,你會(huì)發(fā)現(xiàn)還是有跡可循的。

老規(guī)矩壶冒,第一個(gè)問題缕题,這道題的代碼執(zhí)行過程中,產(chǎn)生了多少個(gè)微任務(wù)胖腾?可能很多人認(rèn)為是7個(gè)烟零,但實(shí)際上應(yīng)該是8個(gè)。

編號(hào) 注冊(cè)時(shí)機(jī) 異步回調(diào)
mt1 .then() console.log(0);return Promise.resolve(4);
mt2 .then(res) console.log(res)
mt3 .then() console.log(1);
mt4 .then() console.log(2);
mt5 .then() console.log(3);
mt6 .then() console.log(5);
mt7 .then() console.log(6);
mt8 return Promise.resolve(4)執(zhí)行并且execution context stack清空后咸作,隱式注冊(cè) 隱式回調(diào)(未體現(xiàn)在代碼中)锨阿,目的是讓mt2變成runnable狀態(tài)
  • 同步任務(wù)執(zhí)行,注冊(cè)mt1~mt7七個(gè)微任務(wù)记罚,此時(shí)execution context stack為空墅诡,并且mt1和mt3的狀態(tài)變?yōu)閞unnable。JS引擎安排mt1和mt3進(jìn)入Microtask隊(duì)列(通過HostEnqueuePromiseJob實(shí)現(xiàn))桐智。
  • Perform a microtask checkpoint末早,由于mt1和mt3是在同一次JS call中變?yōu)閞unnable的,所以mt1和mt3的回調(diào)先后進(jìn)入execution context stack執(zhí)行酵使。
  • mt1回調(diào)進(jìn)入execution context stack執(zhí)行荐吉,輸出0,返回Promise.resolve(4)口渔。mt1出隊(duì)列样屠。由于mt1回調(diào)返回的是一個(gè)狀態(tài)為fulfilled的Promise,所以之后JS引擎會(huì)安排一個(gè)job(job是ecma中的概念,等同于微任務(wù)的概念痪欲,這里先給它編號(hào)mt8)悦穿,其回調(diào)目的是讓mt2的狀態(tài)變?yōu)閒ulfilled(前提是當(dāng)前execution context stack is empty)。所以緊接著還是先執(zhí)行mt3的回調(diào)业踢。
  • mt3回調(diào)進(jìn)入execution context stack執(zhí)行栗柒,輸出1,mt4變?yōu)閞unnable狀態(tài)知举,execution context stack is empty瞬沦,mt3出隊(duì)列。
  • 由于此時(shí)mt4已經(jīng)是runnable狀態(tài)雇锡,JS引擎安排mt4進(jìn)隊(duì)列逛钻,接著JS引擎會(huì)安排mt8進(jìn)隊(duì)列。
  • 接著锰提,mt4回調(diào)進(jìn)入execution context stack執(zhí)行,輸出2边坤,mt5變?yōu)閞unnable谅年,mt4出隊(duì)列茧痒。JS引擎安排mt5進(jìn)入Microtask隊(duì)列踢故。
  • mt8回調(diào)執(zhí)行,目的是讓mt2變成runnable狀態(tài)殿较,mt8出隊(duì)列。mt2進(jìn)隊(duì)列淋纲。
  • mt5回調(diào)執(zhí)行洽瞬,輸出3伙窃,mt6變?yōu)閞unnable为障,mt5出隊(duì)列鳍怨。mt6進(jìn)隊(duì)列鞋喇。
  • mt2回調(diào)執(zhí)行侦香,輸出4罐韩,mt2出隊(duì)列。
  • mt6回調(diào)執(zhí)行缠沈,輸出5洲愤,mt7變?yōu)閞unnable柬赐,mt6出隊(duì)列肛宋。mt7進(jìn)隊(duì)列酝陈。
  • mt7回調(diào)執(zhí)行沉帮,輸出6穆壕,mt7出隊(duì)列喇勋。執(zhí)行完畢川背!總體來看渗常,輸出結(jié)果依次為:0 1 2 3 4 5 6皱碘。

對(duì)這塊執(zhí)行過程尚有疑問的朋友癌椿,可以先往下看看Promise/A+規(guī)范和ECMAScript262規(guī)范中關(guān)于Promise的約定踢俄,再回過頭來思考都办,也歡迎留言與我交流琳钉!

經(jīng)過我在Edge瀏覽器測試啦桌,結(jié)果是:0 1 2 4 3 5 6甫男“宀担可以看到笋庄,不同瀏覽器在實(shí)現(xiàn)Promise的主流程上是吻合的,但是在一些細(xì)枝末節(jié)上還有不一致的地方浩习。實(shí)際應(yīng)用中谱秽,我們只要注意規(guī)避這種問題即可疟赊。

實(shí)現(xiàn)Promise/A+

熱身完畢近哟,接下來就是直面大boss Promise/A+規(guī)范疯淫。Promise/A+規(guī)范列舉了大大小小三十余條細(xì)則熙掺,一眼看過去還是挺暈的币绩。

Promise/A+

仔細(xì)閱讀多遍規(guī)范之后缆镣,我有了一個(gè)基本認(rèn)識(shí)费就,要實(shí)現(xiàn)Promise/A+規(guī)范,關(guān)鍵是要理清其中幾個(gè)核心點(diǎn)眠蚂。

關(guān)系鏈路

本來寫了大幾千字有點(diǎn)覺得疲倦了逝慧,于是想著最后這部分就用文字講解快速收尾笛臣,但是最后這節(jié)寫到一半時(shí)沈堡,我覺得我寫不下去了诞丽,純文字的東西太干了刑赶,干得沒法吸收撞叨,這對(duì)那些對(duì)Promise掌握程度不夠的讀者來說是相當(dāng)不友好的谒所。所以劣领,我覺得還是先用一張圖來描述一下Promise的關(guān)系鏈路尖淘。

首先,Promise它是一個(gè)對(duì)象趁桃,而Promise/A+規(guī)范則是圍繞著Promise的原型方法.then()展開的卫病。

  • .then()的特殊性在于蟀苛,它會(huì)返回一個(gè)新的Promise實(shí)例帜平,在這種連續(xù)調(diào)用.then()的情況下,就會(huì)串起一個(gè)Promise鏈淑掌,這與原型鏈又有一些相似之處抛腕〉5校“恬不知恥”地再推薦一篇「思維導(dǎo)圖學(xué)前端 」6k字一文搞懂Javascript對(duì)象,原型刹悴,繼承土匀,哈哈哈就轧。
  • 另一個(gè)靈活的地方在于,p1.then(onFulfilled, onRejected)返回的新Promise實(shí)例p2乎莉,其狀態(tài)轉(zhuǎn)移的發(fā)生是在p1的狀態(tài)轉(zhuǎn)移發(fā)生之后(這里的之后指的是異步的之后)惋啃。并且肥橙,p2的狀態(tài)轉(zhuǎn)移為Fulfilled還是Rejected,這一點(diǎn)取決于onFulfilledonRejected的返回值椭坚,這里有一個(gè)較為復(fù)雜的分析過程善茎,也就是后面所述的Promise Resolution Procedure算法烁焙。

我這里畫了一個(gè)簡單的時(shí)序圖骄蝇,畫圖水平很差九火,只是為了讓讀者們先有個(gè)基本印象。

image

其中還有很多細(xì)節(jié)是沒提到的(因?yàn)榧?xì)節(jié)真的太多了虑鼎,全部畫出來就相當(dāng)復(fù)雜,具體過程請(qǐng)看我文末附的源碼)媒楼。

nextTick

看了前面內(nèi)容划址,相信大家都有一個(gè)概念,微任務(wù)是一個(gè)異步任務(wù)世澜,而我們要實(shí)現(xiàn)Promise的整套異步機(jī)制寥裂,必然要具備模擬微任務(wù)異步回調(diào)的能力。在規(guī)范中也提到了這么一條信息:

This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick.

我這里選擇的是用微任務(wù)來實(shí)現(xiàn)異步回調(diào)诺舔,如果用宏任務(wù)來實(shí)現(xiàn)異步回調(diào)许昨,那么在Promise微任務(wù)隊(duì)列執(zhí)行過程中就可能會(huì)穿插宏任務(wù)车要,這就不太符合微任務(wù)隊(duì)列的調(diào)度邏輯了类垫。這里還對(duì)Node環(huán)境和瀏覽器環(huán)境做了兼容悉患,Node環(huán)境中可以使用process.nextTick回調(diào)來模擬微任務(wù)的執(zhí)行售躁,而在瀏覽器環(huán)境中我們可以選擇MutationObserver回窘。

function nextTick(callback) {
  if (typeof process !== 'undefined' && typeof process.nextTick === 'function') {
    process.nextTick(callback)
  } else {
    const observer = new MutationObserver(callback)
    const textNode = document.createTextNode('1')
    observer.observe(textNode, {
      characterData: true
    })
    textNode.data = '2'
  }
}

狀態(tài)轉(zhuǎn)移

  • Promise實(shí)例一共有三種狀態(tài),分別是Pending, Fulfilled, Rejected酒觅,初始狀態(tài)是Pending。

    const PROMISE_STATES = {
      PENDING: 'pending',
      FULFILLED: 'fulfilled',
      REJECTED: 'rejected'
    }
    
    class MyPromise {
      constructor(executor) {
        this.state = PROMISE_STATES.PENDING;
      }
      // ...其他代碼
    }
    
  • 一旦Promise的狀態(tài)發(fā)生轉(zhuǎn)移颜凯,就不可再轉(zhuǎn)移為其他狀態(tài)装获。

    /**
     * 封裝Promise狀態(tài)轉(zhuǎn)移的過程
     * @param {MyPromise} promise 發(fā)生狀態(tài)轉(zhuǎn)移的Promise實(shí)例
     * @param {*} targetState 目標(biāo)狀態(tài)
     * @param {*} value 伴隨狀態(tài)轉(zhuǎn)移的值,可能是fulfilled的值精肃,也可能是rejected的原因
     */
    function transition(promise, targetState, value) {
      if (promise.state === PROMISE_STATES.PENDING && targetState !== PROMISE_STATES.PENDING) {
        // 2.1: state只能由pending轉(zhuǎn)為其他態(tài)筐眷,狀態(tài)轉(zhuǎn)移后匀谣,state和value的值不再變化
        Object.defineProperty(promise, 'state', {
          configurable: false,
          writable: false,
          enumerable: true,
          value: targetState
        })
        // ...其他代碼
      }
    }
    
  • 觸發(fā)狀態(tài)轉(zhuǎn)移是靠調(diào)用resolve()reject()實(shí)現(xiàn)的。當(dāng)resolve()被調(diào)用時(shí),當(dāng)前Promise也不一定會(huì)立即變?yōu)镕ulfilled狀態(tài)垫毙,因?yàn)閭魅?code>resolve(value)方法的value有可能也是一個(gè)Promise综芥,這個(gè)時(shí)候,當(dāng)前Promise必須追蹤傳入的這個(gè)Promise的狀態(tài)消请,整個(gè)確定Promise狀態(tài)的過程是通過Promise Resolution Procedure算法實(shí)現(xiàn)的,具體細(xì)節(jié)封裝到了下面代碼中的resolvePromiseWithValue函數(shù)中缸逃。當(dāng)reject()被調(diào)用時(shí),當(dāng)前Promise的狀態(tài)就是確定的昭殉,一定是Rejected蹂风,此時(shí)可以通過transition函數(shù)(封裝了狀態(tài)轉(zhuǎn)移的細(xì)節(jié))將Promise的狀態(tài)進(jìn)行轉(zhuǎn)移,并執(zhí)行后續(xù)動(dòng)作撵渡。

    // resolve的執(zhí)行姥闭,是一個(gè)觸發(fā)信號(hào)靠欢,基于此進(jìn)行下一步的操作
    function resolve(value) {
      resolvePromiseWithValue(this, value)
    }
    // reject的執(zhí)行骡澈,是狀態(tài)可以變?yōu)镽ejected的信號(hào)
    function reject(reason) {
      transition(this, PROMISE_STATES.REJECTED, reason)
    }
    
    class MyPromise {
      constructor(executor) {
        this.state = PROMISE_STATES.PENDING;
        this.fulfillQueue = [];
        this.rejectQueue = [];
        // 構(gòu)造Promise實(shí)例后,立刻調(diào)用executor
        executor(resolve.bind(this), reject.bind(this))
      }
    }
    

鏈?zhǔn)阶粉?/h2>

假設(shè)現(xiàn)在有一個(gè)Promise實(shí)例护锤,我們稱之為p1烙懦。由于promise1.then(onFulfilled, onRejected)會(huì)返回一個(gè)新的Promise(我們稱之為p2),與此同時(shí)赤炒,也會(huì)注冊(cè)一個(gè)微任務(wù)mt1氯析,這個(gè)新的p2會(huì)追蹤其關(guān)聯(lián)的p1的狀態(tài)變化。

當(dāng)p1的狀態(tài)發(fā)生轉(zhuǎn)移時(shí)莺褒,微任務(wù)mt1回調(diào)會(huì)在接下來被執(zhí)行掩缓,如果狀態(tài)是Fulfilled遵岩,則onFulfilled會(huì)被執(zhí)行你辣,否則onRejected會(huì)被執(zhí)行。微任務(wù)mt回調(diào)1執(zhí)行的結(jié)果將作為決定p2狀態(tài)的依據(jù)。以下是Fulfilled情況下的部分關(guān)鍵代碼绢记,其中promise指的是p1扁达,而chainedPromise指的是p2。

// 回調(diào)應(yīng)異步執(zhí)行蠢熄,所以用到了nextTick
nextTick(() => {
  // then可能會(huì)被調(diào)用多次跪解,所以異步回調(diào)應(yīng)該用數(shù)組來維護(hù)
  promise.fulfillQueue.forEach(({ handler, chainedPromise }) => {
    try {
      if (typeof handler === 'function') {
        const adoptedValue = handler(value)
        // 異步回調(diào)返回的值將決定衍生的Promise的狀態(tài)
        resolvePromiseWithValue(chainedPromise, adoptedValue)
      } else {
        // 存在調(diào)用了then,但是沒傳回調(diào)作為參數(shù)的可能签孔,此時(shí)衍生的Promise的狀態(tài)直接采納其關(guān)聯(lián)的Promise的狀態(tài)叉讥。
        transition(chainedPromise, PROMISE_STATES.FULFILLED, promise.value)
      }
    } catch (error) {
      // 如果回調(diào)拋出了異常,此時(shí)直接將衍生的Promise的狀態(tài)轉(zhuǎn)移為rejected饥追,并用異常error作為reason
      transition(chainedPromise, PROMISE_STATES.REJECTED, error)
    }
  })
  // 最后清空該P(yáng)romise關(guān)聯(lián)的回調(diào)隊(duì)列
  promise.fulfillQueue = [];
})

Promise Resolution Procedure算法

Promise Resolution Procedure算法是一種抽象的執(zhí)行過程图仓,它的語法形式是[[Resolve]](promise, x "[Resolve]"),接受的參數(shù)是一個(gè)Promise實(shí)例和一個(gè)值x但绕,通過值x的可能性救崔,來決定這個(gè)Promise實(shí)例的狀態(tài)走向。如果直接硬看規(guī)范捏顺,會(huì)有點(diǎn)吃力六孵,這里直接說人話解釋一些細(xì)節(jié)。

2.3.1

如果promise和值x引用同一個(gè)對(duì)象幅骄,應(yīng)該直接將promise的狀態(tài)置為Rejected劫窒,并且用一個(gè)TypeError作為reject的原因。

If promise and x refer to the same object, reject promise with a TypeError as the reason.

【說人話】舉個(gè)例子拆座,老板說只要今年業(yè)績超過10億主巍,業(yè)績就超過10億。這顯然是個(gè)病句挪凑,你不能拿預(yù)期本身作為條件孕索。正確的玩法是,老板說只要今年業(yè)績超過10億岖赋,就發(fā)1000萬獎(jiǎng)金(嘿嘿檬果,這種事期待一下就好了)。

代碼實(shí)現(xiàn):

if (promise === x) {
    // 2.3.1 由于Promise采納狀態(tài)的機(jī)制唐断,這里必須進(jìn)行全等判斷选脊,防止出現(xiàn)死循環(huán)
    transition(promise, PROMISE_STATES.REJECTED, new TypeError('promise and x cannot refer to a same object.'))
}

2.3.2

如果x是一個(gè)Promise實(shí)例,promise應(yīng)該采納x的狀態(tài)脸甘。

2.3.2 If x is a promise, adopt its state [3.4]:

2.3.2.1 If x is pending, promise must remain pending until x is fulfilled or rejected.

2.3.2.2 If/when x is fulfilled, fulfill promise with the same value.

2.3.2.3 If/when x is rejected, reject promise with the same reason.

【說人話】小王問領(lǐng)導(dǎo):“今年會(huì)發(fā)年終獎(jiǎng)嗎恳啥?發(fā)多少?”領(lǐng)導(dǎo)聽了心里想丹诀,“這個(gè)事我之前也在打聽钝的,不過還沒定下來翁垂,得看老板的意思建椰÷菥洌”,于是領(lǐng)導(dǎo)對(duì)小王說:“會(huì)發(fā)的困食,不過要等消息碗脊!”啼肩。

注意,這個(gè)時(shí)候衙伶,領(lǐng)導(dǎo)對(duì)小王許下了承諾祈坠,但是這個(gè)承諾p2的狀態(tài)還是pending,需要看老板給的承諾p1的狀態(tài)矢劲。

  • 可能性1:過了幾天赦拘,老板對(duì)領(lǐng)導(dǎo)說:“今年業(yè)務(wù)做得可以,年終獎(jiǎng)發(fā)1000萬”芬沉。這里相當(dāng)于p1已經(jīng)是fulfilled狀態(tài)了躺同,value是1000萬。領(lǐng)導(dǎo)拿了這個(gè)準(zhǔn)信了丸逸,自然可以跟小王兌現(xiàn)承諾p2了笋籽,于是對(duì)小王說:“年終獎(jiǎng)可以下來了,是1000萬椭员!”。這時(shí)笛园,承諾p2的狀態(tài)就是fulfilled了隘击,value也是1000萬。小王這個(gè)時(shí)候就“別墅靠海”了研铆。
image
  • 可能性2:過了幾天埋同,老板有點(diǎn)發(fā)愁,對(duì)領(lǐng)導(dǎo)說:“今年業(yè)績不太行啊棵红,年終獎(jiǎng)就不發(fā)了吧凶赁,明年,咱們明年多發(fā)點(diǎn)逆甜∈蓿”顯然,這里p1就是rejected了交煞,領(lǐng)導(dǎo)一看這情況不對(duì)啊咏窿,但也沒辦法,只能對(duì)小王說:“小王啊素征,今年公司情況特殊集嵌,年終獎(jiǎng)就不發(fā)了萝挤。”這p2也隨之rejected了根欧,小王內(nèi)心有點(diǎn)炸裂......
image

注意怜珍,Promise A/+規(guī)范2.3.2小節(jié)這里有兩個(gè)大的方向,一個(gè)是x的狀態(tài)未定凤粗,一個(gè)是x的狀態(tài)已定酥泛。在代碼實(shí)現(xiàn)上,這里有個(gè)技巧侈沪,對(duì)于狀態(tài)未定的情況揭璃,必須用訂閱的方式來實(shí)現(xiàn),而.then就是訂閱的絕佳途徑亭罪。

else if (isPromise(x)) {
    // 2.3.2 如果x是一個(gè)Promise實(shí)例瘦馍,則追蹤并采納其狀態(tài)
    if (x.state !== PROMISE_STATES.PENDING) {
      // 假設(shè)x的狀態(tài)已經(jīng)發(fā)生轉(zhuǎn)移,則直接采納其狀態(tài)
      transition(promise, x.state, x.state === PROMISE_STATES.FULFILLED ? x.value : x.reason)
    } else {
      // 假設(shè)x的狀態(tài)還是pending应役,則只需等待x狀態(tài)確定后再進(jìn)行promise的狀態(tài)轉(zhuǎn)移
      // 而x的狀態(tài)轉(zhuǎn)移結(jié)果是不定的情组,所以兩種情況我們都需要進(jìn)行訂閱
      // 這里用一個(gè).then很巧妙地完成了訂閱動(dòng)作
      x.then(value => {
        // x狀態(tài)轉(zhuǎn)移為fulfilled,由于callback傳過來的value是不確定的類型箩祥,所以需要繼續(xù)應(yīng)用Promise Resolution Procedure算法
        resolvePromiseWithValue(promise, value, thenableValues)
      }, reason => {
        // x狀態(tài)轉(zhuǎn)移為rejected
        transition(promise, PROMISE_STATES.REJECTED, reason)
      })
    }
}

多的細(xì)節(jié)咱這篇文章就不一一分析了院崇,寫著寫著快1萬字了,就先結(jié)束掉吧袍祖,感興趣的讀者可以直接打開源碼看(往下看)底瓣。

這是跑測試用例的效果圖,可以看到蕉陋,872個(gè)case是全部通過的捐凭。

image

完整代碼

這里直接給出我寫的Promise/A+規(guī)范的Javascript實(shí)現(xiàn),供大家參考凳鬓。后面如果有時(shí)間茁肠,會(huì)考慮詳細(xì)分析下。

缺陷

我這個(gè)版本的Promise/A+規(guī)范實(shí)現(xiàn)缩举,不具備檢測execution context stack為空的能力垦梆,所以在細(xì)節(jié)上會(huì)有一點(diǎn)問題(execution context stack還未清空就插入了微任務(wù)),無法適配上面那道「瀏覽器不講武德仅孩?」的題目所述場景托猩。

方法論

不管是手寫實(shí)現(xiàn)Promise/A+規(guī)范,還是實(shí)現(xiàn)其他Native Code辽慕,其本質(zhì)上繞不開以下幾點(diǎn):

  • 準(zhǔn)確理解Native Code實(shí)現(xiàn)的能力站刑,就像你理解一個(gè)需求要實(shí)現(xiàn)哪些功能點(diǎn)一樣,并確定實(shí)現(xiàn)上的優(yōu)先級(jí)鼻百。
  • 針對(duì)每個(gè)功能點(diǎn)或者功能描述绞旅,逐一用代碼實(shí)現(xiàn)摆尝,優(yōu)先打通主干流程。
  • 設(shè)計(jì)足夠豐富的測試用例因悲,回歸測試堕汞,不斷迭代,保證場景的覆蓋率晃琳,最終打造一段優(yōu)質(zhì)的代碼讯检。

總結(jié)

看到結(jié)尾,相信大家也累了卫旱,感謝各位讀者的閱讀人灼!希望本文對(duì)宏任務(wù)和微任務(wù)的解讀能給各位讀者帶來一點(diǎn)啟發(fā)。Promise/A+規(guī)范總體來說還是比較晦澀難懂的顾翼,這對(duì)新手來說是不太友好的投放,因此我建議有一定程度的Promise實(shí)際使用經(jīng)驗(yàn)后再深入學(xué)習(xí)Promise/A+規(guī)范。通過學(xué)習(xí)和理解Promise/A+規(guī)范的實(shí)現(xiàn)機(jī)制适贸,你會(huì)更懂Promise的一些內(nèi)部細(xì)節(jié)灸芳,對(duì)于設(shè)計(jì)一些復(fù)雜的異步過程會(huì)有極大的幫助,再不濟(jì)也能提升你的異步調(diào)試和排錯(cuò)能力拜姿。

這里還有一些規(guī)范和文章可以參考:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末烙样,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蕊肥,更是在濱河造成了極大的恐慌谒获,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件壁却,死亡現(xiàn)場離奇詭異究反,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)儒洛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來狼速,“玉大人琅锻,你說我怎么就攤上這事∠蚝” “怎么了恼蓬?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長僵芹。 經(jīng)常有香客問我处硬,道長,這世上最難降的妖魔是什么拇派? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任荷辕,我火速辦了婚禮凿跳,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘疮方。我一直安慰自己控嗜,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布骡显。 她就那樣靜靜地躺著疆栏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪惫谤。 梳的紋絲不亂的頭發(fā)上壁顶,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音溜歪,去河邊找鬼若专。 笑死,一個(gè)胖子當(dāng)著我的面吹牛痹愚,可吹牛的內(nèi)容都是我干的富岳。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼拯腮,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼窖式!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起动壤,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤萝喘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后琼懊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體阁簸,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年哼丈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了启妹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡醉旦,死狀恐怖饶米,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情车胡,我是刑警寧澤檬输,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站匈棘,受9級(jí)特大地震影響丧慈,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜主卫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一逃默、第九天 我趴在偏房一處隱蔽的房頂上張望鹃愤。 院中可真熱鬧,春花似錦笑旺、人聲如沸昼浦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽关噪。三九已至,卻和暖如春乌妙,著一層夾襖步出監(jiān)牢的瞬間使兔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來泰國打工藤韵, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留虐沥,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓泽艘,卻偏偏與公主長得像欲险,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子匹涮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

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