現(xiàn)詳解)
1. 為什么需要手寫Promise在2015年ES6標(biāo)準(zhǔn)發(fā)布之前前端開發(fā)中處理異步操作主要依賴回調(diào)函數(shù)。隨著應(yīng)用復(fù)雜度提升回調(diào)地獄Callback Hell成為每個(gè)前端開發(fā)者都經(jīng)歷過的噩夢。Promise的出現(xiàn)徹底改變了這種局面它通過鏈?zhǔn)秸{(diào)用和統(tǒng)一的錯(cuò)誤處理機(jī)制讓異步代碼擁有了接近同步代碼的可讀性。我至今記得第一次在生產(chǎn)環(huán)境調(diào)試多層嵌套回調(diào)時(shí)的崩潰體驗(yàn)。那是一個(gè)電商促銷活動頁面需要依次執(zhí)行用戶登錄校驗(yàn)→獲取購物車數(shù)據(jù)→查詢庫存狀態(tài)→計(jì)算優(yōu)惠金額→生成訂單預(yù)覽。五層回調(diào)嵌套讓代碼縮進(jìn)達(dá)到了可怕的深度更不用說錯(cuò)誤處理分散在各個(gè)層級。正是這次經(jīng)歷讓我下定決心深入理解Promise機(jī)制。手寫Promise實(shí)現(xiàn)的意義遠(yuǎn)不止于應(yīng)付面試。當(dāng)你真正從零實(shí)現(xiàn)過Promise能透徹理解then/catch/finally的鏈?zhǔn)秸{(diào)用原理會明白微任務(wù)隊(duì)列與事件循環(huán)的關(guān)系可以精準(zhǔn)處理各種邊界情況如then中返回Promise對async/await的底層實(shí)現(xiàn)不再感到神秘2. Promise核心規(guī)范解讀2.1 狀態(tài)機(jī)模型Promise本質(zhì)上是一個(gè)狀態(tài)機(jī)包含三種狀態(tài)Pending等待態(tài)Fulfilled執(zhí)行態(tài)Rejected拒絕態(tài)狀態(tài)轉(zhuǎn)換是不可逆的且只能發(fā)生一次。這個(gè)特性在實(shí)現(xiàn)時(shí)需要特別注意我曾在早期版本中犯過允許重復(fù)改變狀態(tài)的錯(cuò)誤// 錯(cuò)誤示例 class MyPromise { constructor(executor) { this.state pending executor( value { this.state fulfilled // 可能被后續(xù)reject覆蓋 this.value value }, reason { this.state rejected // 可能被后續(xù)resolve覆蓋 this.reason reason } ) } }正確的實(shí)現(xiàn)應(yīng)該加入狀態(tài)保護(hù)if (this.state ! pending) return this.state fulfilled2.2 Then方法規(guī)范then方法是Promise最復(fù)雜的部分規(guī)范中關(guān)于它的描述占據(jù)了大量篇幅。核心要點(diǎn)包括參數(shù)可選性onFulfilled和onRejected都是可選參數(shù)值穿透當(dāng)參數(shù)不是函數(shù)時(shí)需要實(shí)現(xiàn)值穿透異步執(zhí)行then回調(diào)必須異步執(zhí)行返回新Promise每次調(diào)用then都返回新Promise這里有個(gè)容易忽略的細(xì)節(jié)then方法的回調(diào)需要放在微任務(wù)隊(duì)列。在瀏覽器環(huán)境中可以用MutationObserver模擬const nextTick (fn) { const observer new MutationObserver(fn) const textNode document.createTextNode() observer.observe(textNode, { characterData: true }) textNode.data 1 }2.3 Promise解決過程Promise Resolution Procedure是規(guī)范中定義的抽象操作用于處理then方法返回值的解析過程。它需要處理以下情況返回值是普通值直接fulfill返回值是Promise等待其狀態(tài)改變返回值包含then方法按thenable處理我曾在這個(gè)環(huán)節(jié)踩過循環(huán)引用的坑const p new MyPromise(resolve { const obj {} obj.self obj // 循環(huán)引用 resolve(obj) })正確處理方式需要加入遞歸檢測function resolvePromise(promise2, x, resolve, reject) { if (x promise2) { return reject(new TypeError(循環(huán)引用)) } // ...其他處理邏輯 }3. 完整實(shí)現(xiàn)與關(guān)鍵點(diǎn)解析3.1 基礎(chǔ)框架搭建我們先構(gòu)建Promise的骨架結(jié)構(gòu)class MyPromise { constructor(executor) { this.state pending this.value undefined this.reason undefined this.onFulfilledCallbacks [] this.onRejectedCallbacks [] const resolve (value) { if (this.state ! pending) return this.state fulfilled this.value value this.onFulfilledCallbacks.forEach(fn fn()) } const reject (reason) { if (this.state ! pending) return this.state rejected this.reason reason this.onRejectedCallbacks.forEach(fn fn()) } try { executor(resolve, reject) } catch (e) { reject(e) } } }這里有幾個(gè)關(guān)鍵設(shè)計(jì)回調(diào)隊(duì)列處理異步注冊then回調(diào)的情況立即執(zhí)行executor同步執(zhí)行錯(cuò)誤捕獲executor中的異常自動reject3.2 Then方法實(shí)現(xiàn)完整then方法實(shí)現(xiàn)如下then(onFulfilled, onRejected) { // 參數(shù)校驗(yàn)與默認(rèn)值處理 onFulfilled typeof onFulfilled function ? onFulfilled : v v onRejected typeof onRejected function ? onRejected : err { throw err } const promise2 new MyPromise((resolve, reject) { if (this.state fulfilled) { setTimeout(() { try { const x onFulfilled(this.value) resolvePromise(promise2, x, resolve, reject) } catch (e) { reject(e) } }) } else if (this.state rejected) { setTimeout(() { try { const x onRejected(this.reason) resolvePromise(promise2, x, resolve, reject) } catch (e) { reject(e) } }) } else { this.onFulfilledCallbacks.push(() { setTimeout(() { try { const x onFulfilled(this.value) resolvePromise(promise2, x, resolve, reject) } catch (e) { reject(e) } }) }) this.onRejectedCallbacks.push(() { setTimeout(() { try { const x onRejected(this.reason) resolvePromise(promise2, x, resolve, reject) } catch (e) { reject(e) } }) }) } }) return promise2 }實(shí)現(xiàn)要點(diǎn)說明使用setTimeout模擬微任務(wù)實(shí)際項(xiàng)目應(yīng)該用queueMicrotask三種狀態(tài)分別處理錯(cuò)誤捕獲包裹每個(gè)回調(diào)返回新Promise實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用3.3 靜態(tài)方法實(shí)現(xiàn)Promise還包含一些重要的靜態(tài)方法static resolve(value) { if (value instanceof MyPromise) return value return new MyPromise(resolve resolve(value)) } static reject(reason) { return new MyPromise((_, reject) reject(reason)) } static all(promises) { return new MyPromise((resolve, reject) { const result [] let count 0 const processResult (value, index) { result[index] value if (count promises.length) resolve(result) } promises.forEach((p, i) { MyPromise.resolve(p).then( value processResult(value, i), reject ) }) }) } static race(promises) { return new MyPromise((resolve, reject) { promises.forEach(p { MyPromise.resolve(p).then(resolve, reject) }) }) }特別提醒Promise.all在處理空數(shù)組時(shí)會立即resolve這個(gè)邊界情況需要特別注意。4. 常見問題與調(diào)試技巧4.1 異步時(shí)序問題在測試Promise實(shí)現(xiàn)時(shí)最常遇到的就是異步時(shí)序問題。比如console.log(1) new MyPromise(resolve { console.log(2) resolve(3) }).then(console.log) console.log(4) // 正確輸出順序1 2 4 3如果輸出順序不對說明then回調(diào)沒有正確異步執(zhí)行。建議使用以下測試用例驗(yàn)證let a 0 new MyPromise(resolve resolve(1)) .then(v { a v return 2 }) .then(v { a v }) setTimeout(() { console.log(a) // 應(yīng)該輸出3 })4.2 內(nèi)存泄漏排查Promise實(shí)現(xiàn)不當(dāng)可能導(dǎo)致內(nèi)存泄漏。我曾遇到一個(gè)案例長時(shí)間運(yùn)行的Node服務(wù)中未處理的rejected Promise會持續(xù)增長。解決方法是在構(gòu)造函數(shù)中加入以下保護(hù)if (!(this instanceof MyPromise)) { throw new TypeError(Promise必須通過new調(diào)用) }使用Chrome DevTools的Memory面板可以檢測Promise內(nèi)存泄漏記錄堆快照過濾MyPromise檢查實(shí)例數(shù)量是否異常增長4.3 與原生Promise的差異雖然我們的實(shí)現(xiàn)遵循Promise/A規(guī)范但與原生Promise仍存在一些差異微任務(wù)隊(duì)列原生使用真正的微任務(wù)我們用setTimeout模擬調(diào)試信息原生Promise有更好的toString輸出性能優(yōu)化原生實(shí)現(xiàn)經(jīng)過高度優(yōu)化可以通過以下方式測試兼容性MyPromise.deferred function() { const dfd {} dfd.promise new MyPromise((resolve, reject) { dfd.resolve resolve dfd.reject reject }) return dfd }然后使用Promise/A測試套件進(jìn)行驗(yàn)證。5. 從Promise到async/await理解Promise是掌握async/await的基礎(chǔ)。實(shí)際上async函數(shù)可以看作是基于Promise的語法糖async function foo() { return 1 } // 等價(jià)于 function foo() { return Promise.resolve(1) }而await的行為類似于調(diào)用thenconst result await somePromise // 近似等價(jià)于 somePromise.then(result { // 后續(xù)代碼 })在實(shí)現(xiàn)自己的Promise后你會更清楚為什么await只能在async函數(shù)中使用如何正確處理async函數(shù)中的錯(cuò)誤為什么多個(gè)await是順序執(zhí)行而非并行一個(gè)常見的誤區(qū)是過度使用順序await// 低效寫法 const user await getUser() const posts await getPosts(user.id) // 優(yōu)化寫法 const [user, posts] await Promise.all([ getUser(), getPosts(user.id) ])手寫Promise的經(jīng)歷讓我養(yǎng)成了查看Babel轉(zhuǎn)譯結(jié)果的習(xí)慣。比如下面這個(gè)async函數(shù)async function example() { try { const v1 await step1() const v2 await step2(v1) return v2 } catch (err) { console.error(err) } }通過Babel轉(zhuǎn)譯后可以看到它本質(zhì)上是一個(gè)返回Promise的生成器函數(shù)用到了大量的Promise處理邏輯。這正是我們手動實(shí)現(xiàn)過的那些機(jī)制。