91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

怎么實現Promise/A+規范

發布時間:2021-10-18 10:18:22 來源:億速云 閱讀:113 作者:柒染 欄目:web開發

這篇文章給大家介紹怎么實現Promise/A+規范,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。

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

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

  • github倉庫:promises-aplus-robin[1](順手點個star就更好了)

  • 源碼[2]

  • 源碼注釋版[3]

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

怎么實現Promise/A+規范

Promise源于現實世界

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

許下承諾

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

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

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

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

Promise跟這些餅很像,分為三種狀態:

  • pending: 餅已畫好,坐等實現。

  • fulfilled: 餅真的實現了,走上人生巔峰。

  • rejected: 不好意思,畫餅失敗,emmm...

訂閱承諾

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

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

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

鏈式傳播

眾所周知,老板可以給高層或領導們畫餅,而領導們拿著老板畫的餅,也必須給底下員工繼續畫餅,讓打工人們雞血不停,這樣大家的餅才都有可能兌現。

這種自上而下發餅的行為與Promise的鏈式調用在思路上不謀而合。

bossBing.then(   success => {     // leader接過boss的餅,繼續往下面發餅     return leaderBing   } ).then(   success => {     console.log('leader畫的餅真的實現了,別墅靠海')   },   fail => {     console.log('leader畫的餅炸了,以淚洗面...')   } )

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

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

初識Promise

還記得最早接觸Promise的時候,我感覺能把ajax過程封裝起來就挺“厲害”了。那個時候對Promise的印象大概就是:優雅的異步封裝,不再需要寫高耦合的callback。

這里臨時手擼一個簡單的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);     }     // 設置請求頭     const mergedHeaders = Object.assign({}, defaultHeaders, headers)     Object.keys(mergedHeaders).forEach(key => {       xhr.setRequestHeader(key, mergedHeaders[key]);     })     // 狀態監聽     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數據,發送請求     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的形式調用接口 request(options).then(res => {   // 請求成功 }, fail => {   // 請求失敗 })

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

  • 封裝一個函數,將包含異步過程的代碼包裹在構造Promise的executor中,所封裝的函數最后需要return這個Promise實例。

  • Promise有三種狀態,Pending, Fulfilled, Rejected。而resolve(), reject()是狀態轉移的觸發器。

  • 確定狀態轉移的條件,在本例中,我們認為ajax響應且狀態碼為200時,請求成功(執行resolve()),否則請求失敗(執行reject())。

ps: 實際業務中,除了判斷HTTP狀態碼,我們還會另外判斷內部錯誤碼(業務系統中前后端約定的狀態code)。

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

宏任務與微任務

要理解Promise/A+規范,必須先溯本求源,Promise與微任務息息相關,所以我們有必要先對宏任務和微任務有個基本認識。

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

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()會沒有效果。

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

現代瀏覽器采用多進程架構,這一點可以參考Inside look at modern web  browser[4]。而和我們前端關系最緊密的就是其中的Renderer Process,Javascript便是運行在Renderer  Process的Main Thread中。

怎么實現Promise/A+規范

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

渲染進程控制了展示在Tab頁中的網頁的一切事情。可以理解為渲染進程就是專門為具體的某個網頁服務的。

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

為了保證界面交互的連貫性和平滑度,Main  Thread中,Javascript的執行和頁面的渲染會交替執行(出于性能考慮,某些情況下,瀏覽器判斷不需要執行界面渲染,會略過渲染的步驟)。目前大多數設備的屏幕刷新率為60次/秒,1幀大約是16.67ms,在這1幀的周期內,既要完成Javascript的執行,還要完成界面的渲染(if  necessary),利用人眼的殘影效應,讓用戶覺得界面交互是非常流暢的。

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

怎么實現Promise/A+規范

怎么實現Promise/A+規范

怎么實現Promise/A+規范

PS:requestIdleCallback是空閑回調,在1幀的末尾,如果還有時間富余,就會調用requestIdleCallback。注意不要在requestIdleCallback中修改DOM,或者讀取布局信息導致觸發Forced  Synchronized Layout,否則會引發性能和體驗問題。具體見Using requestIdleCallback[5]。

我們知道,一個網頁中的Render Process只有一個Main  Thread,本質上來說,Javascript的任務在執行階段都是按順序執行,但是JS引擎在解析Javascript代碼時,會把代碼分為同步任務和異步任務。同步任務直接進入Main  Thread執行;異步任務進入任務隊列,并關聯著一個異步回調。

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

異步任務又分為Task和Microtask,各自有單獨的數據結構和內存來維護。

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

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)

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

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

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

Task和Microtask都有哪些?

  • Tasks:

    • setTimeout

    • setInterval

    • MessageChannel

    • I/0(文件,網絡)相關API

    • DOM事件監聽:瀏覽器環境

    • setImmediate:Node環境,IE好像也支持(見caniuse數據)

  • Microtasks:

    • requestAnimationFrame:瀏覽器環境

    • MutationObserver:瀏覽器環境

    • Promise.prototype.then, Promise.prototype.catch,  Promise.prototype.finally

    • process.nextTick:Node環境

    • queueMicrotask

requestAnimationFrame是不是微任務?

requestAnimationFrame簡稱rAF,經常被我們用來做動畫效果,因為其回調函數執行頻率與瀏覽器屏幕刷新頻率保持一致,也就是我們通常說的它能實現60FPS的效果。在rAF被大范圍應用前,我們經常使用setTimeout來處理動畫。但是setTimeout在主線程繁忙時,不一定能及時地被調度,從而出現卡頓現象。

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

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

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

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

你會發現,在無限遞歸的情況下,rAF回調正常執行,瀏覽器也可正常交互,沒有出現阻塞的現象。

怎么實現Promise/A+規范

而如果rAF是微任務的話,則不會有這種待遇。不信你可以翻到后面一節內容「如果Microtask執行時又創建了Microtask,怎么處理?」。

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

Task和Microtask各有1個隊列?

最初,我認為既然瀏覽器區分了Task和Microtask,那就只要各自安排一個隊列存儲任務即可。事實上,Task根據task  source的不同,安排了獨立的隊列。比如Dom事件屬于Task,但是Dom事件有很多種類型,為了方便user  agent細分Task并精細化地安排各種不同類型Task的處理優先級,甚至做一些優化工作,必須有一個task  source來區分。同理,Microtask也有自己的microtask task source。

具體解釋見HTML標準中的一段話:

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的消費機制是怎樣的?

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

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

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

怎么實現Promise/A+規范

Task和Microtask什么時候進入相應隊列?

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

宏任務進隊列

對于Task而言,任務注冊時就會進入隊列,只是任務的狀態還不是runnable,不具備被Event Loop撈起的條件。

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

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

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

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

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

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

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

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

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

微任務進隊列

前面我們提到一個觀點,執行完一個Task后,如果Microtask隊列不為空,會把Microtask隊列中所有的Microtask都取出來執行。我認為,Microtask不是在注冊時就進入Microtask隊列,因為Event  Loop處理Microtask隊列時,并不會判斷Microtask的狀態。反過來想,如果Microtask在注冊時就進入Microtask隊列,就會存在Microtask還未變為runnable狀態就被執行的情況,這顯然是不合理的。我的觀點是,Microtask在變為runnable狀態時才進入Microtask隊列。

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

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

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

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

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

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

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

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

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

如果Microtask執行時又創建了Microtask,怎么處理?

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

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

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

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

為什么要區分Task和Microtask?

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

做幾道題熱熱身?

在沖擊Promise/A+規范前,不妨先用幾個習題來測試下自己對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); })

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

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

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

怎么實現Promise/A+規范

那么這背后的執行順序到底是怎樣的呢?仔細分析下,你會發現還是有跡可循的。

老規矩,第一個問題,這道題的代碼執行過程中,產生了多少個微任務?可能很多人認為是7個,但實際上應該是8個。

編號注冊時機異步回調
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);
mt8return Promise.resolve(4)執行并且execution context stack清空后,隱式注冊隱式回調(未體現在代碼中),目的是讓mt2變成runnable狀態
  • 同步任務執行,注冊mt1~mt7七個微任務,此時execution context  stack為空,并且mt1和mt3的狀態變為runnable。JS引擎安排mt1和mt3進入Microtask隊列(通過HostEnqueuePromiseJob實現)。

  • Perform a microtask checkpoint,由于mt1和mt3是在同一次JS  call中變為runnable的,所以mt1和mt3的回調先后進入execution context stack執行。

  • mt1回調進入execution context  stack執行,輸出0,返回Promise.resolve(4)。mt1出隊列。由于mt1回調返回的是一個狀態為fulfilled的Promise,所以之后JS引擎會安排一個job(job是ecma中的概念,等同于微任務的概念,這里先給它編號mt8),其回調目的是讓mt2的狀態變為fulfilled(前提是當前execution  context stack is empty)。所以緊接著還是先執行mt3的回調。

  • mt3回調進入execution context stack執行,輸出1,mt4變為runnable狀態,execution context stack  is empty,mt3出隊列。

  • 由于此時mt4已經是runnable狀態,JS引擎安排mt4進隊列,接著JS引擎會安排mt8進隊列。

  • 接著,mt4回調進入execution context  stack執行,輸出2,mt5變為runnable,mt4出隊列。JS引擎安排mt5進入Microtask隊列。

  • mt8回調執行,目的是讓mt2變成runnable狀態,mt8出隊列。mt2進隊列。

  • mt5回調執行,輸出3,mt6變為runnable,mt5出隊列。mt6進隊列。

  • mt2回調執行,輸出4,mt4出隊列。

  • mt6回調執行,輸出5,mt7變為runnable,mt6出隊列。mt7進隊列。

  • mt7回調執行,輸出6,mt7出隊列。執行完畢!總體來看,輸出結果依次為:0 1 2 3 4 5 6。

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

經過我在Edge瀏覽器測試,結果是:0 1 2 4 3 5  6。可以看到,不同瀏覽器在實現Promise的主流程上是吻合的,但是在一些細枝末節上還有不一致的地方。實際應用中,我們只要注意規避這種問題即可。

實現Promise/A+

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

怎么實現Promise/A+規范

仔細閱讀多遍規范之后,我有了一個基本認識,要實現Promise/A+規范,關鍵是要理清其中幾個核心點。

關系鏈路

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

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

  • .then()的特殊性在于,它會返回一個新的Promise實例,在這種連續調用.then()的情況下,就會串起一個Promise鏈,這與原型鏈又有一些相似之處。“恬不知恥”地再推薦一篇「思維導圖學前端  」6k字一文搞懂Javascript對象,原型,繼承[7],哈哈哈。

  • 另一個靈活的地方在于,p1.then(onFulfilled,  onRejected)返回的新Promise實例p2,其狀態轉移的發生是在p1的狀態轉移發生之后(這里的之后指的是異步的之后)。并且,p2的狀態轉移為Fulfilled還是Rejected,這一點取決于onFulfilled或onRejected的返回值,這里有一個較為復雜的分析過程,也就是后面所述的Promise  Resolution Procedure算法。

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

怎么實現Promise/A+規范

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

nextTick

看了前面內容,相信大家都有一個概念,微任務是一個異步任務,而我們要實現Promise的整套異步機制,必然要具備模擬微任務異步回調的能力。在規范中也提到了這么一條信息:

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.

我這里選擇的是用微任務來實現異步回調,如果用宏任務來實現異步回調,那么在Promise微任務隊列執行過程中就可能會穿插宏任務,這就不太符合微任務隊列的調度邏輯了。這里還對Node環境和瀏覽器環境做了兼容,Node環境中可以使用process.nextTick回調來模擬微任務的執行,而在瀏覽器環境中我們可以選擇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'   } }

狀態轉移

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

const PROMISE_STATES = {   PENDING: 'pending',   FULFILLED: 'fulfilled',   REJECTED: 'rejected' }  class MyPromise {   constructor(executor) {     this.state = PROMISE_STATES.PENDING;   }   // ...其他代碼 }

一旦Promise的狀態發生轉移,就不可再轉移為其他狀態。

/**  * 封裝Promise狀態轉移的過程  * @param {MyPromise} promise 發生狀態轉移的Promise實例  * @param {*} targetState 目標狀態  * @param {*} value 伴隨狀態轉移的值,可能是fulfilled的值,也可能是rejected的原因  */ function transition(promise, targetState, value) {   if (promise.state === PROMISE_STATES.PENDING && targetState !== PROMISE_STATES.PENDING) {     // 2.1: state只能由pending轉為其他態,狀態轉移后,state和value的值不再變化     Object.defineProperty(promise, 'state', {       configurable: false,       writable: false,       enumerable: true,       value: targetState     })     // ...其他代碼   } }

觸發狀態轉移是靠調用resolve()或reject()實現的。當resolve()被調用時,當前Promise也不一定會立即變為Fulfilled狀態,因為傳入resolve(value)方法的value有可能也是一個Promise,這個時候,當前Promise必須追蹤傳入的這個Promise的狀態,整個確定Promise狀態的過程是通過Promise  Resolution  Procedure算法實現的,具體細節封裝到了下面代碼中的resolvePromiseWithValue函數中。當reject()被調用時,當前Promise的狀態就是確定的,一定是Rejected,此時可以通過transition函數(封裝了狀態轉移的細節)將Promise的狀態進行轉移,并執行后續動作。

// resolve的執行,是一個觸發信號,基于此進行下一步的操作 function resolve(value) {   resolvePromiseWithValue(this, value) } // reject的執行,是狀態可以變為Rejected的信號 function reject(reason) {   transition(this, PROMISE_STATES.REJECTED, reason) }  class MyPromise {   constructor(executor) {     this.state = PROMISE_STATES.PENDING;     this.fulfillQueue = [];     this.rejectQueue = [];     // 構造Promise實例后,立刻調用executor     executor(resolve.bind(this), reject.bind(this))   } }

鏈式追蹤

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

當p1的狀態發生轉移時,微任務mt1回調會在接下來被執行,如果狀態是Fulfilled,則onFulfilled會被執行,否則onRejected會被執行。微任務mt1回調執行的結果將作為決定p2狀態的依據。以下是Fulfilled情況下的部分關鍵代碼,其中promise指的是p1,而chainedPromise指的是p2。

// 回調應異步執行,所以用到了nextTick nextTick(() => {   // then可能會被調用多次,所以異步回調應該用數組來維護   promise.fulfillQueue.forEach(({ handler, chainedPromise }) => {     try {       if (typeof handler === 'function') {         const adoptedValue = handler(value)         // 異步回調返回的值將決定衍生的Promise的狀態         resolvePromiseWithValue(chainedPromise, adoptedValue)       } else {         // 存在調用了then,但是沒傳回調作為參數的可能,此時衍生的Promise的狀態直接采納其關聯的Promise的狀態。         transition(chainedPromise, PROMISE_STATES.FULFILLED, promise.value)       }     } catch (error) {       // 如果回調拋出了異常,此時直接將衍生的Promise的狀態轉移為rejected,并用異常error作為reason       transition(chainedPromise, PROMISE_STATES.REJECTED, error)     }   })   // 最后清空該Promise關聯的回調隊列   promise.fulfillQueue = []; })

Promise Resolution Procedure算法

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

2.3.1

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

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

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

代碼實現:

if (promise === x) {     // 2.3.1 由于Promise采納狀態的機制,這里必須進行全等判斷,防止出現死循環     transition(promise, PROMISE_STATES.REJECTED, new TypeError('promise and x cannot refer to a same object.')) }

2.3.2

如果x是一個Promise實例,promise應該采納x的狀態。

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.

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

注意,這個時候,領導對小王許下了承諾,但是這個承諾p2的狀態還是pending,需要看老板給的承諾p1的狀態。

  • 可能性1:過了幾天,老板對領導說:“今年業務做得可以,年終獎發1000萬”。這里相當于p1已經是fulfilled狀態了,value是1000萬。領導拿了這個準信了,自然可以跟小王兌現承諾p2了,于是對小王說:“年終獎可以下來了,是1000萬!”。這時,承諾p2的狀態就是fulfilled了,value也是1000萬。小王這個時候就“別墅靠海”了。

  • 可能性2:過了幾天,老板有點發愁,對領導說:“今年業績不太行啊,年終獎就不發了吧,明年,咱們明年多發點。”顯然,這里p1就是rejected了,領導一看這情況不對啊,但也沒辦法,只能對小王說:“小王啊,今年公司情況特殊,年終獎就不發了。”這p2也隨之rejected了,小王內心有點炸裂......

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

else if (isPromise(x)) {     // 2.3.2 如果x是一個Promise實例,則追蹤并采納其狀態     if (x.state !== PROMISE_STATES.PENDING) {       // 假設x的狀態已經發生轉移,則直接采納其狀態       transition(promise, x.state, x.state === PROMISE_STATES.FULFILLED ? x.value : x.reason)     } else {       // 假設x的狀態還是pending,則只需等待x狀態確定后再進行promise的狀態轉移       // 而x的狀態轉移結果是不定的,所以兩種情況我們都需要進行訂閱       // 這里用一個.then很巧妙地完成了訂閱動作       x.then(value => {         // x狀態轉移為fulfilled,由于callback傳過來的value是不確定的類型,所以需要繼續應用Promise Resolution Procedure算法         resolvePromiseWithValue(promise, value, thenableValues)       }, reason => {         // x狀態轉移為rejected         transition(promise, PROMISE_STATES.REJECTED, reason)       })     } }

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

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

怎么實現Promise/A+規范

完整代碼

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

  • github倉庫:promises-aplus-robin[1](順手點個star就更好了)

  • 源碼[2]

  • 源碼注釋版[3]

缺陷

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

方法論

不管是手寫實現Promise/A+規范,還是實現其他Native Code,其本質上繞不開以下幾點:

  • 準確理解Native Code實現的能力,就像你理解一個需求要實現哪些功能點一樣,并確定實現上的優先級。

  • 針對每個功能點或者功能描述,逐一用代碼實現,優先打通主干流程。

  • 設計足夠豐富的測試用例,回歸測試,不斷迭代,保證場景的覆蓋率,最終打造一段優質的代碼。

關于怎么實現Promise/A+規范就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

克东县| 原阳县| 新营市| 扎囊县| 郯城县| 仲巴县| 孟州市| 图们市| 南汇区| 外汇| 思茅市| 台湾省| 沾益县| 义马市| 咸丰县| 牟定县| 洪泽县| 苍梧县| 迁安市| 滨海县| 东明县| 泉州市| 盈江县| 江油市| 和顺县| 宜兴市| 潞城市| 光泽县| 宣化县| 西华县| 龙门县| 桑植县| 平潭县| 阿巴嘎旗| 安平县| 新宾| 岳阳市| 新沂市| 南木林县| 丹棱县| 武强县|