偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

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

開發(fā) 前端
我一度以為自己很懂Promise,直到前段時間嘗試去實現(xiàn)Promise/A+規(guī)范時,才發(fā)現(xiàn)自己對Promise的理解還過于淺薄。

[[386242]]

本文轉(zhuǎn)載自微信公眾號「前端司南」,作者Tusi。轉(zhuǎn)載本文請聯(lián)系前端司南公眾號。   

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

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

  • github倉庫:promises-aplus-robin[1](順手點個star就更好了)
  • 源碼[2]
  • 源碼注釋版[3]

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

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

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

許下承諾

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

有些餅馬上就兌現(xiàn)了,比如口頭褒獎,因為它本身沒有給企業(yè)帶來什么成本;有些餅卻關(guān)乎企業(yè)實際利益,它們可能未來可期,也可能猴年馬月,或是無疾而終,又或者直接宣告畫餅失敗。

畫餅這個動作,于Javascript而言,就是創(chuàng)建一個Promise實例:

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

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

  • pending: 餅已畫好,坐等實現(xiàn)。
  • fulfilled: 餅真的實現(xiàn)了,走上人生巔峰。
  • rejected: 不好意思,畫餅失敗,emmm...

訂閱承諾

有人畫餅,自然有人接餅。所謂“接餅”,就是對于這張餅的可能性做下設(shè)想。如果餅真的實現(xiàn)了,鄙人將別墅靠海;如果餅失敗了,本打工仔以淚洗面。

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

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

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

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

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

  1. bossBing.then
  2.   success => { 
  3.     // leader接過boss的餅,繼續(xù)往下面發(fā)餅 
  4.     return leaderBing 
  5.   } 
  6. ).then
  7.   success => { 
  8.     console.log('leader畫的餅真的實現(xiàn)了,別墅靠海'
  9.   }, 
  10.   fail => { 
  11.     console.log('leader畫的餅炸了,以淚洗面...'
  12.   } 

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

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

初識Promise

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

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

  1. function isObject(val) { 
  2.   return Object.prototype.toString.call(val) === '[object Object]'
  3.  
  4. function serialize(params) { 
  5.     let result = ''
  6.     if (isObject(params)) { 
  7.       Object.keys(params).forEach((key) => { 
  8.         let val = encodeURIComponent(params[key]); 
  9.         result += `${key}=${val}&`; 
  10.       }); 
  11.     } 
  12.     return result; 
  13.  
  14. const defaultHeaders = { 
  15.   "Content-Type""application/x-www-form-urlencoded" 
  16.  
  17. // ajax簡單封裝 
  18. function request(options) { 
  19.   return new Promise((resolve, reject) => { 
  20.     const { method, url, params, headers } = options 
  21.     const xhr = new XMLHttpRequest(); 
  22.     if (method === 'GET' || method === 'DELETE') { 
  23.       // GET和DELETE一般用querystring傳參 
  24.       const requestURL = url + '?' + serialize(params) 
  25.       xhr.open(method, requestURL, true); 
  26.     } else { 
  27.       xhr.open(method, url, true); 
  28.     } 
  29.     // 設(shè)置請求頭 
  30.     const mergedHeaders = Object.assign({}, defaultHeaders, headers) 
  31.     Object.keys(mergedHeaders).forEach(key => { 
  32.       xhr.setRequestHeader(key, mergedHeaders[key]); 
  33.     }) 
  34.     // 狀態(tài)監(jiān)聽 
  35.     xhr.onreadystatechange = function () { 
  36.       if (xhr.readyState === 4) { 
  37.         if (xhr.status === 200) { 
  38.           resolve(xhr.response) 
  39.         } else { 
  40.           reject(xhr.status) 
  41.         } 
  42.       } 
  43.     } 
  44.     xhr.onerror = function(e) { 
  45.       reject(e) 
  46.     } 
  47.     // 處理body數(shù)據(jù),發(fā)送請求 
  48.     const data = method === 'POST' || method === 'PUT' ? serialize(params) : null 
  49.     xhr.send(data); 
  50.   }) 
  51.  
  52. const options = { 
  53.   method: 'GET'
  54.   url: '/user/page'
  55.   params: { 
  56.     pageNo: 1, 
  57.     pageSize: 10 
  58.   } 
  59. // 通過Promise的形式調(diào)用接口 
  60. request(options).then(res => { 
  61.   // 請求成功 
  62. }, fail => { 
  63.   // 請求失敗 
  64. }) 

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

  • 封裝一個函數(shù),將包含異步過程的代碼包裹在構(gòu)造Promise的executor中,所封裝的函數(shù)最后需要return這個Promise實例。
  • 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時,請求成功(執(zhí)行resolve()),否則請求失敗(執(zhí)行reject())。

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

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

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

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

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

  1. var button = document.createElement('button'); 
  2. button.innerText = '新增輸入框' 
  3. document.body.append(button) 
  4.  
  5. button.onmousedown = function() { 
  6.   var input = document.createElement('input'); 
  7.   document.body.appendChild(input); 
  8.   setTimeout(function() { 
  9.     input.focus(); 
  10.   }, 0) 

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

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

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

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

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

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

為了保證界面交互的連貫性和平滑度,Main Thread中,Javascript的執(zhí)行和頁面的渲染會交替執(zhí)行(出于性能考慮,某些情況下,瀏覽器判斷不需要執(zhí)行界面渲染,會略過渲染的步驟)。目前大多數(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幀的末尾,如果還有時間富余,就會調(diào)用requestIdleCallback。注意不要在requestIdleCallback中修改DOM,或者讀取布局信息導(dǎo)致觸發(fā)Forced Synchronized Layout,否則會引發(fā)性能和體驗問題。具體見Using requestIdleCallback[5]。

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

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

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

用一個簡單的例子來感受下:

  1. var a = 1; 
  2. console.log('a:', a) 
  3. var b = 2; 
  4. console.log('b:', b) 
  5. setTimeout(function task1(){ 
  6.   console.log('task1:', 5) 
  7.   Promise.resolve(6).then(function microtask2(res){ 
  8.     console.log('microtask2:', res) 
  9.   }) 
  10. }, 0) 
  11. Promise.resolve(4).then(function microtask1(res){ 
  12.   console.log('microtask1:', res) 
  13. }) 
  14. var b = 3; 
  15. console.log('c:', c) 

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

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

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

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)常被我們用來做動畫效果,因為其回調(diào)函數(shù)執(zhí)行頻率與瀏覽器屏幕刷新頻率保持一致,也就是我們通常說的它能實現(xiàn)60FPS的效果。在rAF被大范圍應(yīng)用前,我們經(jīng)常使用setTimeout來處理動畫。但是setTimeout在主線程繁忙時,不一定能及時地被調(diào)度,從而出現(xiàn)卡頓現(xiàn)象。

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

我們不妨自己問問自己,rAF是宏任務(wù)嗎?我想了一下,顯然不是,rAF可以用來代替定時器動畫,怎么能和定時器任務(wù)一樣被Event Loop調(diào)度呢?

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

  1. function recursionRaf() { 
  2.  requestAnimationFrame(() => { 
  3.         console.log('raf回調(diào)'
  4.         recursionRaf() 
  5.     }) 
  6. recursionRaf(); 

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

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

所以,rAF的任務(wù)級別是很高的,擁有單獨的隊列維護(hù)。在瀏覽器1幀的周期內(nèi),rAF與Javascript執(zhí)行,瀏覽器重繪是同一個Level的。(其實,大家在前面那張「解剖1幀」的圖中也能看出來了。)

Task和Microtask各有1個隊列?

最初,我認(rèn)為既然瀏覽器區(qū)分了Task和Microtask,那就只要各自安排一個隊列存儲任務(wù)即可。事實上,Task根據(jù)task source的不同,安排了獨立的隊列。比如Dom事件屬于Task,但是Dom事件有很多種類型,為了方便user agent細(xì)分Task并精細(xì)化地安排各種不同類型Task的處理優(yōu)先級,甚至做一些優(yōu)化工作,必須有一個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的消費機(jī)制是怎樣的?

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

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

說再多,都不如一張圖直觀,請看!

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

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

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

對于Task而言,任務(wù)注冊時就會進(jìn)入隊列,只是任務(wù)的狀態(tài)還不是runnable,不具備被Event Loop撈起的條件。

我們先用Dom事件為例舉個例子。

  1. document.body.addEventListener('click'function(e) { 
  2.     console.log('被點擊了', e) 
  3. }) 

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

這里再舉個例子,順便解釋下為什么setTimeout 0會有延遲。

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

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

可以看到,經(jīng)過這樣一個線程間通信的過程,即便是延時0毫秒的定時器,其回調(diào)也并不是在真正意義上的0毫秒之后執(zhí)行,因為通信過程就需要耗費時間。網(wǎng)上有個觀點說setTimeout 0的響應(yīng)時間最少是4ms,其實也是有依據(jù)的,不過也是有條件的。

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

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

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

前面我們提到一個觀點,執(zhí)行完一個Task后,如果Microtask隊列不為空,會把Microtask隊列中所有的Microtask都取出來執(zhí)行。我認(rèn)為,Microtask不是在注冊時就進(jìn)入Microtask隊列,因為Event Loop處理Microtask隊列時,并不會判斷Microtask的狀態(tài)。反過來想,如果Microtask在注冊時就進(jìn)入Microtask隊列,就會存在Microtask還未變?yōu)閞unnable狀態(tài)就被執(zhí)行的情況,這顯然是不合理的。我的觀點是,Microtask在變?yōu)閞unnable狀態(tài)時才進(jìn)入Microtask隊列。

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

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

讀者們,我的第一個問題是,Promise的微任務(wù)什么時候被注冊?new Promise的時候?還是什么時候?不妨來猜一猜!

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

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

基于這個例子,我們再來加深下難度。

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

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

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

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

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

下面就是一個糟糕的示例。

  1. // bad case 
  2. function recursionMicrotask() { 
  3.  Promise.resolve().then(() => { 
  4.   recursionMicrotask() 
  5.  }) 
  6. recursionMicrotask(); 

請不要輕易嘗試,否則頁面會卡死哦!(因為Microtask占著Main Thread不釋放,瀏覽器渲染都沒辦法進(jìn)行了)

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

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

做幾道題熱熱身?

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

基本操作

  1. function mutationCallback(mutationRecords, observer) { 
  2.     console.log('mt1'
  3.  
  4. const observer = new MutationObserver(mutationCallback) 
  5. observer.observe(document.body, { attributes: true }) 
  6.  
  7. Promise.resolve().then(() => { 
  8.     console.log('mt2'
  9.     setTimeout(() => { 
  10.         console.log('t1'
  11.     }, 0) 
  12.     document.body.setAttribute('test'"a"
  13. }).then(() => { 
  14.     console.log('mt3'
  15. }) 
  16.  
  17. setTimeout(() => { 
  18.     console.log('t2'
  19. }, 0) 

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

瀏覽器不講武德?

  1. Promise.resolve().then(() => { 
  2.     console.log(0); 
  3.     return Promise.resolve(4); 
  4. }).then((res) => { 
  5.     console.log(res) 
  6. }) 
  7.  
  8. Promise.resolve().then(() => { 
  9.     console.log(1); 
  10. }).then(() => { 
  11.     console.log(2); 
  12. }).then(() => { 
  13.     console.log(3); 
  14. }).then(() => { 
  15.     console.log(5); 
  16. }).then(() =>{ 
  17.     console.log(6); 
  18. }) 

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

先輸出0,再輸出1,我還能理解,為什么輸出2和3后又突然跳到4呢,瀏覽器你不講武德啊!

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

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

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

編號 注冊時機(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清空后,隱式注冊 隱式回調(diào)(未體現(xiàn)在代碼中),目的是讓mt2變成runnable狀態(tài)
  • 同步任務(wù)執(zhí)行,注冊mt1~mt7七個微任務(wù),此時execution context stack為空,并且mt1和mt3的狀態(tài)變?yōu)閞unnable。JS引擎安排mt1和mt3進(jìn)入Microtask隊列(通過HostEnqueuePromiseJob實現(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出隊列。由于mt1回調(diào)返回的是一個狀態(tài)為fulfilled的Promise,所以之后JS引擎會安排一個job(job是ecma中的概念,等同于微任務(wù)的概念,這里先給它編號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出隊列。
  • 由于此時mt4已經(jīng)是runnable狀態(tài),JS引擎安排mt4進(jìn)隊列,接著JS引擎會安排mt8進(jìn)隊列。
  • 接著,mt4回調(diào)進(jìn)入execution context stack執(zhí)行,輸出2,mt5變?yōu)閞unnable,mt4出隊列。JS引擎安排mt5進(jìn)入Microtask隊列。
  • mt8回調(diào)執(zhí)行,目的是讓mt2變成runnable狀態(tài),mt8出隊列。mt2進(jìn)隊列。
  • mt5回調(diào)執(zhí)行,輸出3,mt6變?yōu)閞unnable,mt5出隊列。mt6進(jìn)隊列。
  • mt2回調(diào)執(zhí)行,輸出4,mt4出隊列。
  • mt6回調(diào)執(zhí)行,輸出5,mt7變?yōu)閞unnable,mt6出隊列。mt7進(jìn)隊列。
  • mt7回調(diào)執(zhí)行,輸出6,mt7出隊列。執(zhí)行完畢!總體來看,輸出結(jié)果依次為:0 1 2 3 4 5 6。

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

經(jīng)過我在Edge瀏覽器測試,結(jié)果是:0 1 2 4 3 5 6??梢钥吹剑煌瑸g覽器在實現(xiàn)Promise的主流程上是吻合的,但是在一些細(xì)枝末節(jié)上還有不一致的地方。實際應(yīng)用中,我們只要注意規(guī)避這種問題即可。

實現(xiàn)Promise/A+

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

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

關(guān)系鏈路

本來寫了大幾千字有點覺得疲倦了,于是想著最后這部分就用文字講解快速收尾,但是最后這節(jié)寫到一半時,我覺得我寫不下去了,純文字的東西太干了,干得沒法吸收,這對那些對Promise掌握程度不夠的讀者來說是相當(dāng)不友好的。所以,我覺得還是先用一張圖來描述一下Promise的關(guān)系鏈路。

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

  • .then()的特殊性在于,它會返回一個新的Promise實例,在這種連續(xù)調(diào)用.then()的情況下,就會串起一個Promise鏈,這與原型鏈又有一些相似之處。“恬不知恥”地再推薦一篇「思維導(dǎo)圖學(xué)前端 」6k字一文搞懂Javascript對象,原型,繼承[7],哈哈哈。
  • 另一個靈活的地方在于,p1.then(onFulfilled, onRejected)返回的新Promise實例p2,其狀態(tài)轉(zhuǎn)移的發(fā)生是在p1的狀態(tài)轉(zhuǎn)移發(fā)生之后(這里的之后指的是異步的之后)。并且,p2的狀態(tài)轉(zhuǎn)移為Fulfilled還是Rejected,這一點取決于onFulfilled或onRejected的返回值,這里有一個較為復(fù)雜的分析過程,也就是后面所述的Promise Resolution Procedure算法。

我這里畫了一個簡單的時序圖,畫圖水平很差,只是為了讓讀者們先有個基本印象。

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

nextTick

看了前面內(nèi)容,相信大家都有一個概念,微任務(wù)是一個異步任務(wù),而我們要實現(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ù)來實現(xiàn)異步回調(diào),如果用宏任務(wù)來實現(xiàn)異步回調(diào),那么在Promise微任務(wù)隊列執(zhí)行過程中就可能會穿插宏任務(wù),這就不太符合微任務(wù)隊列的調(diào)度邏輯了。這里還對Node環(huán)境和瀏覽器環(huán)境做了兼容,Node環(huán)境中可以使用process.nextTick回調(diào)來模擬微任務(wù)的執(zhí)行,而在瀏覽器環(huán)境中我們可以選擇MutationObserver。

  1. function nextTick(callback) { 
  2.   if (typeof process !== 'undefined' && typeof process.nextTick === 'function') { 
  3.     process.nextTick(callback) 
  4.   } else { 
  5.     const observer = new MutationObserver(callback) 
  6.     const textNode = document.createTextNode('1'
  7.     observer.observe(textNode, { 
  8.       characterData: true 
  9.     }) 
  10.     textNode.data = '2' 
  11.   } 

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

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

  1. const PROMISE_STATES = { 
  2.   PENDING: 'pending'
  3.   FULFILLED: 'fulfilled'
  4.   REJECTED: 'rejected' 
  5.  
  6. class MyPromise { 
  7.   constructor(executor) { 
  8.     this.state = PROMISE_STATES.PENDING; 
  9.   } 
  10.   // ...其他代碼 

一旦Promise的狀態(tài)發(fā)生轉(zhuǎn)移,就不可再轉(zhuǎn)移為其他狀態(tài)。

  1. /** 
  2.  * 封裝Promise狀態(tài)轉(zhuǎn)移的過程 
  3.  * @param {MyPromise} promise 發(fā)生狀態(tài)轉(zhuǎn)移的Promise實例 
  4.  * @param {*} targetState 目標(biāo)狀態(tài) 
  5.  * @param {*} value 伴隨狀態(tài)轉(zhuǎn)移的值,可能是fulfilled的值,也可能是rejected的原因 
  6.  */ 
  7. function transition(promise, targetState, value) { 
  8.   if (promise.state === PROMISE_STATES.PENDING && targetState !== PROMISE_STATES.PENDING) { 
  9.     // 2.1: state只能由pending轉(zhuǎn)為其他態(tài),狀態(tài)轉(zhuǎn)移后,state和value的值不再變化 
  10.     Object.defineProperty(promise, 'state', { 
  11.       configurable: false
  12.       writable: false
  13.       enumerable: true
  14.       value: targetState 
  15.     }) 
  16.     // ...其他代碼 
  17.   } 

觸發(fā)狀態(tài)轉(zhuǎn)移是靠調(diào)用resolve()或reject()實現(xiàn)的。當(dāng)resolve()被調(diào)用時,當(dāng)前Promise也不一定會立即變?yōu)镕ulfilled狀態(tài),因為傳入resolve(value)方法的value有可能也是一個Promise,這個時候,當(dāng)前Promise必須追蹤傳入的這個Promise的狀態(tài),整個確定Promise狀態(tài)的過程是通過Promise Resolution Procedure算法實現(xiàn)的,具體細(xì)節(jié)封裝到了下面代碼中的resolvePromiseWithValue函數(shù)中。當(dāng)reject()被調(diào)用時,當(dāng)前Promise的狀態(tài)就是確定的,一定是Rejected,此時可以通過transition函數(shù)(封裝了狀態(tài)轉(zhuǎn)移的細(xì)節(jié))將Promise的狀態(tài)進(jìn)行轉(zhuǎn)移,并執(zhí)行后續(xù)動作。

  1. // resolve的執(zhí)行,是一個觸發(fā)信號,基于此進(jìn)行下一步的操作 
  2. function resolve(value) { 
  3.   resolvePromiseWithValue(this, value) 
  4. // reject的執(zhí)行,是狀態(tài)可以變?yōu)镽ejected的信號 
  5. function reject(reason) { 
  6.   transition(this, PROMISE_STATES.REJECTED, reason) 
  7.  
  8. class MyPromise { 
  9.   constructor(executor) { 
  10.     this.state = PROMISE_STATES.PENDING; 
  11.     this.fulfillQueue = []; 
  12.     this.rejectQueue = []; 
  13.     // 構(gòu)造Promise實例后,立刻調(diào)用executor 
  14.     executor(resolve.bind(this), reject.bind(this)) 
  15.   } 

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

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

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

  1. // 回調(diào)應(yīng)異步執(zhí)行,所以用到了nextTick 
  2. nextTick(() => { 
  3.   // then可能會被調(diào)用多次,所以異步回調(diào)應(yīng)該用數(shù)組來維護(hù) 
  4.   promise.fulfillQueue.forEach(({ handler, chainedPromise }) => { 
  5.     try { 
  6.       if (typeof handler === 'function') { 
  7.         const adoptedValue = handler(value) 
  8.         // 異步回調(diào)返回的值將決定衍生的Promise的狀態(tài) 
  9.         resolvePromiseWithValue(chainedPromise, adoptedValue) 
  10.       } else { 
  11.         // 存在調(diào)用了then,但是沒傳回調(diào)作為參數(shù)的可能,此時衍生的Promise的狀態(tài)直接采納其關(guān)聯(lián)的Promise的狀態(tài)。 
  12.         transition(chainedPromise, PROMISE_STATES.FULFILLED, promise.value) 
  13.       } 
  14.     } catch (error) { 
  15.       // 如果回調(diào)拋出了異常,此時直接將衍生的Promise的狀態(tài)轉(zhuǎn)移為rejected,并用異常error作為reason 
  16.       transition(chainedPromise, PROMISE_STATES.REJECTED, error) 
  17.     } 
  18.   }) 
  19.   // 最后清空該P(yáng)romise關(guān)聯(lián)的回調(diào)隊列 
  20.   promise.fulfillQueue = []; 
  21. }) 

Promise Resolution Procedure算法

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

2.3.1

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

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

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

代碼實現(xiàn):

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

2.3.2

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

  1. 2.3.2 If x is a promise, adopt its state [3.4]: 
  2. 2.3.2.1 If x is pending, promise must remain pending until x is fulfilled or rejected. 
  3. 2.3.2.2 If/when x is fulfilled, fulfill promise with the same value. 
  4. 2.3.2.3 If/when x is rejected, reject promise with the same reason. 

【說人話】小王問領(lǐng)導(dǎo):“今年會發(fā)年終獎嗎?發(fā)多少?”領(lǐng)導(dǎo)聽了心里想,“這個事我之前也在打聽,不過還沒定下來,得看老板的意思。”,于是領(lǐng)導(dǎo)對小王說:“會發(fā)的,不過要等消息!”。

注意,這個時候,領(lǐng)導(dǎo)對小王許下了承諾,但是這個承諾p2的狀態(tài)還是pending,需要看老板給的承諾p1的狀態(tài)。

  • 可能性1:過了幾天,老板對領(lǐng)導(dǎo)說:“今年業(yè)務(wù)做得可以,年終獎發(fā)1000萬”。這里相當(dāng)于p1已經(jīng)是fulfilled狀態(tài)了,value是1000萬。領(lǐng)導(dǎo)拿了這個準(zhǔn)信了,自然可以跟小王兌現(xiàn)承諾p2了,于是對小王說:“年終獎可以下來了,是1000萬!”。這時,承諾p2的狀態(tài)就是fulfilled了,value也是1000萬。小王這個時候就“別墅靠海”了。
  • 可能性2:過了幾天,老板有點發(fā)愁,對領(lǐng)導(dǎo)說:“今年業(yè)績不太行啊,年終獎就不發(fā)了吧,明年,咱們明年多發(fā)點。”顯然,這里p1就是rejected了,領(lǐng)導(dǎo)一看這情況不對啊,但也沒辦法,只能對小王說:“小王啊,今年公司情況特殊,年終獎就不發(fā)了。”這p2也隨之rejected了,小王內(nèi)心有點炸裂......

注意,Promise A/+規(guī)范2.3.2小節(jié)這里有兩個大的方向,一個是x的狀態(tài)未定,一個是x的狀態(tài)已定。在代碼實現(xiàn)上,這里有個技巧,對于狀態(tài)未定的情況,必須用訂閱的方式來實現(xiàn),而.then就是訂閱的絕佳途徑。

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

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

這是跑測試用例的效果圖,可以看到,872個case是全部通過的。

完整代碼

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

  • github倉庫:promises-aplus-robin[1](順手點個star就更好了)
  • 源碼[2]
  • 源碼注釋版[3]

缺陷

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

方法論

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

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

總結(jié)

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

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

 

  • Promises/A+規(guī)范[6]
  • Event Loop Processing Model[8]
  • tasks-microtasks-queues-and-schedules[9]
  • Jobs and Host Operations to Enqueue Jobs[10]

參考

[1]github倉庫:promises-aplus-robin: https://github.com/cumt-robin/promises-aplus-robin

[2]源碼: https://github.com/cumt-robin/promises-aplus-robin/blob/main/promises-aplus-robin.js

[3]源碼注釋版: https://github.com/cumt-robin/promises-aplus-robin/blob/main/promises-aplus-robin-annotated.js

[4]Inside look at modern web browser: https://developers.google.com/web/updates/2018/09/inside-browser-part1

[5]Using requestIdleCallback: https://developers.google.com/web/updates/2015/08/using-requestidlecallback?hl=en#using_requestidlecallback_to_make_dom_changes

[6]Promise/A+規(guī)范: https://promisesaplus.com/

[7]「思維導(dǎo)圖學(xué)前端 」6k字一文搞懂Javascript對象,原型,繼承: https://juejin.cn/post/6844904194097299463

[8]Event Loop Processing Model: https://html.spec.whatwg.org/#event-loop-processing-model

[9]tasks-microtasks-queues-and-schedules: https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

[10]Jobs and Host Operations to Enqueue Jobs: https://tc39.es/ecma262/#sec-jobs

 

責(zé)任編輯:武曉燕 來源: 前端司南
相關(guān)推薦

2020-07-29 17:35:08

Promise源碼前端

2019-08-13 09:29:14

Kafka運(yùn)營數(shù)據(jù)

2020-08-26 10:03:31

MySQL索引

2020-08-13 10:15:34

MySQL數(shù)據(jù)庫面試

2019-07-15 16:35:43

MySQL索引阿里

2021-09-30 07:52:06

AbortSignalpromiseAPI

2022-01-04 20:52:50

函數(shù)異步Promise

2025-03-27 10:13:03

2023-09-15 15:31:23

異步編程Promise

2017-05-11 20:20:59

JavascriptPromiseWeb

2018-03-13 16:04:45

Promise執(zhí)行順序

2021-04-12 09:09:57

Webpack 工具架構(gòu)

2020-11-05 11:10:43

程序員開發(fā)工具

2021-04-29 08:28:24

架構(gòu)參數(shù)傳遞

2020-12-15 08:01:24

Promise參數(shù)ES6

2021-04-28 08:21:21

Promise.any服務(wù)器場景

2021-04-27 08:31:37

Promisereject信息

2025-06-13 09:40:45

2020-09-24 11:46:03

Promise

2019-12-09 15:20:09

JavascriptPromise前端
點贊
收藏

51CTO技術(shù)棧公眾號