手写一个简易的Promise

1. 简述 Promise

所谓 Promise,简单来说,就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。
Promise 对异步调用进行封装,是一种异步编程的解决方案。
从语法上来说,Promise 是一个对象,从它可以获取异步操作的消息。

1.1 解决什么问题

有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数,即回调地狱。

1.2 优点
  • 减少缩进
    让回调函数变成了规范的链式写法,程序流程可以看得很清楚。
改写前:
f1( xxx , function f2(a){
  f3( yyy , function f4(b){
      f5( a + b , function f6(){})
  })
})

改写后:
f1(xxx)
   .then(f2)    // f2 里面调用f3
   .then(f4)    // f4 里面调用f5,注意,f2 的输出作为 f4 的输入,即可将 a 传给 f4
   .then(f6)
  • 消灭 if (error)的写法
    为多个回调函数中抛出的错误,统一指定处理方法。

而且,Promise 还有一个传统写法没有的好处:它的状态一旦改变,无论何时查询,都能得到这个状态。

1.3 用法
function fn(){
  //new Promise 接受一个函数,返回一个Promise实例
  return new Promise(( resolve, reject ) => {
       resolve()   // 成功时调用
       reject()     // 失败时调用
  })
} 

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

new Promise 接受一个函数,返回一个 Promise 实例

1.4 完整API

Promise是一个类

  • JS里类是特殊的函数
  • 类属性:length(可以忽略)
    永远是1,因为构造函数只接受一个参数
  • 类方法:all / allSettled / race / reject/ resolve
  • 对象属性:then / finally / catch
  • 对象内部属性:state = pending / fulfilled / rejected

API 的规则是? Promise / A+规格文档 (JS 的 Promise的公开标准,中文翻译 笔者不保证其准确性)

1.5 其他

Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。
状态具有不受外界影响和不可逆2个特点。

  • 不受外界影响
    指只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

  • 不可逆
    一旦状态改变,就不会再变化,会一直保持这个结果,称为 resolved(已定型),任何时候都可以得到这个结果。

2. 写之前的准备工作

2.1 创建目录
promise-demo
    src
        promise.ts
    test
        index.ts
2.2 测试驱动开发

按照规范文档写测试用例,
测试失败 -> 改代码 -> 测试成功 -> 加测试 -> ...

引入chaisinon (测试框架)
普通的测试用 chai就够用了, sinon是用于测试函数的库。

  • chai 安装步骤
yarn global add ts-node mocha

//初始化
yarn init -y

yarn add chai mocha --dev

//添加TypeScript的类型声明文件
yarn add @types/chai @types/mocha --dev

//为了使用 yarn test 将以下两个安装到本地
yarn add --dev ts-node
yarn add --dev typescript

修改 package.json文件:
添加 test命令,这样就不用每次执行的时候都用 mocha -r ts-node/register test/**/*.ts命令,可以直接使用 yarn test 进行测试。

"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. 具体实现

3.1 new Promise() 必须接受一个函数作为参数

测试代码:

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

const assert = chai.assert

describe("Promise", () => {
  it("是一个类", () => {
    assert.isFunction(Promise)
    assert.isObject(Promise.prototype)
  })
  it("new Promise() 如果接受的不是一个函数就会报错", () => {
    //assert.thow(fn)的作用:如果fn报错,控制台就不报错;如果fn不报错,控制台就报错。
    //即,预测fn会报错
    assert.throw(() => {
      // @ts-ignore
      new Promise()
    })
    assert.throw(() => {
      //@ts-ignore
      new Promise(1)
    })
    assert.throw(() => {
      //@ts-ignore
      new Promise(false)
    })
  })
})

assert.thow(fn)的作用:如果 fn报错,控制台就不报错;如果 fn不报错,控制台就报错。
即,预测 fn 会报错。

实现代码:

class Promise2 {
  constructor(fn) {
    if (typeof fn !== "function") {
      throw new Error("只接受函数作为参数!")
    }
  }
}
export default Promise2

测试通过。

测试结果.PNG
3.2 new Promise(fn) 会生成一个对象,对象有 then 方法
test/index.ts

it("new Promise(fn)会生成一个对象,对象有 then 方法", () => {
    const promise = new Promise(() => { })
    assert.isObject(promise)
    assert.isFunction(promise.then)
 })

src/promise.ts

添加
then() {}
3.3 new Promise(fn)中的 fn 会立即执行

如何判断一个函数会立即执行? => sinon提供了简便的方法

使用sinon :

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

chai.use(sinonChai)
it("new Promise(fn)中的 fn 会立即执行", () => {
   //sinon提供了一个假的函数,这个假的函数知道自己有没被调用
    let fn = sinon.fake()
    new Promise(fn)
   //如果这个函数被调用了,called 属性就为 true
    assert(fn.called)
 })
3.4 new Promise(fn)中的 fn 执行的时候必须接受 resolve 和 reject 两个函数
it("new Promise(fn)中的 fn 执行的时候必须接受 resolve 和 reject 两个函数", done => {
    new Promise((resolve, reject) => {
      assert.isFunction(resolve)
      assert.isFunction(reject)
      done()
    })
  })

关于done :因为有可能这两个语句根本没有执行,测试也会通过,所以使用 done 。用于保证 只有在运行 assert.isFunction(resolve); assert.isFunction(reject)之后才会结束这个测试用例。

3.5 promise.then(success)中的 success 会在 resolve 被调用的时候执行
it("promise.then(success)中的 success 会在 resolve 被调用的时候执行", done => {
    let success = sinon.fake()
    const promise = new Promise((resolve, reject) => {
      assert.isFalse(success.called)
      resolve()
      //先等resolve里的success执行
      setTimeout(() => {
        assert.isTrue(success.called)
        done()
      })
    })
    promise.then(success)
  })

then的时候,是先把 success 保存下来,等fn 调用 resolve的时候,resolve就会调用 success。(异步调用)
resolve需要先等一会,等 success先传入。

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

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

promise.then(nulll,fail) 处的代码类似,不再说明。

3.6 参考文档写测试用例

promisethen 方法接收两个参数:

promise.then(onFulfilled, onRejected)
  • onFulfilledonRejected 都是可选的参数,此外,如果参数不是函数,必须忽略
then(succeed?, fail?) {
    if (typeof succeed === "function") {
      this.succeed = succeed
    }
    if (typeof fail === "function") {
      this.fail = fail
    }
  }
  • 如果 onFulfilled 是函数:
    此函数必须在 promise 完成(fulfilled)后被调用,并把 promise 的值(resolve接收的参数)作为onFulfilled它的第一个参数;
    此函数不能被调用超过一次
resolve(result) {
    if (this.state !== "pending") return;
    this.state = "fulfilled"
    nextTick(() => {
      if (typeof this.succeed === "function") {
        this.succeed(result)
      }
    })
  }

onRejected 类似,不再说明。

  • then 可以在同一个 promise 里被多次调用
    promise变为 fulfilled ,各个相应的 onFulfilled 回调 必须按照最原始的 then 顺序来执行
    即传的是 0 1 2,调用的时候的顺序就是0 1 2

将目前的代码进行修改,目前的 then只保存一个 succeed 和 一个 fail,但实际上有可能会调用多次。

  resolve(result) {
    if (this.state !== "pending") return;
    this.state = "fulfilled"
    nextTick(() => {
      //遍历callbacks,调用所有的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
    }
    //把函数推到 callbacks 里面
    this.callbacks.push(handle)
  }
  • then必须返回一个promise (便于使用链式调用)
    需要创建新的 Promise 实例来对第二个then 中接收的 succeedfail进行存储并执行
    在原本的 resolvereject 函数中,执行第二个 Promise 实例的resolve方法
    参数传递
it("2.2.7 then必须返回一个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,调用所有的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(() => { })
    //把函数推到 callbacks 里面
    this.callbacks.push(handle)
    return handle[2]
  }

再添加错误处理进行完善。

4. 手写Promise完整代码

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

  constructor(fn) {
    if (typeof fn !== "function") {
      throw new Error("只接受函数作为参数!")
    }
    fn(this.resolve.bind(this), this.reject.bind(this))
  }
  resolve(result) {
    if (this.state !== "pending") return;
    this.state = "fulfilled"
    nextTick(() => {
      //遍历callbacks,调用所有的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,调用所有的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(() => { })
    //把函数推到 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)
  }
}

代码地址可查看:这里

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354