JavaScript之手寫Promise

為更好的理解鼎文, 推薦閱讀Promise/A+ 規(guī)范

實(shí)現(xiàn)一個(gè)簡易版 Promise

在完成符合 Promise/A+ 規(guī)范的代碼之前冈止,我們可以先來實(shí)現(xiàn)一個(gè)簡易版 Promise,因?yàn)樵诿嬖囍胁穸眨绻隳軐?shí)現(xiàn)出一個(gè)簡易版的 Promise 基本可以過關(guān)了川尖。

那么我們先來搭建構(gòu)建函數(shù)的大體框架

const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'

function MyPromise(fn) {
  const that = this
  that.state = PENDING
  that.value = null
  that.resolvedCallbacks = []
  that.rejectedCallbacks = []
  // 待完善 resolve 和 reject 函數(shù)
  // 待完善執(zhí)行 fn 函數(shù)
}

  • 首先我們創(chuàng)建了三個(gè)常量用于表示狀態(tài),對(duì)于經(jīng)常使用的一些值都應(yīng)該通過常量來管理卓练,便于開發(fā)及后期維護(hù)
  • 在函數(shù)體內(nèi)部首先創(chuàng)建了常量 that,因?yàn)榇a可能會(huì)異步執(zhí)行购啄,用于獲取正確的 this 對(duì)象
  • 一開始 Promise 的狀態(tài)應(yīng)該是 pending
  • value 變量用于保存 resolve 或者 reject 中傳入的值
  • resolvedCallbacksrejectedCallbacks 用于保存 then 中的回調(diào)襟企,因?yàn)楫?dāng)執(zhí)行完 Promise 時(shí)狀態(tài)可能還是等待中,這時(shí)候應(yīng)該把 then 中的回調(diào)保存起來用于狀態(tài)改變時(shí)使用

接下來我們來完善 resolve 和 reject 函數(shù)狮含,添加在 MyPromise 函數(shù)體內(nèi)部

function resolve(value) {
  if (that.state === PENDING) {
    that.state = RESOLVED
    that.value = value
    that.resolvedCallbacks.map(cb => cb(that.value))
  }
}

function reject(value) {
  if (that.state === PENDING) {
    that.state = REJECTED
    that.value = value
    that.rejectedCallbacks.map(cb => cb(that.value))
  }
}

這兩個(gè)函數(shù)代碼類似顽悼,就一起解析了

  • 首先兩個(gè)函數(shù)都得判斷當(dāng)前狀態(tài)是否為等待中曼振,因?yàn)橐?guī)范規(guī)定只有等待態(tài)才可以改變狀態(tài)
  • 將當(dāng)前狀態(tài)更改為對(duì)應(yīng)狀態(tài),并且將傳入的值賦值給 value
  • 遍歷回調(diào)數(shù)組并執(zhí)行

完成以上兩個(gè)函數(shù)以后蔚龙,我們就該實(shí)現(xiàn)如何執(zhí)行 Promise 中傳入的函數(shù)了

try {
  fn(resolve, reject)
} catch (e) {
  reject(e)
}
  • 實(shí)現(xiàn)很簡單冰评,執(zhí)行傳入的參數(shù)并且將之前兩個(gè)函數(shù)當(dāng)做參數(shù)傳進(jìn)去
  • 要注意的是,可能執(zhí)行函數(shù)過程中會(huì)遇到錯(cuò)誤木羹,需要捕獲錯(cuò)誤并且執(zhí)行 reject 函數(shù)

最后我們來實(shí)現(xiàn)較為復(fù)雜的 then 函數(shù)

MyPromise.prototype.then = function(onFulfilled, onRejected) {
  const that = this
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
  onRejected =
    typeof onRejected === 'function'
      ? onRejected
      : r => {
          throw r
        }
  if (that.state === PENDING) {
    that.resolvedCallbacks.push(onFulfilled)
    that.rejectedCallbacks.push(onRejected)
  }
  if (that.state === RESOLVED) {
    onFulfilled(that.value)
  }
  if (that.state === REJECTED) {
    onRejected(that.value)
  }
}
  • 首先判斷兩個(gè)參數(shù)是否為函數(shù)類型甲雅,因?yàn)檫@兩個(gè)參數(shù)是可選參數(shù)

  • 當(dāng)參數(shù)不是函數(shù)類型時(shí),需要?jiǎng)?chuàng)建一個(gè)函數(shù)賦值給對(duì)應(yīng)的參數(shù)坑填,同時(shí)也實(shí)現(xiàn)了透傳抛人,比如如下代碼

// 該代碼目前在簡單版中會(huì)報(bào)錯(cuò)
// 只是作為一個(gè)透傳的例子
Promise.resolve(4).then().then((value) => console.log(value))
  • 接下來就是一系列判斷狀態(tài)的邏輯,當(dāng)狀態(tài)不是等待態(tài)時(shí)脐瑰,就去執(zhí)行相對(duì)應(yīng)的函數(shù)妖枚。如果狀態(tài)是等待態(tài)的話,就往回調(diào)函數(shù)中 push 函數(shù)蚪黑,比如如下代碼就會(huì)進(jìn)入等待態(tài)的邏輯
new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(1)
  }, 0)
}).then(value => {
  console.log(value)
})

以上就是簡單版 Promise 實(shí)現(xiàn)

實(shí)現(xiàn)一個(gè)符合 Promise/A+ 規(guī)范的 Promise

接下來大部分代碼都是根據(jù)規(guī)范去實(shí)現(xiàn)的盅惜。

我們先來改造一下 resolvereject 函數(shù)

function resolve(value) {
  if (value instanceof MyPromise) {
    return value.then(resolve, reject)
  }
  setTimeout(() => {
    if (that.state === PENDING) {
      that.state = RESOLVED
      that.value = value
      that.resolvedCallbacks.map(cb => cb(that.value))
    }
  }, 0)
}
function reject(value) {
  setTimeout(() => {
    if (that.state === PENDING) {
      that.state = REJECTED
      that.value = value
      that.rejectedCallbacks.map(cb => cb(that.value))
    }
  }, 0)
}
  • 對(duì)于 resolve 函數(shù)來說中剩,首先需要判斷傳入的值是否為 Promise 類型
  • 為了保證函數(shù)執(zhí)行順序忌穿,需要將兩個(gè)函數(shù)體代碼使用 setTimeout 包裹起來

接下來繼續(xù)改造 then 函數(shù)中的代碼,首先我們需要新增一個(gè)變量 promise2结啼,因?yàn)槊總€(gè) then 函數(shù)都需要返回一個(gè)新的 Promise 對(duì)象掠剑,該變量用于保存新的返回對(duì)象,然后我們先來改造判斷等待態(tài)的邏輯

if (that.state === PENDING) {
  return (promise2 = new MyPromise((resolve, reject) => {
    that.resolvedCallbacks.push(() => {
      try {
        const x = onFulfilled(that.value)
        resolutionProcedure(promise2, x, resolve, reject)
      } catch (r) {
        reject(r)
      }
    })

    that.rejectedCallbacks.push(() => {
      try {
        const x = onRejected(that.value)
        resolutionProcedure(promise2, x, resolve, reject)
      } catch (r) {
        reject(r)
      }
    })
  }))
}
  • 首先我們返回了一個(gè)新的 Promise 對(duì)象郊愧,并在 Promise 中傳入了一個(gè)函數(shù)
  • 函數(shù)的基本邏輯還是和之前一樣朴译,往回調(diào)數(shù)組中 push 函數(shù)
  • 同樣,在執(zhí)行函數(shù)的過程中可能會(huì)遇到錯(cuò)誤属铁,所以使用了 try...catch 包裹
  • 規(guī)范規(guī)定眠寿,執(zhí)行 onFulfilled 或者 onRejected 函數(shù)時(shí)會(huì)返回一個(gè) x,并且執(zhí)行 Promise 解決過程焦蘑,這是為了不同的 Promise 都可以兼容使用盯拱,比如 JQueryPromise 能兼容 ES6Promise

接下來我們改造判斷執(zhí)行態(tài)的邏輯

if (that.state === RESOLVED) {
  return (promise2 = new MyPromise((resolve, reject) => {
    setTimeout(() => {
      try {
        const x = onFulfilled(that.value)
        resolutionProcedure(promise2, x, resolve, reject)
      } catch (reason) {
        reject(reason)
      }
    })
  }))
}
  • 其實(shí)大家可以發(fā)現(xiàn)這段代碼和判斷等待態(tài)的邏輯基本一致,無非是傳入的函數(shù)的函數(shù)體需要異步執(zhí)行例嘱,這也是規(guī)范規(guī)定的
  • 對(duì)于判斷拒絕態(tài)的邏輯這里就不一一贅述了狡逢,留給大家自己完成這個(gè)作業(yè)

最后,當(dāng)然也是最難的一部分拼卵,也就是實(shí)現(xiàn)兼容多種 PromiseresolutionProcedure 函數(shù)

function resolutionProcedure(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(new TypeError('Error'))
  }
}

首先規(guī)范規(guī)定了 x 不能與 promise2 相等奢浑,這樣會(huì)發(fā)生循環(huán)引用的問題,比如如下代碼

let p = new MyPromise((resolve, reject) => {
  resolve(1)
})
let p1 = p.then(value => {
  return p1
})

然后需要判斷 x 的類型

if (x instanceof MyPromise) {
    x.then(function(value) {
        resolutionProcedure(promise2, value, resolve, reject)
    }, reject)
}

這里的代碼是完全按照規(guī)范實(shí)現(xiàn)的腋腮。如果 xPromise 的話雀彼,需要判斷以下幾個(gè)情況:

  1. 如果 x 處于等待態(tài)壤蚜,Promise 需保持為等待態(tài)直至 x 被執(zhí)行或拒絕
  2. 如果 x 處于其他狀態(tài),則用相同的值處理 Promise

當(dāng)然以上這些是規(guī)范需要我們判斷的情況徊哑,實(shí)際上我們不判斷狀態(tài)也是可行的仍律。

接下來我們繼續(xù)按照規(guī)范來實(shí)現(xiàn)剩余的代碼

let called = false
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
  try {
    let then = x.then
    if (typeof then === 'function') {
      then.call(
        x,
        y => {
          if (called) return
          called = true
          resolutionProcedure(promise2, y, resolve, reject)
        },
        e => {
          if (called) return
          called = true
          reject(e)
        }
      )
    } else {
      resolve(x)
    }
  } catch (e) {
    if (called) return
    called = true
    reject(e)
  }
} else {
  resolve(x)
}
  • 首先創(chuàng)建一個(gè)變量 called 用于判斷是否已經(jīng)調(diào)用過函數(shù)
  • 然后判斷 x 是否為對(duì)象或者函數(shù),如果都不是的話实柠,將 x 傳入 resolve
  • 如果 x 是對(duì)象或者函數(shù)的話水泉,先把 x.then 賦值給 then,然后判斷 then 的類型窒盐,如果不是函數(shù)類型的話草则,就將 x 傳入 resolve
  • 如果 then 是函數(shù)類型的話,就將 x 作為函數(shù)的作用域 this 調(diào)用之蟹漓,并且傳遞兩個(gè)回調(diào)函數(shù)作為參數(shù)炕横,第一個(gè)參數(shù)叫做 resolvePromise ,第二個(gè)參數(shù)叫做 rejectPromise葡粒,兩個(gè)回調(diào)函數(shù)都需要判斷是否已經(jīng)執(zhí)行過函數(shù)份殿,然后進(jìn)行相應(yīng)的邏輯
  • 以上代碼在執(zhí)行的過程中如果拋錯(cuò)了,將錯(cuò)誤傳入 reject 函數(shù)中

以上就是符合 Promise/A+ 規(guī)范的實(shí)現(xiàn)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末嗽交,一起剝皮案震驚了整個(gè)濱河市卿嘲,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌夫壁,老刑警劉巖拾枣,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異盒让,居然都是意外死亡梅肤,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門邑茄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來姨蝴,“玉大人,你說我怎么就攤上這事肺缕∽笠剑” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵搓谆,是天一觀的道長炒辉。 經(jīng)常有香客問我,道長泉手,這世上最難降的妖魔是什么黔寇? 我笑而不...
    開封第一講書人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮斩萌,結(jié)果婚禮上缝裤,老公的妹妹穿的比我還像新娘屏轰。我一直安慰自己,他們只是感情好憋飞,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開白布霎苗。 她就那樣靜靜地躺著,像睡著了一般榛做。 火紅的嫁衣襯著肌膚如雪唁盏。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,443評(píng)論 1 302
  • 那天检眯,我揣著相機(jī)與錄音厘擂,去河邊找鬼。 笑死锰瘸,一個(gè)胖子當(dāng)著我的面吹牛刽严,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播避凝,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼舞萄,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了管削?” 一聲冷哼從身側(cè)響起倒脓,我...
    開封第一講書人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎佩谣,沒想到半個(gè)月后把还,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體实蓬,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡茸俭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了安皱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片调鬓。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖酌伊,靈堂內(nèi)的尸體忽然破棺而出腾窝,到底是詐尸還是另有隱情,我是刑警寧澤居砖,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布虹脯,位于F島的核電站,受9級(jí)特大地震影響奏候,放射性物質(zhì)發(fā)生泄漏循集。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一蔗草、第九天 我趴在偏房一處隱蔽的房頂上張望咒彤。 院中可真熱鬧疆柔,春花似錦、人聲如沸镶柱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽歇拆。三九已至鞋屈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間故觅,已是汗流浹背谐区。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留逻卖,地道東北人宋列。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像评也,于是被迫代替她去往敵國和親炼杖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354

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