手寫(xiě)一個(gè)簡(jiǎn)易的Promise

1. 簡(jiǎn)述 Promise

所謂 Promise,簡(jiǎn)單來(lái)說(shuō)史飞,就是一個(gè)容器牵舱,里面保存著某個(gè)未來(lái)才會(huì)結(jié)束的事件(通常是一個(gè)異步操作)的結(jié)果。
Promise 對(duì)異步調(diào)用進(jìn)行封裝赖歌,是一種異步編程的解決方案枉圃。
從語(yǔ)法上來(lái)說(shuō),Promise 是一個(gè)對(duì)象庐冯,從它可以獲取異步操作的消息孽亲。

1.1 解決什么問(wèn)題

有了 Promise 對(duì)象,就可以將異步操作以同步操作的流程表達(dá)出來(lái)展父,避免了層層嵌套的回調(diào)函數(shù)墨林,即回調(diào)地獄。

1.2 優(yōu)點(diǎn)
  • 減少縮進(jìn)
    讓回調(diào)函數(shù)變成了規(guī)范的鏈?zhǔn)綄?xiě)法犯祠,程序流程可以看得很清楚旭等。
改寫(xiě)前:
f1( xxx , function f2(a){
  f3( yyy , function f4(b){
      f5( a + b , function f6(){})
  })
})

改寫(xiě)后:
f1(xxx)
   .then(f2)    // f2 里面調(diào)用f3
   .then(f4)    // f4 里面調(diào)用f5,注意衡载,f2 的輸出作為 f4 的輸入搔耕,即可將 a 傳給 f4
   .then(f6)
  • 消滅 if (error)的寫(xiě)法
    為多個(gè)回調(diào)函數(shù)中拋出的錯(cuò)誤,統(tǒng)一指定處理方法。

而且弃榨,Promise 還有一個(gè)傳統(tǒng)寫(xiě)法沒(méi)有的好處:它的狀態(tài)一旦改變菩收,無(wú)論何時(shí)查詢(xún),都能得到這個(gè)狀態(tài)鲸睛。

1.3 用法
function fn(){
  //new Promise 接受一個(gè)函數(shù)娜饵,返回一個(gè)Promise實(shí)例
  return new Promise(( resolve, reject ) => {
       resolve()   // 成功時(shí)調(diào)用
       reject()     // 失敗時(shí)調(diào)用
  })
} 

fn().then(success, fail).then(success2, fail2)

new Promise 接受一個(gè)函數(shù),返回一個(gè) Promise 實(shí)例

1.4 完整API

Promise是一個(gè)類(lèi)

  • JS里類(lèi)是特殊的函數(shù)
  • 類(lèi)屬性:length(可以忽略)
    永遠(yuǎn)是1官辈,因?yàn)闃?gòu)造函數(shù)只接受一個(gè)參數(shù)
  • 類(lèi)方法:all / allSettled / race / reject/ resolve
  • 對(duì)象屬性:then / finally / catch
  • 對(duì)象內(nèi)部屬性:state = pending / fulfilled / rejected

API 的規(guī)則是箱舞? Promise / A+規(guī)格文檔 (JS 的 Promise的公開(kāi)標(biāo)準(zhǔn),中文翻譯 筆者不保證其準(zhǔn)確性)

1.5 其他

Promise 對(duì)象代表一個(gè)異步操作拳亿,有三種狀態(tài):pending(進(jìn)行中)晴股、fulfilled(已成功)和rejected(已失敗)肺魁。
狀態(tài)具有不受外界影響和不可逆2個(gè)特點(diǎn)电湘。

  • 不受外界影響
    指只有異步操作的結(jié)果,可以決定當(dāng)前是哪一種狀態(tài)鹅经,任何其他操作都無(wú)法改變這個(gè)狀態(tài)寂呛。這也是 Promise 這個(gè)名字的由來(lái),它的英語(yǔ)意思就是“承諾”瘾晃,表示其他手段無(wú)法改變昧谊。

  • 不可逆
    一旦狀態(tài)改變,就不會(huì)再變化酗捌,會(huì)一直保持這個(gè)結(jié)果呢诬,稱(chēng)為 resolved(已定型),任何時(shí)候都可以得到這個(gè)結(jié)果胖缤。

2. 寫(xiě)之前的準(zhǔn)備工作

2.1 創(chuàng)建目錄
promise-demo
    src
        promise.ts
    test
        index.ts
2.2 測(cè)試驅(qū)動(dòng)開(kāi)發(fā)

按照規(guī)范文檔寫(xiě)測(cè)試用例尚镰,
測(cè)試失敗 -> 改代碼 -> 測(cè)試成功 -> 加測(cè)試 -> ...

引入chaisinon (測(cè)試框架)
普通的測(cè)試用 chai就夠用了, sinon是用于測(cè)試函數(shù)的庫(kù)哪廓。

  • chai 安裝步驟
yarn global add ts-node mocha

//初始化
yarn init -y

yarn add chai mocha --dev

//添加TypeScript的類(lèi)型聲明文件
yarn add @types/chai @types/mocha --dev

//為了使用 yarn test 將以下兩個(gè)安裝到本地
yarn add --dev ts-node
yarn add --dev typescript

修改 package.json文件:
添加 test命令狗唉,這樣就不用每次執(zhí)行的時(shí)候都用 mocha -r ts-node/register test/**/*.ts命令,可以直接使用 yarn test 進(jìn)行測(cè)試涡真。

"scripts": {
    "test": "mocha -r ts-node/register test/**/*.ts"
  },
  • sinon 安裝步驟
yarn add sinon sinon-chai --dev
yarn add @types/sinon @types/sinon-chai --dev

3. 具體實(shí)現(xiàn)

3.1 new Promise() 必須接受一個(gè)函數(shù)作為參數(shù)

測(cè)試代碼:

import * as chai from "chai"
import Promise from "../src/promise"

const assert = chai.assert

describe("Promise", () => {
  it("是一個(gè)類(lèi)", () => {
    assert.isFunction(Promise)
    assert.isObject(Promise.prototype)
  })
  it("new Promise() 如果接受的不是一個(gè)函數(shù)就會(huì)報(bào)錯(cuò)", () => {
    //assert.thow(fn)的作用:如果fn報(bào)錯(cuò)分俯,控制臺(tái)就不報(bào)錯(cuò);如果fn不報(bào)錯(cuò)哆料,控制臺(tái)就報(bào)錯(cuò)缸剪。
    //即,預(yù)測(cè)fn會(huì)報(bào)錯(cuò)
    assert.throw(() => {
      // @ts-ignore
      new Promise()
    })
    assert.throw(() => {
      //@ts-ignore
      new Promise(1)
    })
    assert.throw(() => {
      //@ts-ignore
      new Promise(false)
    })
  })
})

assert.thow(fn)的作用:如果 fn報(bào)錯(cuò)东亦,控制臺(tái)就不報(bào)錯(cuò)杏节;如果 fn不報(bào)錯(cuò),控制臺(tái)就報(bào)錯(cuò)。
即奋渔,預(yù)測(cè) fn 會(huì)報(bào)錯(cuò)镊逝。

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

class Promise2 {
  constructor(fn) {
    if (typeof fn !== "function") {
      throw new Error("只接受函數(shù)作為參數(shù)!")
    }
  }
}
export default Promise2

測(cè)試通過(guò)嫉鲸。

測(cè)試結(jié)果.PNG
3.2 new Promise(fn) 會(huì)生成一個(gè)對(duì)象撑蒜,對(duì)象有 then 方法
test/index.ts

it("new Promise(fn)會(huì)生成一個(gè)對(duì)象,對(duì)象有 then 方法", () => {
    const promise = new Promise(() => { })
    assert.isObject(promise)
    assert.isFunction(promise.then)
 })

src/promise.ts

添加
then() {}
3.3 new Promise(fn)中的 fn 會(huì)立即執(zhí)行

如何判斷一個(gè)函數(shù)會(huì)立即執(zhí)行玄渗? => sinon提供了簡(jiǎn)便的方法

使用sinon :

import * as sinon from "sinon"
import * as sinonChai from "sinon-chai"

chai.use(sinonChai)
it("new Promise(fn)中的 fn 會(huì)立即執(zhí)行", () => {
   //sinon提供了一個(gè)假的函數(shù)座菠,這個(gè)假的函數(shù)知道自己有沒(méi)被調(diào)用
    let fn = sinon.fake()
    new Promise(fn)
   //如果這個(gè)函數(shù)被調(diào)用了,called 屬性就為 true
    assert(fn.called)
 })
3.4 new Promise(fn)中的 fn 執(zhí)行的時(shí)候必須接受 resolve 和 reject 兩個(gè)函數(shù)
it("new Promise(fn)中的 fn 執(zhí)行的時(shí)候必須接受 resolve 和 reject 兩個(gè)函數(shù)", done => {
    new Promise((resolve, reject) => {
      assert.isFunction(resolve)
      assert.isFunction(reject)
      done()
    })
  })

關(guān)于done :因?yàn)橛锌赡苓@兩個(gè)語(yǔ)句根本沒(méi)有執(zhí)行捻爷,測(cè)試也會(huì)通過(guò),所以使用 done 份企。用于保證 只有在運(yùn)行 assert.isFunction(resolve); assert.isFunction(reject)之后才會(huì)結(jié)束這個(gè)測(cè)試用例也榄。

3.5 promise.then(success)中的 success 會(huì)在 resolve 被調(diào)用的時(shí)候執(zhí)行
it("promise.then(success)中的 success 會(huì)在 resolve 被調(diào)用的時(shí)候執(zhí)行", done => {
    let success = sinon.fake()
    const promise = new Promise((resolve, reject) => {
      assert.isFalse(success.called)
      resolve()
      //先等resolve里的success執(zhí)行
      setTimeout(() => {
        assert.isTrue(success.called)
        done()
      })
    })
    promise.then(success)
  })

then的時(shí)候,是先把 success 保存下來(lái)司志,等fn 調(diào)用 resolve的時(shí)候甜紫,resolve就會(huì)調(diào)用 success。(異步調(diào)用)
resolve需要先等一會(huì)骂远,等 success先傳入囚霸。

class Promise2 {
  succeed = null
  constructor(fn) {
    if (typeof fn !== "function") {
      throw new Error("只接受函數(shù)作為參數(shù)!")
    }
    fn(this.resolve.bind(this), this.reject.bind(this))
  }
  resolve() {
    nextTick(() => {
      this.succeed()
    })
  }
  reject() {

  }
  then(succeed) {
    this.succeed = succeed
  }
}

promise.then(nulll,fail) 處的代碼類(lèi)似激才,不再說(shuō)明拓型。

3.6 參考文檔寫(xiě)測(cè)試用例

promisethen 方法接收兩個(gè)參數(shù):

promise.then(onFulfilled, onRejected)
  • onFulfilledonRejected 都是可選的參數(shù),此外瘸恼,如果參數(shù)不是函數(shù)劣挫,必須忽略
then(succeed?, fail?) {
    if (typeof succeed === "function") {
      this.succeed = succeed
    }
    if (typeof fail === "function") {
      this.fail = fail
    }
  }
  • 如果 onFulfilled 是函數(shù):
    此函數(shù)必須在 promise 完成(fulfilled)后被調(diào)用,并把 promise 的值(resolve接收的參數(shù))作為onFulfilled它的第一個(gè)參數(shù);
    此函數(shù)不能被調(diào)用超過(guò)一次
resolve(result) {
    if (this.state !== "pending") return;
    this.state = "fulfilled"
    nextTick(() => {
      if (typeof this.succeed === "function") {
        this.succeed(result)
      }
    })
  }

onRejected 類(lèi)似东帅,不再說(shuō)明压固。

  • then 可以在同一個(gè) promise 里被多次調(diào)用
    當(dāng) promise變?yōu)?fulfilled ,各個(gè)相應(yīng)的 onFulfilled 回調(diào) 必須按照最原始的 then 順序來(lái)執(zhí)行
    即傳的是 0 1 2,調(diào)用的時(shí)候的順序就是0 1 2

將目前的代碼進(jìn)行修改靠闭,目前的 then只保存一個(gè) succeed 和 一個(gè) fail帐我,但實(shí)際上有可能會(huì)調(diào)用多次。

  resolve(result) {
    if (this.state !== "pending") return;
    this.state = "fulfilled"
    nextTick(() => {
      //遍歷callbacks愧膀,調(diào)用所有的handle[0]
      this.callbacks.forEach(handle => {
        if (typeof handle[0] === "function") {
          handle[0].call(undefined, result)
        }
      })

    })
  }
  then(succeed?, fail?) {
    const handle = []
    if (typeof succeed === "function") {
      handle[0] = succeed
    }
    if (typeof fail === "function") {
      handle[1] = fail
    }
    //把函數(shù)推到 callbacks 里面
    this.callbacks.push(handle)
  }
  • then必須返回一個(gè)promise (便于使用鏈?zhǔn)秸{(diào)用)
    需要?jiǎng)?chuàng)建新的 Promise 實(shí)例來(lái)對(duì)第二個(gè)then 中接收的 succeedfail進(jìn)行存儲(chǔ)并執(zhí)行
    在原本的 resolvereject 函數(shù)中拦键,執(zhí)行第二個(gè) Promise 實(shí)例的resolve方法
    參數(shù)傳遞
it("2.2.7 then必須返回一個(gè)promise", done => {
    const promise = new Promise((resolve, reject) => {
      resolve()
    })
    const promise2 = promise.then(() => "成功", () => { })
    assert(promise2 instanceof Promise)
    promise2.then(result => {
      assert.equal(result, "成功")
      done()
    })
  })
resolve(result) {
    if (this.state !== "pending") return;
    this.state = "fulfilled"
    nextTick(() => {
      //遍歷callbacks,調(diào)用所有的handle[0]
      this.callbacks.forEach(handle => {
        let x
        if (typeof handle[0] === "function") {
          x = handle[0].call(undefined, result)
        }
        handle[2].resolve(x)
      })
    })
  }

  then(succeed?, fail?) {
    const handle = []
    if (typeof succeed === "function") {
      handle[0] = succeed
    }
    if (typeof fail === "function") {
      handle[1] = fail
    }
    handle[2] = new Promise2(() => { })
    //把函數(shù)推到 callbacks 里面
    this.callbacks.push(handle)
    return handle[2]
  }

再添加錯(cuò)誤處理進(jìn)行完善檩淋。

4. 手寫(xiě)Promise完整代碼

class Promise2 {
  state = "pending"
  callbacks = []

  constructor(fn) {
    if (typeof fn !== "function") {
      throw new Error("只接受函數(shù)作為參數(shù)矿咕!")
    }
    fn(this.resolve.bind(this), this.reject.bind(this))
  }
  resolve(result) {
    if (this.state !== "pending") return;
    this.state = "fulfilled"
    nextTick(() => {
      //遍歷callbacks,調(diào)用所有的handle[0]
      this.callbacks.forEach(handle => {
        let x
        if (typeof handle[0] === "function") {
          try {
            x = handle[0].call(undefined, result)
          } catch (error) {
            handle[2].reject(error)
          }
        }
        handle[2].resolve(x)
      })
    })
  }
  reject(reason) {
    if (this.state !== "pending") return;
    this.state = "rejected"
    nextTick(() => {
      //遍歷callbacks,調(diào)用所有的handle[1]
      this.callbacks.forEach(handle => {
        let x
        if (typeof handle[1] === "function") {
          try {
            x = handle[1].call(undefined, reason)
          } catch (error) {
            handle[2].reject(error)
          }
        }
        handle[2].resolve(x)
      })
    })
  }
  then(succeed?, fail?) {
    const handle = []
    if (typeof succeed === "function") {
      handle[0] = succeed
    }
    if (typeof fail === "function") {
      handle[1] = fail
    }
    handle[2] = new Promise2(() => { })
    //把函數(shù)推到 callbacks 里面
    this.callbacks.push(handle)
    return handle[2]
  }
}

export default Promise2

function nextTick(fn) {
  if (process !== undefined && typeof process.nextTick === "function") {
    return process.nextTick(fn)
  } else {
    var counter = 1
    var observer = new MutationObserver(fn)
    var textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
      characterData: true
    });

    counter = counter + 1
    textNode.data = String(counter)
  }
}

代碼地址可查看:這里

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末碳柱,一起剝皮案震驚了整個(gè)濱河市捡絮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌莲镣,老刑警劉巖福稳,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異瑞侮,居然都是意外死亡的圆,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)半火,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)越妈,“玉大人,你說(shuō)我怎么就攤上這事钮糖∶仿樱” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵店归,是天一觀的道長(zhǎng)阎抒。 經(jīng)常有香客問(wèn)我,道長(zhǎng)消痛,這世上最難降的妖魔是什么且叁? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮秩伞,結(jié)果婚禮上逞带,老公的妹妹穿的比我還像新娘。我一直安慰自己纱新,他們只是感情好掰担,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著怒炸,像睡著了一般带饱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上阅羹,一...
    開(kāi)封第一講書(shū)人閱讀 51,182評(píng)論 1 299
  • 那天勺疼,我揣著相機(jī)與錄音,去河邊找鬼捏鱼。 笑死执庐,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的导梆。 我是一名探鬼主播轨淌,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼迂烁,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了递鹉?” 一聲冷哼從身側(cè)響起盟步,我...
    開(kāi)封第一講書(shū)人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎躏结,沒(méi)想到半個(gè)月后却盘,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡媳拴,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年黄橘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屈溉。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡塞关,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出子巾,到底是詐尸還是另有隱情帆赢,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布砰左,位于F島的核電站匿醒,受9級(jí)特大地震影響场航,放射性物質(zhì)發(fā)生泄漏缠导。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一溉痢、第九天 我趴在偏房一處隱蔽的房頂上張望僻造。 院中可真熱鬧,春花似錦孩饼、人聲如沸髓削。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)立膛。三九已至,卻和暖如春梯码,著一層夾襖步出監(jiān)牢的瞬間宝泵,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工轩娶, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留儿奶,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓鳄抒,卻偏偏與公主長(zhǎng)得像闯捎,于是被迫代替她去往敵國(guó)和親椰弊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353