创建自己的 JavaScript Promise 来实现 [From Scratch]

今天,我们创建自己的 JavaScript Promise 实现 [From Scratch]。


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

  new Promise((resolve, reject) => {
    ...
    resolve(someValue)
  })

我们传递一个定义 Promise 特定行为的回调。

Promise 是一个容器:

  • 为我们提供 API 来管理和转换价值

  • 这让我们能够管理和转变实际上尚不存在的价值。

使用容器来包装值是函数式编程范例中的常见做法。函数式编程中有不同种类的“容器”。最著名的是函子和单子。


实施承诺以了解其内部结构


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)

非常简单,到目前为止,这个实现除了具有 success ( resolve) 和 error ( reject) 回调的任何函数之外,没有做任何其他事情。

因此,检查一下,当我们从头开始做出承诺时,我们有一个额外的(通常不公开的)步骤需要实施。

2. 映射

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

我们的实现目前缺少什么功能和/或行为之一?

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

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

这是让 Promise 如此强大的主要功能之一。它们帮助我们逃离回调地狱。

这也是我们目前尚未实现的 Promise 实现的一部分。在我们的实现中,结合使 Promise 链正常工作所需的所有功能可能会有点混乱 - 但我们得到了这一点。

让我们深入研究、简化并设置 JavaScript Promise 的实现,以始终从语句返回或解析其他 Promise .then()


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

嗯,这听起来是不是很奇怪?让我们仔细看看。

啊哈,这听起来和Array.prototype.map实现方式完全一样,不是吗?

.map的类型签名是:

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

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

这可以是一个StringBoolean ,然后它将采用a (字符串)的数组并返回b (布尔)的数组。

我们可以构建一个Promise.prototype.map具有非常相似签名的函数,该函数Array.prototype.map允许我们将已解决的 Promise 结果映射到另一个正在进行的 Promise。这就是我们能够链接.then's具有返回任何随机结果的回调函数的方式,但随后似乎神奇地以某种方式返回 Promise,而无需我们实例化任何新的 Promise。

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

以下是我们如何在幕后实现这一魔法:

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

我们刚才做了什么?


让我们来分解一下。

  • 当我们创建或实例化 Promise 时,我们定义了一个回调,它是我们成功解析结果时使用的 then 回调。
  • 我们创建一个接受映射器函数的映射函数。这个映射函数返回一个新的承诺。在返回新的 Promise 之前,它会尝试解析先前 Promise 使用的结果。我们将map先前 Promise 的结果转换为新的 Promise,然后回到在我们的 map 方法中实例化的新创建的 Promise 的范围内。
  • then我们可以继续这种模式,根据需要附加尽可能多的回调,并始终返回一个新的 Promise,而无需在我们的map方法之外外部实例化任何新的 Promise。
(resolve, reject) => this.then(...))

正在发生的事情是我们this.then立即打电话。thethis指的是我们当前的 Promise,因此this.then将为我们提供 Promise 当前的内部值,或者如果我们的 Promise 失败则给出当前的错误。我们现在需要给它一个resolve回调reject

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

这是我们的地图功能中最重要的部分。首先,我们用mapper当前值填充我们的函数x

promise.map(x => x + 1)// The mapper is actuallyx => x + 1// so when we domapper(10)// it returns 11.

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

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

  map(mapper) {
    return new Promise((resolve, reject) => this.then(
      x => resolve(mapper(x)),
      reject
    ))
  }
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'

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


多使用我们的 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)

就像这样,我们就被束缚了。我们链接的每个回调都是一个有点死的简单函数。

这就是为什么我们喜欢在函数式编程中进行柯里化。现在我们可以编写以下代码:

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

可以说,鉴于您更熟悉函数语法,您可以说这段代码更干净。另一方面,如果您不熟悉函数语法,那么这段代码会变得非常混乱。

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

步骤1:

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

第 2 步:.then现在:

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

第 3 步:then就是现在:

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

第 4步:

  .map(x => x.data)

第 5 步:

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

第 6 步:

  .map(items => items.filter(isEven))

第 7 步:

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

第 8 步:

  .map(items => items.sort(priceAsc))

第 9 步:

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

第 10 步:

  .then(renderPrices, console.error)

.then叫做。我们执行的代码如下所示:

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

3. 链接和flatMap()


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

当您在方法内返回另一个 Promise 时.then,它​​会等待它解析并将解析的值传递给下一个.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])

这是我们的签名细分,但如果很难理解,我建议您尝试多次追踪逻辑尾部,如果它没有点击,则尝试深入研究下面的直接实现。我们非常深入,并且没有函数式编程的经验,这种语法可能很难跟踪,但请尽力而为,让我们继续下面的内容。

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
      )
    )
  }}

我们知道 的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)

怎么样:)

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

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

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

flatMap方法也称为chainbind。我们刚刚构建的实际上称为任务,方法.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
    ))
  }}

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

这是什么意思?

由于任务是惰性的,我们的程序在调用fork/.then方法之前不会真正执行任何操作。

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

通过分离以 为特征的三种行为.then,使其变得懒惰,

仅仅通过分离 的三个行为.then,并使其变得懒惰,我们实际上用 20 行代码实现了400 多行 polyfill。不错吧?


总结一下


  • Promise 是保存值的容器 - 就像数组一样

  • .then具有三种行为特征(这就是它可能令人困惑的原因)

    • .then立即执行 Promise 的内部回调

    • .then编写一个函数,该函数获取 Promise 的未来值并进行转换,以便返回包含转换后的值的新 Promise

    • 如果您在方法中返回 Promise .then,它将像数组中的数组一样对待它,并通过展平 Promise 来解决此嵌套冲突,这样我们就不再在 Promise 中包含 Promise 并删除嵌套。


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


  • Promise 为您编写函数

    • 组合正确地分离了关注点。它鼓励您编写只做一件事的小函数(类似于单一职责原则)。因此,这些函数很容易理解和重用,并且可以组合在一起以实现更复杂的事情,而无需创建高度依赖的单个函数。

  • Promise 抽象了您正在处理异步值的事实。

  • Promise 只是一个可以在代码中传递的对象,就像常规值一样。这种将概念(在我们的例子中是异步,可能失败或成功的计算)转变为对象的概念称为具体化

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值