如何做出 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...

相关推荐
敲代码的小吉米10 分钟前
前端上传el-upload、原生input本地文件pdf格式(纯前端预览本地文件不走后端接口)
前端·javascript·pdf·状态模式
da-peng-song27 分钟前
ArcGIS Desktop使用入门(二)常用工具条——数据框工具(旋转视图)
开发语言·javascript·arcgis
低代码布道师2 小时前
第五部分:第一节 - Node.js 简介与环境:让 JavaScript 走进厨房
开发语言·javascript·node.js
满怀10152 小时前
【Vue 3全栈实战】从响应式原理到企业级架构设计
前端·javascript·vue.js·vue
伟笑3 小时前
elementUI 循环出来的表单,怎么做表单校验?
前端·javascript·elementui
确实菜,真的爱3 小时前
electron进程通信
前端·javascript·electron
魔术师ID5 小时前
vue 指令
前端·javascript·vue.js
Clown955 小时前
Go语言爬虫系列教程 实战项目JS逆向实现CSDN文章导出教程
javascript·爬虫·golang
星空寻流年6 小时前
css3基于伸缩盒模型生成一个小案例
javascript·css·css3
waterHBO7 小时前
直接从图片生成 html
前端·javascript·html