如何做出 JS Promise [从头开始]

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


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

javascript 复制代码
  new Promise((resolve, reject) => {
    ...
    resolve(someValue)
  })

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

Promise 是一个容器:

  • 为我们提供 API 来管理和转换价值
  • 这让我们能够管理和转变实际上尚不存在的价值。

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


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


1、then()方法

javascript 复制代码
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的类型签名是:

rust 复制代码
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。

rust 复制代码
map :: (a -> b) -> Promise a -> Promise b

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

javascript 复制代码
class Promise 
{
  constructor(then) 
  {
    this.then = then
  }

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

我们刚才做了什么?


好吧,让我们来分解一下。

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

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

ini 复制代码
// next resolve =
x => resolve(mapper(x))

// next reject =
reject

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

scss 复制代码
promise.map(x => x + 1)
// The mapper is actually
x => x + 1
// so when we do
mapper(10)
// it returns 11.

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

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

javascript 复制代码
  map(mapper) {
    return new Promise((resolve, reject) => this.then(
      x => resolve(mapper(x)),
      reject
    ))
  }
javascript 复制代码
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来覆盖我们的函数。 这会将我们的值传递给映射器并解析返回的值。****resolve
x


多使用我们的 Promise 实现:


javascript 复制代码
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)

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

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

go 复制代码
getItems
  .map(JSON.parse)
  .map(prop('data'))
  .map(filter(isEven))
  .map(sort(priceAsc))
  .then(renderPrices, console.error)

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

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

步骤1:

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

第 2 步:.then现在:

scss 复制代码
then = (resolve, reject) => {
  HTTP.get('/items', (err, body) => {
    if (err) return reject(err)
    resolve(body)
  })
}
c 复制代码
  .map(JSON.parse)

.then就是现在:

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

步骤3:

ini 复制代码
  .map(x => x.data)

.then就是现在:

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

步骤4:

ini 复制代码
  .map(items => items.filter(isEven))

.then就是现在:

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

第6步:

ini 复制代码
  .map(items => items.sort(priceAsc))

.then就是现在:

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

第6步:

go 复制代码
  .then(renderPrices, console.error)

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

scss 复制代码
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:

ini 复制代码
[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])

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

javascript 复制代码
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函数。

ini 复制代码
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

rust 复制代码
map :: (a -> b) -> Monad a -> Monad b

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

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

javascript 复制代码
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 实际上是某些计算上下文的具体化。

s.juejin.cn/ds/ieyRoEGe...

相关推荐
轻口味2 分钟前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王37 分钟前
React Hooks
前端·javascript·react.js
迷途小码农零零发1 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
真滴book理喻4 小时前
Vue(四)
前端·javascript·vue.js
程序员_三木4 小时前
Three.js入门-Raycaster鼠标拾取详解与应用
开发语言·javascript·计算机外设·webgl·three.js
开心工作室_kaic6 小时前
springboot476基于vue篮球联盟管理系统(论文+源码)_kaic
前端·javascript·vue.js
川石教育6 小时前
Vue前端开发-缓存优化
前端·javascript·vue.js·缓存·前端框架·vue·数据缓存
搏博6 小时前
使用Vue创建前后端分离项目的过程(前端部分)
前端·javascript·vue.js
温轻舟6 小时前
前端开发 之 12个鼠标交互特效上【附完整源码】
开发语言·前端·javascript·css·html·交互·温轻舟
web135085886356 小时前
2024-05-18 前端模块化开发——ESModule模块化
开发语言·前端·javascript