如何从零开始创建 JS Promise
你知道我有电子报吗?📬
如果您想在我发布新博客文章或
重大项目公告时收到通知,请访问
https://cleancodestudio.paperform.co/
幕后承诺
今天,我们将从零开始创建我们自己的 JavaScript Promise 实现。
要创建新的承诺,我们只需new Promise像这样使用:
new Promise((resolve, reject) => {
...
resolve(someValue)
})
我们传递一个回调函数,该函数定义了 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)
非常简单,到目前为止,这个实现并不比任何带有成功回调(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
简单来说,这意味着 map 接受一个函数并将类型转换a为类型b。
这可以是字符串到布尔值的转换,那么它将接受一个字符串数组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 回调函数,也就是在我们成功解析结果时使用的回调函数。
-
- 我们创建了一个 map 函数,它接受一个 mapper 函数作为参数。这个 map 函数返回一个新的 Promise。在返回新 Promise 之前,它会尝试解析前一个 Promise 的结果。我们将
map前一个 Promise 的结果放入一个新的 Promise 中,然后我们又回到了新创建的、在 map 方法中实例化的 Promise 的作用域内。
- 我们创建了一个 map 函数,它接受一个 mapper 函数作为参数。这个 map 函数返回一个新的 Promise。在返回新 Promise 之前,它会尝试解析前一个 Promise 的结果。我们将
-
- 我们可以继续这种模式,
.then根据需要添加任意数量的回调函数,并且始终返回一个新的 Promise,而无需在我们的map方法之外外部实例化任何新的 Promise。
- 我们可以继续这种模式,
(resolve, reject) => this.then(...))
现在的情况是,我们this.then立即调用了 `the`。`the`this指的是我们当前的 Promise,因此this.then它会返回 Promise 的当前内部值,如果 Promise 失败,则会返回当前错误。现在我们需要给它提供一个 `a`resolve和一个reject回调函数:
// next resolve =
x => resolve(mapper(x))
// next reject =
reject
这是我们 map 函数中最关键的部分。首先,我们将mapper当前值传递给函数x:
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 被拒绝,我们只需传递新的 reject 方法,而不对值进行任何修改。
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:
第一步:
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)
.then现在是:
then = (resolve, reject) => {
HTTP.get('/items', (err, body) => {
if (err) return reject(err)
resolve(JSON.parse(body))
})
}
步骤 3:
.map(x => x.data)
.then现在是:
then = (resolve, reject) => {
HTTP.get('/items', (err, body) => {
if (err) return reject(err)
resolve(JSON.parse(body).data)
})
}
第四步:
.map(items => items.filter(isEven))
.then现在是:
then = (resolve, reject) => {
HTTP.get('/items', (err, body) => {
if (err) return reject(err)
resolve(JSON.parse(body).data.filter(isEven))
})
}
步骤 6:
.map(items => items.sort(priceAsc))
.then现在是:
then = (resolve, reject) => {
HTTP.get('/items', (err, body) => {
if (err) return reject(err)
resolve(JSON.parse(body).data.filter(isEven).sort(priceAsc))
})
}
步骤 6:
.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 实现仍然缺少一些东西——链式调用。
当你在.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])
这是我们常用的解析方法,但如果难以理解,我建议您多尝试几次,追溯一下逻辑脉络。如果还是不明白,那就直接跳到下面的实现部分。我们讲得比较深入,如果您没有函数式编程经验,这种语法可能比较难懂,但请尽力尝试,我们继续往下看。
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 是一个容器,它实现了 a.map和一个.flatMap具有以下类型签名的方法:
map :: (a -> b) -> Monad a -> Monad b
flatMap :: (a -> Monad b) -> Monad a -> Monad b
该flatMap方法也称为chain或bind。我们刚刚构建的实际上称为 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
))
}
}
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你知道我有电子报吗?📬
如果您想在我发布新博客文章或
重大项目公告时收到通知,请前往: