逐步递进地手写一个Promise

我看网上的代码都是一步到位的,很少有人会解释某一行代码是怎么来的,这会对新手同学造成很大困扰。咱既然要教,那就把逻辑和道理一步步给说明白,让读者看一遍两遍就能完全搞懂。我将用功能依次递进的三个版本来阐述一个比较完整的 Promise 代码是怎么来的。

版本一

编码前想想 Promise 的规则
  1. Promise 是一个构造函数,其入参是一个函数execute,这个函数的参数也是两个函数(resolve, reject)。
  2. Promise 返回一个对象,这个对象有一个 then 函数,这个函数的参数也是两个函数(onFulfilled, onRejected)。
  3. Promise 是一个状态机,初始状态是 pending,只有调用了 resolve / reject 后,才会变成 fulfilled / rejected,且状态不可逆。
根据以上规则,写出如下源代码
function myPromise(execute) {
  this.status = "pending"
  this.value = null
  this.reason = null

  const resolve = value => {
    if (this.status === "pending") {
      this.value = value
      this.status = "fulfilled"
    }
  }

  const reject = reason => {
    if (this.status === "pending") {
      this.reason = reason
      this.status = "rejected"
    }
  }
  
  execute(resolve, reject)
}

myPromise.prototype.then = function (onFulfilled, onRejected) {
  onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value
  onRejected = typeof onRejected === "function" ? onRejected : reason => reason

  if (this.status === "fulfilled") {
    onFulfilled(this.value)
  }

  if (this.status === "rejected") {
    onRejected(this.reason)
  }
}

// 测试代码
new myPromise((resolve, reject) => {
  resolve("hello wsx")
})
.then(res => {
  console.log(res)
})

// 测试结果
// hello wsx
但它不是完美的,它有什么问题?

至此,我们实现了一个"尝鲜版"的 Promise,它显然是不完美的,它有什么问题?我们尝试执行以下测试代码

new myPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('hello wsx');
  }, 1000)
}).then(res => {
  console.log(res)
})

// 测试结果
// 什么也没返回

执行代码完毕后得知,这个版本的 Promise 只能执行同步代码,因为如果换成异步的(如代码中的 setTimeout),那么内部执行顺序就变为是:execute > then > setTimeout。其中在 then 步骤,根据我们上面写的源代码得知,此时的 Promise 状态还没有变成 fulfilled,所以它无法通过判断,也就无法执行 then 的回调函数,也就无法打印 res。

版本二

在步骤一的基础上继续添加规则
  1. 发布订阅。知道了版本一的缺陷,我们不能马上执行 then 的回调 onFulfilled / onRejected ,需要把它收集起来,等到 execute 调用了 resolve / reject 执行完毕后,才统一执行。
  2. onFulfilled 和 onRejected 应该是微任务
开始写代码

更新位置有3处,其中前2处是新增,第3处是改动,读者也应该按照这三个顺序来理解版本二的变动内容,可先看代码再看以下描述。

第1处,添加了两个数组,是为了实现发布订阅用。如何订阅,如何发布,请看下2处描述。

第2处,新增了 pending 判断,版本一的缺点就是如果 excute 的 resolve 是异步回调的,那么执行了 then 时,此时的状态还是 pending,需要在这个阶段把 onFulfilled / onRejected 收集(订阅)起来。

第3处,改动了 resolve / reject 的函数提,将原来的逻辑套在微任务里(queueMicrotask),并且在最后遍历新增的两个函数,即发布。

function myPromise(execute) {
  this.status = "pending"
  this.value = null
  this.reason = null

  /** 1 */
  this.onFulfilledArray = []
  this.onRejectedArray = []

  const resolve = value => {
    /** 3 */
    queueMicrotask(() => {
      if (this.status === "pending") {
        this.value = value
        this.status = "fulfilled"
        this.onFulfilledArray.forEach(func => func(value))
      }
    })
  }

  const reject = reason => {
    /** 3 */
    queueMicrotask(() => {
      if (this.status === "pending") {
        this.reason = reason
        this.status = "rejected"
        this.onRejectedArray.forEach(func => func(value))
      }
    })
  }
  
  execute(resolve, reject)
}

myPromise.prototype.then = function (onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value
    onRejected = typeof onRejected === "function" ? onRejected : reason => reason

  if (this.status === "fulfilled") {
    onFulfilled(this.value)
  }

  if (this.status === "rejected") {
    onRejected(this.reason)
  }

  /** 2 */
  if (this.status === "pending") {
    this.onFulfilledArray.push(onFulfilled)
    this.onRejectedArray.push(onRejected)
  }
}

至此,我们再去执行版本一的测试代码(异步那个),就没有问题了。

但这不是我们的最终版本

它仍然存在问题,我准备了两段测试代码,其中代码1执行正常,代码2执行异常。异常的原因是,不支持链式调用。然而 Promise 的一大爽点就是链式调用,你可以在 new 了一个 Promise 之后,再紧接着使用 then、catch、finally 等一系列操作。

// 测试代码1
// 使用一个变量保存 Promise, 再分别执行 then
const p = new myPromise((resolve, reject) => {
    setTimeout(() => {
        resolve('hello wsx');
    }, 1000)
})

p.then(res => {
    console.log(res)
})

p.then(res => {
    console.log(res)
})

// 测试结果,输出了两次,正常
// hello wsx
// hello wsx


// 测试代码2
// 链式调用
new myPromise((resolve, reject) => {
    setTimeout(() => {
        resolve('hello wsx');
    }, 1000)
})
.then(res => {
    console.log(res)
})
.then(res => {
    console.log(res)
})

// 测试结果
// Uncaught TypeError: Cannot read properties of undefined (reading 'then')
// hello wsx

版本三

再增规则
  1. 链式调用。执行完 then 后,应该返回一个 新的 Promise。这表示你需要再 new 一个 Promise 并返回出去,这是一定要的,因为根据版本一的第3点描述:Promise 是一个状态机,且不可逆。
  2. 新 Promise 的 resolve / reject,也需要推到微任务中执行。
根据新增的规则,我们写下最终代码

其中涉及到两处改动,已经使用注释标了出来,读者直接看改动部分即可,主要还是针对新增的两条规则做的适配。

function myPromise(execute) {
  this.status = "pending"
  this.value = null
  this.reason = null

  this.onFulfilledArray = []
  this.onRejectedArray = []

  const resolve = value => {
    queueMicrotask(() => {
      if (this.status === "pending") {
        this.value = value
        this.status = "fulfilled"
        this.onFulfilledArray.forEach(func => func(value))
      }
    })
  }

  const reject = reason => {
    queueMicrotask(() => {
      if (this.status === "pending") {
        this.reason = reason
        this.status = "rejected"
        this.onRejectedArray.forEach(func => func(value))
      }
    })
  }
  
  execute(resolve, reject)
}

myPromise.prototype.then = function (onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value
    onRejected = typeof onRejected === "function" ? onRejected : reason => reason

  if (this.status === "fulfilled") {
    /** 1 */
    return new myPromise((resolve, reject) => {
      queueMicrotask(() => {
        try {
          const result = onFulfilled(this.value)
          resolve(result)
        }
        catch(err) {
          reject(err)
        }
      })
    })
  }

  if (this.status === "rejected") {
    /** 1 */
    return new myPromise((resolve, reject) => {
      queueMicrotask(() => {
        try {
          const result = onRejected(this.value)
          resolve(result)
        }
        catch(err) {
          reject(err)
        }
      })
    })
  }

  if (this.status === "pending") {
    /** 2 */
    return new myPromise((resolve, reject) => {
      this.onFulfilledArray.push(() => {
        queueMicrotask(() => {
          try {
            const result = onFulfilled(this.value)
            resolve(result)
          }
          catch(err) {
            reject(err)
          }
        })
      })  
    })
    
    this.onRejectedArray.push(() => {
      queueMicrotask(() => {
        try {
          const result = onRejected(this.value)
          resolve(result)
        }
        catch(err) {
          reject(err)
        }
      })
    })
  }
}

这样,我们再套用版本二最后的测试代码,就能正常执行了,现在的版本3已经支持链式调用。到目前为止,已经实现了功能递进的三个版本,已经对 Promise 有了 80%的理解,其实已经足够了,不管是应对面试,还是在工作中使用,都是搓搓有余的。

剩下的 20% 是什么?

当然肯定还有读者会问,剩下的20%是什么?其实剩下的是一个规范问题,读者可自行上网搜索有关 Promise/A+ 的内容,这就是我所说的20%,简单来说就是一些大家需要约定俗成的东西,或者一些约束。比如版本一中的第3点

Promise 是一个状态机,初始状态是 pending,只有调用了 resolve / reject 后,才会变成 fulfilled / rejected,且状态不可逆。

就是一个规范 ,它就是一个状态机,且状态不可逆,所以你在源码就得这么实现。

还有 then 返回的仍是一个 Promise(resolve, reject),如果调用 resolve 时传入参数也是一个 Promise,那么该如何如何遵循规则。

这些都是属于规范的东西,有兴趣的读者可以读一下相关内容。

相关推荐

  1. 逐步递进一个Promise

    2024-01-09 10:56:02       32 阅读
  2. jsPromise.prototype.finally

    2024-01-09 10:56:02       19 阅读
  3. js实现 Promise.all

    2024-01-09 10:56:02       22 阅读
  4. 一个vuex?

    2024-01-09 10:56:02       37 阅读
  5. 面试第二期 Promsie相关

    2024-01-09 10:56:02       25 阅读
  6. 一个线程池

    2024-01-09 10:56:02       18 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-01-09 10:56:02       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-01-09 10:56:02       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-01-09 10:56:02       19 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-01-09 10:56:02       20 阅读

热门阅读

  1. 探索 GitHub:高效使用技巧与实例分享

    2024-01-09 10:56:02       40 阅读
  2. git常用指令及应用案例

    2024-01-09 10:56:02       43 阅读
  3. 程序员必备的面试技巧

    2024-01-09 10:56:02       36 阅读
  4. Django创建RSS订阅

    2024-01-09 10:56:02       36 阅读
  5. 网络协议到底是什么?

    2024-01-09 10:56:02       38 阅读
  6. js中window的OPen方法,弹窗的特征

    2024-01-09 10:56:02       34 阅读
  7. 每日算法打卡:激光炸弹 day 8

    2024-01-09 10:56:02       32 阅读
  8. 【python】神经网络

    2024-01-09 10:56:02       38 阅读