发布于 2026-01-06 10 阅读
0

如何从零开始创建 JS Promise

如何从零开始创建 JS Promise

Twitter关注

你知道我有电子报吗?📬

如果您想在我发布新博客文章或
重大项目公告时收到通知,请访问
https://cleancodestudio.paperform.co/


幕后承诺


今天,我们将从零开始创建我们自己的 JavaScript Promise 实现。


要创建新的承诺,我们只需new Promise像这样使用:

  new Promise((resolve, reject) => {
    ...
    resolve(someValue)
  })
Enter fullscreen mode Exit fullscreen mode

我们传递一个回调函数,该函数定义了 Promise 的具体行为。

承诺是一个容器:

  • 为我们提供一个用于管理和转换值的 API
  • 这样我们就可以管理和转换那些实际上还不存在的值。

在函数式编程范式中,使用容器来包装值是一种常见的做法。函数式编程中有不同类型的“容器”,其中最著名的是函子(Functor)和单子(Monad)。


履行承诺,了解其内部运作机制


1. 该then()方法

class Promise 
{
   constructor (then) 
   {
      this.then = then
   }
}

const getItems = new Promise((resolve, reject) => {
  HTTP.get('/items', (err, body) => {
    if (err) return reject(err)
    resolve(body)
  })
})

getItems.then(renderItems, console.error)
Enter fullscreen mode Exit fullscreen mode

非常简单,到目前为止,这个实现并不比任何带有成功回调(resolve)和错误reject回调()的函数做更多的事情。

所以请注意,当我们从头开始做出承诺时,我们还需要额外执行一个通常不公开的步骤。

2. 地图绘制

目前,我们的 Promise 实现还无法正常工作——它过于简化,没有包含正常工作所需的所有行为。

我们的实现目前缺少哪些特性和/或行为?

首先,我们无法进行链式.then()调用。

Promise 可以链式调用多个.then()方法,并且每次当其中任何一个.then()语句的结果被解析时,都应该返回一个新的 Promise。

这是承诺如此强大的主要特性之一。它们能帮助我们摆脱回调地狱。

这也是我们目前尚未实现的 Promise 实现部分。要把所有必要的功能整合起来,让这个 Promise 链在我们的实现中正常工作,可能会有点复杂——但我们已经搞定了。

让我们深入探讨,简化并设置 JavaScript Promise 的实现,使其始终从.then()语句中返回或解析另一个 Promise。


首先,我们需要一个方法,能够转换 Promise 中包含的值,并返回一个新的 Promise。

嗯,这听起来是不是有点耳熟?让我们仔细看看。

啊哈,这听起来就像是Array.prototype.map完美地实现了目标——不是吗?

.map的类型签名是:

map :: (a -> b) -> Array a -> Array b
Enter fullscreen mode Exit fullscreen mode

简单来说,这意味着 map 接受一个函数并将类型转换a为类型b

这可以是字符串布尔值的转换,那么它将接受一个字符串数组a ,并返回一个布尔值数组b 。

我们可以构建一个Promise.prototype.map签名非常相似的函数,该函数Array.prototype.map允许我们将已解析的 Promise 结果映射到另一个后续的 Promise。这样,我们就可以链式调用那些.then's具有回调函数的函数,这些回调函数会返回任意随机结果,但似乎神奇地返回 Promise,而无需我们实例化任何新的 Promise。

map :: (a -> b) -> Promise a -> Promise b
Enter fullscreen mode Exit fullscreen mode

以下是我们幕后实现这一神奇效果的方法:

class Promise 
{
  constructor(then) 
  {
    this.then = then
  }

  map (mapper) 
  {
     return new Promise(
       (resolve, reject) => 
          this.then(x => resolve(mapper(x)), 
          reject
       )
     )
   }
}
Enter fullscreen mode Exit fullscreen mode

我们刚才做了什么?


好的,我们来详细分析一下。

    1. 当我们创建或实例化一个 Promise 时,我们定义了一个回调函数,即我们的 then 回调函数,也就是在我们成功解析结果时使用的回调函数。
    1. 我们创建了一个 map 函数,它接受一个 mapper 函数作为参数。这个 map 函数返回一个新的 Promise。在返回新 Promise 之前,它会尝试解析前一个 Promise 的结果。我们将map前一个 Promise 的结果放入一个新的 Promise 中,然后我们又回到了新创建的、在 map 方法中实例化的 Promise 的作用域内。
    1. 我们可以继续这种模式,.then根据需要添加任意数量的回调函数,并且始终返回一个新的 Promise,而无需在我们的map方法之外外部实例化任何新的 Promise。
(resolve, reject) => this.then(...))
Enter fullscreen mode Exit fullscreen mode

现在的情况是,我们this.then立即调用了 `the`。`the`this指的是我们当前的 Promise,因此this.then它会返回 Promise 的当前内部值,如果 Promise 失败,则会返回当前错误。现在我们需要给它提供一个 `a`resolve和一个reject回调函数:

// next resolve =
x => resolve(mapper(x))

// next reject =
reject

Enter fullscreen mode Exit fullscreen mode

这是我们 map 函数中最关键的部分。首先,我们将mapper当前值传递给函数x

promise.map(x => x + 1)
// The mapper is actually
x => x + 1
// so when we do
mapper(10)
// it returns 11.
Enter fullscreen mode Exit fullscreen mode

然后,我们将这个新值(11在本例中)直接传递给resolve我们正在创建的新 Promise 的函数。

如果 Promise 被拒绝,我们只需传递新的 reject 方法,而不对值进行任何修改。

  map(mapper) {
    return new Promise((resolve, reject) => this.then(
      x => resolve(mapper(x)),
      reject
    ))
  }
Enter fullscreen mode Exit fullscreen mode
const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve(10), 1000)
})

promise
  .map(x => x + 1)
// => Promise (11)
  .then(x => console.log(x), err => console.error(err))
// => it's going to log '11'
Enter fullscreen mode Exit fullscreen mode

总而言之,我们在这里所做的事情非常简单。我们只是用映射函数和下一个函数的组合resolve来重写我们的函数 这将把我们的值传递给映射器并解析返回值。resolve
x


再进一步了解我们的 Promise 实现:


const getItems = new Promise((resolve, reject) => {
  HTTP.get('/items', (err, body) => {
    if (err) return reject(err)
    resolve(body)
  })
})

getItems
  .map(JSON.parse)
  .map(json => json.data)
  .map(items => items.filter(isEven))
  .map(items => items.sort(priceAsc))
  .then(renderPrices, console.error)

Enter fullscreen mode Exit fullscreen mode

就这样,我们实现了链式调用。我们链式调用的每个回调函数都是一个非常简单且功能简单的函数。

这就是我们喜欢在函数式编程中使用柯里化的原因。现在我们可以编写以下代码:

getItems
  .map(JSON.parse)
  .map(prop('data'))
  .map(filter(isEven))
  .map(sort(priceAsc))
  .then(renderPrices, console.error)
Enter fullscreen mode Exit fullscreen mode

可以说,如果你更熟悉函数式编程语法,那么这段代码会显得更简洁。但如果你不熟悉函数式编程语法,那么这段代码可能会让你感到非常困惑。

为了更好地理解我们正在做的事情,让我们明确定义一下我们的.then()方法在每次调用时将如何转换.map

第一步:

new Promise((resolve, reject) => {
  HTTP.get('/items', (err, body) => {
    if (err) return reject(err)
    resolve(body)
  })
})
Enter fullscreen mode Exit fullscreen mode

步骤 2:.then现在是:

then = (resolve, reject) => {
  HTTP.get('/items', (err, body) => {
    if (err) return reject(err)
    resolve(body)
  })
}
Enter fullscreen mode Exit fullscreen mode
  .map(JSON.parse)
Enter fullscreen mode Exit fullscreen mode

.then现在是:

then = (resolve, reject) => {
  HTTP.get('/items', (err, body) => {
    if (err) return reject(err)
    resolve(JSON.parse(body))
  })
}
Enter fullscreen mode Exit fullscreen mode

步骤 3:

  .map(x => x.data)
Enter fullscreen mode Exit fullscreen mode

.then现在是:

then = (resolve, reject) => {
  HTTP.get('/items', (err, body) => {
    if (err) return reject(err)
    resolve(JSON.parse(body).data)
  })
}
Enter fullscreen mode Exit fullscreen mode

第四步:

  .map(items => items.filter(isEven))
Enter fullscreen mode Exit fullscreen mode

.then现在是:

then = (resolve, reject) => {
  HTTP.get('/items', (err, body) => {
    if (err) return reject(err)
    resolve(JSON.parse(body).data.filter(isEven))
  })
}
Enter fullscreen mode Exit fullscreen mode

步骤 6:

  .map(items => items.sort(priceAsc))
Enter fullscreen mode Exit fullscreen mode

.then现在是:

then = (resolve, reject) => {
  HTTP.get('/items', (err, body) => {
    if (err) return reject(err)
    resolve(JSON.parse(body).data.filter(isEven).sort(priceAsc))
  })
}
Enter fullscreen mode Exit fullscreen mode

步骤 6:

  .then(renderPrices, console.error)
Enter fullscreen mode Exit fullscreen mode

.then被调用。我们执行的代码如下所示:

HTTP.get('/items', (err, body) => {
  if (err) return console.error(err)
  renderMales(JSON.parse(body).data.filter(isEven).sort(priceAsc))
})
Enter fullscreen mode Exit fullscreen mode

3. 连锁和flatMap()


我们的 Promise 实现仍然缺少一些东西——链式调用。

当你在.then方法内部返回另一个 Promise 时,它​​会等待该 Promise 解析完成,并将解析后的值传递给下一个.then内部函数。

这是如何实现的?在 Promise 中,.then它还会将 Promise 容器扁平化。数组的类比是 flatMap:

[1, 2, 3, 4, 5].map(x => [x, x + 1])
// => [ [1, 2], [2, 3], [3, 4], [4, 5], [5, 6] ]

[1, 2 , 3, 4, 5].flatMap(x => [x, x + 1])
// => [ 1, 2, 2, 3, 3, 4, 4, 5, 5, 6 ]

getPerson.flatMap(person => getFriends(person))
// => Promise(Promise([Person]))

getPerson.flatMap(person => getFriends(person))
// => Promise([Person])
Enter fullscreen mode Exit fullscreen mode

这是我们常用的解析方法,但如果难以理解,我建议您多尝试几次,追溯一下逻辑脉络。如果还是不明白,那就直接跳到下面的实现部分。我们讲得比较深入,如果您没有函数式编程经验,这种语法可能比较难懂,但请尽力尝试,我们继续往下看。


class Promise 
{
  constructor(then) 
  {
    this.then = then
  }

  map(mapper) 
  {
    return new Promise(
      (resolve, reject) => this.then(
         x => resolve(mapper(x)),
         reject
      )
     )
  }

  flatMap(mapper) {
    return new Promise(
      (resolve, reject) => this.then(
         x => mapper(x).then(resolve, reject),
         reject
      )
    )
  }
}

Enter fullscreen mode Exit fullscreen mode

我们知道flatMap映射函数会返回一个 Promise。当我们获取到值 x 时,我们调用映射函数,然后通过调用.then返回的 Promise 来转发 resolve 和 reject 函数。


getPerson
  .map(JSON.parse)
  .map(x => x.data)
  .flatMap(person => getFriends(person))
  .map(json => json.data)
  .map(friends => friends.filter(isMale))
  .map(friends => friends.sort(ageAsc))
  .then(renderMaleFriends, console.error)

Enter fullscreen mode Exit fullscreen mode

怎么样?:)

我们通过分离 Promise 的不同行为,实际上创建了一个 Monad。

简单来说,monad 是一个容器,它实现了 a.map和一个.flatMap具有以下类型签名的方法:

map :: (a -> b) -> Monad a -> Monad b

flatMap :: (a -> Monad b) -> Monad a -> Monad b
Enter fullscreen mode Exit fullscreen mode

flatMap方法也称为chainbind。我们刚刚构建的实际上称为 Task,该.then方法通常命名为fork


class Task 
{
  constructor(fork) 
  {
    this.fork = fork
  }

  map(mapper) 
  {
    return new Task((resolve, reject) => this.fork(
      x => resolve(mapper(x)),
      reject
    ))
  }

  chain(mapper) 
  {
    return new Task((resolve, reject) => this.fork(
      x => mapper(x).fork(resolve, reject),
      reject
    ))
  }
}

Enter fullscreen mode Exit fullscreen mode

Task 和 Promise 的主要区别在于 Task 是惰性的,而 Promise 不是。

这是什么意思?

由于 Task 是惰性的,fork所以除非你调用/方法,否则我们的程序实际上不会执行任何操作.then

由于 Promise 不是惰性的,即使实例化时其.then方法从未被调用,内部函数仍会立即执行。

通过将三种行为区分开来.then,使其变得懒惰,

仅仅通过分离这三种行为.then,并使其惰性化,我们实际上用 20 行代码实现了400 多行的 polyfill

还不错吧?


总结起来


  • Promise 就像数组一样,是用来保存值的容器。
  • .then它具有三种特征性行为(这也是它容易让人困惑的原因)
    • .then立即执行 Promise 的内部回调函数
    • .then编写一个函数,该函数接受 Promise 的未来值并进行转换,以便返回一个包含转换后值的新 Promise。
    • 如果在方法内部返回 Promise .then,它会将其视为数组中的数组,并通过扁平化 Promise 来解决嵌套冲突,从而消除 Promise 嵌套,避免嵌套现象。

为什么这是我们想要的行为(为什么这种行为是好的?)


  • 承诺为你构建功能

    • 组合能够有效地分离关注点。它鼓励你编写只做一件事的小函数(类似于单一职责原则)。因此,这些函数易于理解和重用,并且可以组合起来实现更复杂的功能,而不会创建高度依赖的独立函数。
  • Promise 将你正在处理的是异步值这一事实抽象化了。

  • Promise 只是一个对象,你可以像传递普通值一样在代码中传递它。这种将概念(在本例中是异步操作,即可能成功或失败的计算)转化为对象的过程称为物化

  • 这也是函数式编程中常见的模式。Monad 实际上是对某些计算上下文的物化。


Clean Code Studio
Clean Code
JavaScript 算法示例
JavaScript 数据结构


你知道我有电子报吗?📬

如果您想在我发布新博客文章或
重大项目公告时收到通知,请前往:

文章来源:https://dev.to/cleancodestudio/this-is-how-to-implement-javascript-promises-from-scratch-357k