今天,我们创建自己的 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
。
这可以是一个String 到Boolean ,然后它将采用 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
)
)
}
}
我们刚才做了什么?
好吧,让我们来分解一下。
-
- 当我们创建或实例化 Promise 时,我们定义了一个回调,它是我们成功解析结果时使用的 then 回调。
-
- 我们创建一个接受映射器函数的映射函数。这个映射函数返回一个新的承诺。在返回新的 Promise 之前,它会尝试解析先前 Promise 使用的结果。我们将
map
先前 Promise 的结果转换为新的 Promise,然后回到在我们的 map 方法中实例化的新创建的 Promise 的范围内。
- 我们创建一个接受映射器函数的映射函数。这个映射函数返回一个新的承诺。在返回新的 Promise 之前,它会尝试解析先前 Promise 使用的结果。我们将
-
.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
方法也称为chain
或bind
。我们刚刚构建的实际上称为任务,方法.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 实际上是某些计算上下文的具体化。