逆向工程——理解 JavaScript 中的 Promises
在Twitter上关注我,很高兴接受您对主题或改进的建议/Chris
我又来写一篇关于逆向工程的文章了。和上次一样,这篇文章的重点是提升你的技能。这次我们来聊聊 Promises,这是一种异步结构,可以让你以同步的方式查看代码。你可能会问,这是什么魔法?读一读,你就会明白的。
如果您错过了我关于该主题的第一篇文章,请点击此处,它是对单元测试库进行逆向工程:


逆向工程:如何用 JavaScript 构建测试库
Chris Noring 为 ITNEXT 撰稿 ・ 2019 年 7 月 18 日
回到本文。Promises。我们计划采用的方法是查看该构造的公共 API,尝试进行一些有根据的猜测,然后开始实现。我们希望能够了解其底层机制,并希望在此过程中能够更智能地实现它。
我们将介绍以下内容:
- 为什么要使用 Promise?这是你需要问自己的最重要的问题之一。我为什么要学习/阅读/使用它?
- Promises 的核心概念是什么
- 实施,我们将实施一个基本的 Promise,但我们也将支持所谓的链接
准备好?
好吧,我们就这么做吧。
为什么
因为 Promises 已经成为 Node.js 和 Web 的 JavaScript 标准的一部分,这意味着 promise 这个词已经被使用了。那么,有什么同义词呢?我脑子里第一个想到的词是swear,这让我回想起了 1994 年。
移动电话/手机看起来像这样:
MS-DOS 非常流行,每个人都在玩 DOOM 游戏,而当他们试图使用电话时,妈妈会因为你使用互联网而对你大喊大叫。;)
瑞典队在足球比赛中获得铜牌,对于所有英国人来说,这是我们的 1966 年。
哦,是的,All-4-One 的《I Swear》荣登榜首
哟,我来听听CODE老头子的事
是的,抱歉。好的。Promises 的优点在于,它允许你以一种看起来同步但实际上异步的方式安排代码。
为什么这样好?
考虑另一种回调地狱,如下所示:
getData((data) => {
getMoreData(data, (moreData) => {
getEvenMoreData(moreData, (evenMoreData) => {
console.log('actually do something')
})
})
})
你说 3 级,我或许还能接受。相信我,你肯定不想接受 3 级或 11 级。这就是我们想要 Promises 的原因。
好的,给我看看
使用 Promises,你可以编写如下结构:
getData()
.then(getMoreData)
.then(geteEvenMoreData)
第一次看到这个,我简直惊呆了,哇!一切都变了。我居然能一行一行地读下去,没有奇怪的制表符什么的,直接从头读起就行了。
Promises 已成为 Web 和 Node.js 的标准,我们不知道如果没有它该怎么办。
什么
让我们尝试确定我们对 Promises 的了解,以便我们可以重新创建它。
因此,通过 Promise,我们可以将所做的任何异步操作包装在 Promise 构造中,如下所示:
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
// do something
resolve('data')
}, 2000)
})
promise
.then(
data => console.log(data)
err => console.error(err)
)
上面我们可以看到,Promise 接受一个工厂函数,该函数有两个参数resolve
和reject
,这两个参数都是函数。当resolve
被调用时then()
,会调用函数中的第一个回调。相反,当reject
被调用时,会调用 中的第二个回调then
,并将其打印为错误。
我们还支持上一节中已经展示过的内容,称为链接,内容如下:
getData()
.then(getMoreData)
.then(geteEvenMoreData)
从代码角度来看,我们可以看到调用会创建另一个 Promise。到目前为止,我们已经提到以同步的方式查看异步then
代码很有用,但还有更多。
让我们通过创建上面提到的函数使上面的例子更加明确
function getData() {
return new Promise((resolve, reject) => {
resolve('data')
})
}
function getMoreData(data) {
return new Promise((resolve, reject) => {
resolve(data +' more data')
})
}
function getEvenMoreData(data) {
return new Promise((resolve, reject) => {
resolve(data + ' even more data')
})
}
function getMostData(data) {
return data + "most";
}
getData()
.then(getMoreData)
.then(getEvenMoreData)
.then(getMostData)
.then(data => {
console.log('printing', data)
})
我们链式调用的额外优势在于,我们可以对返回的数据进行操作,并将其直接发送到下一个函数。因此data
,可以将其作为参数发送,getMoreData()
并将其结果发送到下一个函数,依此类推。另外,请注意上面有一个名为 的方法getMostData()
,这里我们甚至没有构造一个新的 Promise,但只需从函数中返回一些内容并进行解析就足够了。
在开始实现之前,我们再提一下错误处理。现在,我们实际上已经展示了错误处理:
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
// do something
reject('error')
}, 2000)
})
promise
.then(
data => console.log(data)
err => console.error(err)
)
调用reject
会导致第二个回调then
被调用。但我们还可以使用一个名为 的东西catch()
。 的想法catch()
是将其用作一个万能方法。现在,了解它的工作原理很重要。如果我们在then
方法中已经有一个错误回调,catch
就不会被调用。因此,像这样的构造将无法按预期工作:
getData()
.then(getMoreData, console.error)
.then(getEvenMoreData)
.catch(err => console.error)
我们最想要的很可能是这样的场景:
- 打电话
- 如果出错,由本地错误处理程序处理
- 在本地处理错误后,确保我们短路流量
那么实施起来就需要像这样:
getData()
.then(getMoreData, (err) => {
// handle error locally
console.error(err);
throw new Error(err);
})
.then(getEvenMoreData)
.catch(err => console.error)
如果您希望将其短路,上述代码将按预期工作。如果您不这样实现,链式 Promise 实际上将继续执行getEvenMoreData
。
关于 Promises 工作原理的背景和见解已经足够了。接下来让我们尝试实现它们。
执行
当我自己进行这个练习时,我注意到 Promise 的含义比我们看到的要多得多。
实现 Promise 有很多事情要做
- 得到解决/拒绝工作 + 然后
- 链式承诺
- 错误处理,既有本地错误处理程序,也有捕获错误处理程序
- 确保我们在 then 回调中处理 Promise 和更简单的对象的返回
鉴于上述所有场景可能很容易变成一篇需要 20 分钟阅读的文章,我将尝试充分实施以获得有价值的见解。
承诺构建与解决/拒绝
Swear
我们说过,一旦开始实施,我们就会调用它。
好的,接下来是构建时间。让我们看看下面的构造,并尝试让它工作:
const promise = new Promise((resolve, reject) => {
resolve('data')
// reject('error')
})
promise
then(data => console.log(data));
通过观察我们可以得出以下结论:
- 是一个类,Promise 是一个类或者至少是一个构造函数
- 工厂函数输入
resolve
,Promise 采用具有两个输入参数和的工厂函数reject
。 resolve
方法应该触发then
回调
根据以上结论,我们来概括一下:
// remember, Promise = Swear
class Swear {
constructor(fn) {
this.fn = fn;
}
_resolve(data) {
this._fnSuccess(data);
}
then(fnSuccess) {
this._fnSuccess = fnSuccess;
this.fn(this._resolve.bind(this));
}
}
const swear = new Swear((resolve) => {
resolve('data');
})
.then(data => {
console.log('swear', data);
})
在终端中运行此程序,我们得到:
错误回调
好的,到目前为止,我们已经支持了成功resolve()
场景,其中我们使用了 a 中的第一个回调。现在我们希望支持调用,因此以下内容应该可以工作:then()
reject()
new Swear((resolve, reject) => {
reject('err')
})
我们需要按以下方式更改代码才能使其工作:
class Swear {
constructor(fn) {
this.fn = fn;
}
_resolve(data) {
this._fnSuccess(data);
}
_reject(err) {
this._fnFailure(err);
}
then(fnSuccess) {
this._fnSuccess = fnSuccess;
this.fn(this._resolve.bind(this), this._reject.bind(this));
}
}
const swear = new Swear((resolve) => {
reject('error');
})
.then(data => {
console.log('swear', data);
}, err => console.error(err))
运行上述代码,您将收到以下响应:
error error
链接
至此,我们的基本构造已经可以正常工作了。我们做到了吗?
好吧,我们还有很长的路要走。之后我们应该支持链式调用,也就是说,我们应该支持像这样的代码:
const swear = new Swear((resolve) => {
resolve('data');
})
.then(data => {
console.log('swear', data);
return 'test';
})
.then(data => {
console.log(data)
})
这种构造的整个想法是,我们可以从一个 Promise 中获取响应,并将其重塑为其他内容,就像上面的 where turn data
into一样test
。但是如何支持它呢?从上面的代码中,我们应该Swear
在调用时生成一个对象then()
,所以让我们添加这部分:
class Swear {
constructor(fn) {
this.fn = fn;
}
_resolve(data) {
this._fnSuccess(data);
}
then(fnSuccess) {
this._fnSuccess = fnSuccess;
this.fn(this._resolve.bind(this));
return new Swear((resolve) => {
resolve(/* something */)
})
}
}
好的,我们Swear
在 的末尾返回了实例then
,但我们需要给它一些数据。我们从哪里获取这些数据?实际上,它来自于调用this._fnSuccess
,我们在 中执行了此操作_resolve()
。因此,让我们在那里添加一些代码:
class Swear {
constructor(fn) {
this.fn = fn;
}
_resolve(data) {
this._data = this._fnSuccess(data);
}
then(fnSuccess) {
this._fnSuccess = fnSuccess;
this.fn(this._resolve.bind(this));
return new Swear((resolve) => {
resolve(this._data)
})
}
}
swear
.then(data => {
console.log('swear', data);
return 'test';
})
.then(data => {
console.log(data);
})
让我们再试一次此代码:
我们可以在上面看到我们的两个.then()
回调都被命中了。
实现 Catch
Catch 拥有以下能力:
then
如果没有指定错误回调,则捕获错误- 如果回调内部发生异常,则与错误回调一起工作
then
。
从哪里开始呢?嗯,添加一个catch()
方法就是个不错的选择。
catch(fnCatch) {
this._fnCatch = fnCatch;
}
让我们想一想。它应该只在没有其他错误回调处理错误时被调用。它还应该知道错误是什么,无论错误发生在 Promise 链的哪个阶段。
看看 Promise 链是如何工作的,错误似乎不会使链短路,这意味着如果我们保存错误并将其传递下去,应该没问题。我们还应该考虑在处理错误时引入某种“handled”的概念。
好的,下面是其全部实现:
class Swear {
constructor(fn, error = null) {
this.fn = fn;
this.handled = false;
this._error = error;
}
_resolve(data) {
this._data = this._fnSuccess(data);
}
_reject(err) {
this._error = err;
if(this._fnFailure) {
this._fnFailure(err);
this.handled = true;
}
}
then(fnSuccess, fnFailure) {
this._fnSuccess = fnSuccess;
this._fnFailure = fnFailure;
this.fn(this._resolve.bind(this), this._reject.bind(this));
return new Swear((resolve) => {
resolve(this._data)
}, !this.handled ? this._error : null)
}
catch(fnCatch) {
this._fnCatch = fnCatch;
if (!this.handled && this._error && this._fnCatch) {
this._fnCatch(this._error);
}
}
}
const swear = new Swear((resolve, reject) => {
reject('error');
})
swear
.then(data => {
console.log('swear', data);
return 'test';
} /*, err => console.error('Swear error',err)*/)
.then(data => {
console.log(data);
})
.catch(err => console.error('Swear, catch all', err));
从上面的代码可以看出,在then()
方法中,如果错误尚未处理,我们会将错误传递给链中的下一个 Promise。
return new Swear((resolve) => {
resolve(this._data)
}, !this.handled ? this._error : null)
如果本地回调处理了错误,我们会认为该错误已被处理,如我们的_reject()
方法所示:
_reject(err) {
this._error = err;
if(this._fnFailure) {
this._fnFailure(err);
this.handled = true;
}
}
最后,在我们的catch()
方法中,我们都接收一个回调并调用所述回调,如果错误尚未处理,则会出现错误。
catch(fnCatch) {
this._fnCatch = fnCatch;
if (!this.handled && this._error && this._fnCatch) {
this._fnCatch(this._error);
}
}
我们可以删除该_fnCatch()
方法并直接调用fnCatch
。
尝试一下
最大的问题是,它有效吗?
catch
好吧,让我们用本地回调和如下方法尝试一下:
swear
.then(data => {
console.log('swear', data);
return 'test';
} , err => console.error('Swear error',err))
.then(data => {
console.log(data);
})
.catch(err => console.error('Swear, catch all', err));
这看起来像是预期的那样,我们的本地错误处理了它并且我们的catch()
方法从未被调用。
如果没有本地处理程序而只有catch()
方法怎么办?
swear
.then(data => {
console.log('swear', data);
return 'test';
})
.then(data => {
console.log(data);
})
.catch(err => console.error('Swear, catch all', err));
我们就此打住吧..已经有很多见解了,我们不要把它写成一本书。
概括
总而言之,我们着手实现 Promise 的一部分及其一些功能,例如解析/拒绝、本地错误处理程序、链式调用和 catch-all。我们只用了几行代码就完成了这些,但我们也意识到还有一些地方需要改进,例如then()
在返回 Promise/Swear 时能够调用成功回调、在同一个回调或失败回调中抛出异常、处理 Promise.resolve、Promise.reject、Promise.all 和 Promise.any 等静态方法。好了,你懂的,这不是结束,而仅仅是开始。
我要给你们留下 All-4-One 的临别赠言
const swear = new Swear((resolve, reject) => {
resolve('I swear');
})
swear
.then(data => {
return `${data}, by the Moon`
})
.then(data => {
return `${data}, and the stars`
})
.then(data => {
return `${data}, and the sun`
})
.then(data => console.log(data))