逆向工程——理解 JavaScript 中的 Promises

2025-06-09

逆向工程——理解 JavaScript 中的 Promises

在Twitter上关注我,很高兴接受您对主题或改进的建议/Chris

我又来写一篇关于逆向工程的文章了。和上次一样,这篇文章的重点是提升你的技能。这次我们来聊聊 Promises,这是一种异步结构,可以让你以同步的方式查看代码。你可能会问,这是什么魔法?读一读,你就会明白的。

如果您错过了我关于该主题的第一篇文章,请点击此处,它是对单元测试库进行逆向工程:

回到本文。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')
    })
  })
})
Enter fullscreen mode Exit fullscreen mode

你说 3 级,我或许还能接受。相信我,你肯定不想接受 3 级或 11 级。这就是我们想要 Promises 的原因。

好的,给我看看

使用 Promises,你可以编写如下结构:

getData()
  .then(getMoreData)
  .then(geteEvenMoreData)
Enter fullscreen mode Exit fullscreen mode

第一次看到这个,我简直惊呆了,哇!一切都变了。我居然能一行一行地读下去,没有奇怪的制表符什么的,直接从头读起就行了。

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)
  )

Enter fullscreen mode Exit fullscreen mode

上面我们可以看到,Promise 接受一个工厂函数,该函数有两个参数resolvereject,这两个参数都是函数。当resolve被调用时then(),会调用函数中的第一个回调。相反,当reject被调用时,会调用 中的第二个回调then,并将其打印为错误。

我们还支持上一节中已经展示过的内容,称为链接,内容如下:

getData()
  .then(getMoreData)
  .then(geteEvenMoreData)
Enter fullscreen mode Exit fullscreen mode

从代码角度来看,我们可以看到调用会创建另一个 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)
  })

Enter fullscreen mode Exit fullscreen mode

我们链式调用的额外优势在于,我们可以对返回的数据进行操作,并将其直接发送到下一个函数。因此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)
  )
Enter fullscreen mode Exit fullscreen mode

调用reject会导致第二个回调then被调用。但我们还可以使用一个名为 的东西catch()。 的想法catch()是将其用作一个万能方法。现在,了解它的工作原理很重要。如果我们在then方法中已经有一个错误回调,catch就不会被调用。因此,像这样的构造将无法按预期工作:

getData()
  .then(getMoreData, console.error)
  .then(getEvenMoreData)
  .catch(err => console.error)
Enter fullscreen mode Exit fullscreen mode

我们最想要的很可能是这样的场景:

  1. 打电话
  2. 如果出错,由本地错误处理程序处理
  3. 在本地处理错误后,确保我们短路流量

那么实施起来就需要像这样:

getData()
  .then(getMoreData, (err) => {
    // handle error locally
    console.error(err);
    throw new Error(err);
  })
  .then(getEvenMoreData)
  .catch(err => console.error)
Enter fullscreen mode Exit fullscreen mode

如果您希望将其短路,上述代码将按预期工作。如果您不这样实现,链式 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));
Enter fullscreen mode Exit fullscreen mode

通过观察我们可以得出以下结论:

  • 是一个类,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);
  })
Enter fullscreen mode Exit fullscreen mode

在终端中运行此程序,我们得到:

错误回调

好的,到目前为止,我们已经支持了成功resolve()场景,其中我们使用了 a 中的第一个回调。现在我们希望支持调用,因此以下内容应该可以工作:then()reject()

new Swear((resolve, reject) => {
  reject('err')
})
Enter fullscreen mode Exit fullscreen mode

我们需要按以下方式更改代码才能使其工作:

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))
Enter fullscreen mode Exit fullscreen mode

运行上述代码,您将收到以下响应:

error error
Enter fullscreen mode Exit fullscreen mode

链接

至此,我们的基本构造已经可以正常工作了。我们做到了吗?

好吧,我们还有很长的路要走。之后我们应该支持链式调用,也就是说,我们应该支持像这样的代码:

const swear = new Swear((resolve) => {
  resolve('data');
})
  .then(data => {
    console.log('swear', data);
    return 'test';
  })
  .then(data => {
    console.log(data)
  })
Enter fullscreen mode Exit fullscreen mode

这种构造的整个想法是,我们可以从一个 Promise 中获取响应,并将其重塑为其他内容,就像上面的 where turn datainto一样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 */)
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

好的,我们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);
})
Enter fullscreen mode Exit fullscreen mode

让我们再试一次此代码:

我们可以在上面看到我们的两个.then()回调都被命中了。

实现 Catch

Catch 拥有以下能力:

  • then如果没有指定错误回调,则捕获错误
  • 如果回调内部发生异常,则与错误回调一起工作then

从哪里开始呢?嗯,添加一个catch()方法就是个不错的选择。

catch(fnCatch) {
  this._fnCatch = fnCatch;
}
Enter fullscreen mode Exit fullscreen mode

让我们想一想。它应该只在没有其他错误回调处理错误时被调用。它还应该知道错误是什么,无论错误发生在 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));
Enter fullscreen mode Exit fullscreen mode

从上面的代码可以看出,在then()方法中,如果错误尚未处理,我们会将错误传递给链中的下一个 Promise。

return new Swear((resolve) => {
  resolve(this._data)
}, !this.handled ? this._error : null)
Enter fullscreen mode Exit fullscreen mode

如果本地回调处理了错误,我们会认为该错误已被处理,如我们的_reject()方法所示:

_reject(err) {
  this._error = err;
  if(this._fnFailure) {
    this._fnFailure(err);
    this.handled = true;
  }
}
Enter fullscreen mode Exit fullscreen mode

最后,在我们的catch()方法中,我们都接收一个回调并调用所述回调,如果错误尚未处理,则会出现错误。

catch(fnCatch) {
  this._fnCatch = fnCatch;
  if (!this.handled && this._error && this._fnCatch) {
    this._fnCatch(this._error);
  }
}
Enter fullscreen mode Exit fullscreen mode

我们可以删除该_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));
Enter fullscreen mode Exit fullscreen mode

这看起来像是预期的那样,我们的本地错误处理了它并且我们的catch()方法从未被调用。

如果没有本地处理程序而只有catch()方法怎么办?

swear
.then(data => {
  console.log('swear', data);
  return 'test';
})
.then(data => {
  console.log(data);
})
.catch(err => console.error('Swear, catch all', err));
Enter fullscreen mode Exit fullscreen mode

我们就此打住吧..已经有很多见解了,我们不要把它写成一本书。

概括

总而言之,我们着手实现 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))
Enter fullscreen mode Exit fullscreen mode
鏂囩珷鏉ユ簮锛�https://dev.to/itnext/reverse-engineering-understand-promises-1jfc
PREV
适用于开源项目的终极(免费)CI/CD
NEXT
如何使用 React Testing Library 来测试组件表面