遇到这个问题,让我怀疑了一下我所理解的JavaScript

有了前面的《await 到底在等什么?》打底之后,我们就可以来聊一聊最近工作中遇到的关于 mongoose 4.x 版本关于 findOneAndUpdate 这个 API 的兜底 exec 逻辑了。

问题

事情是这样的,最近在做一个 egg 的中间件,这个中间件有一个数据收集的功能,需要将数据存储到 mongoDB 数据库中。但是我对于 mongoose 的语句不是很熟悉,所以直接将需求丢给了 GPT,它帮我写了个例子:

js 复制代码
try {
  // 查询条件
  const query = { name: 'John' }
  // 更新的数据
  const update = { age: 30 }
  // 设置选项,使用 upsert 选项来指定在数据不存在时进行插入操作
  const options = { upsert: true, new: true, setDefaultsOnInsert: true }

  // 执行 findOneAndUpdate 操作
  const result = await ExampleModel.findOneAndUpdate(
    query,
    update,
    options
  ).exec()

  console.log('Result:', result)
} catch (error) {
  console.error('Error:', error)
}

然后我简单改造简化了一下,代码如下:

js 复制代码
const fn = async () => {
  const query = { name: 'John' }
  const update = { age: 30 }
  const options = { upsert: true, new: true, setDefaultsOnInsert: true }
  ExampleModel.findOneAndUpdate(query, update, options)
}
fn()

根据我们的需求,我们只是做一个数据收集 和记录功能,所以并不需要等待数据库处理完成,于是去掉了 await。但其实这不是最致命的,最致命的是我不小心将 .exec() 去掉了

我第一次测试,debugger 发现 findOneAndUpdate 方法执行了,但是数据库并没有数据插入。然后简单对了一下,加上了 await(此时还是没有发现 .exec() 没加上),然后再次测试,发现,诶,数据库数据写入成功了。

也就是说,下面这份代码,就添加了一个 await,诶,数据它就插进去了

js 复制代码
const fn = async () => {
  const query = { name: 'John' }
  const update = { age: 30 }
  const options = { upsert: true, new: true, setDefaultsOnInsert: true }
  await ExampleModel.findOneAndUpdate(query, update, options)
}
fn()

所以到这里我是十分不能理解的,因为根据我们的认知,await 是绝对不可能会影响 ExampleModel.findOneAndUpdate 方法的调用结果的 。但是从目前的现象上来看:好像确实是影响到了,没加 await 数据没入库;加了 await 数据就入库了

这就有点像杨氏双缝干涉实验:观察的时候是粒子性,不观察的时候是波动性;我们这里:等待就落库,不等待就不落库。

当然根据我们对于 JavaScript 最基本的语法了解,这是不可能的。

源码分析(4.x 版本)

通过查看源码,可以发现 ExampleModel.findOneAndUpdate 实际上执行的是 lib/model.js 文件中的 Model.findOneAndUpdate 方法;而 Model.findOneAndUpdate 方法又会调用 lib/query.js 文件中的 Query.prototype.findOneAndUpdate

ExampleModel.findOneAndUpdate -> Model.findOneAndUpdate -> Query.prototype.findOneAndUpdate

而真正执行数据落库操作的逻辑在 Query.prototype.findOneAndUpdate 方法中。通过调试可以发现,添加了 await Query.prototype.findOneAndUpdate 方法会被调用两次,而没有添加 await 只会被调用一次。调用一次是很容易理解的,其实就是通过我们上面的调用链路传递过来的。

我们先来看看 Query.prototype.findOneAndUpdate 方法:

js 复制代码
Query.prototype.findOneAndUpdate = function (criteria, doc, options, callback) {
  // 其他代码省略

  if (!callback) {
    return this
  }
  return this._findOneAndUpdate(callback) // 真正落库操作
}

无论是否添加 await, 第一次调用 callback 都是 undefined,所以 Query.prototype.findOneAndUpdate 的调用结果其实就是 this。调到这里我还是一脸懵逼。

最后通过调用链路追踪,发现添加 await 之后,罪魁祸首在这里:

js 复制代码
Query.prototype.then = function (resolve, reject) {
  return this.exec().then(resolve, reject)
}

mongoose 它实现了 then 方法,到这里我相信你应该就很好理解:为什么加 await 数据就入库了。其实结合我们前面的《await 到底在等什么?》await 实际上会去调用对象的 then 方法,那么这里的 then 方法里面执行了 this.exec()

也就是 mongoosethen 方法里面做了一个兜底 exec() 逻辑 ,所以我们添加 await,才能插入数据成功。

总结

仔细分析一下,问题其实还是很简单的,只是因为 findOneAndUpdate 方法被调用时,如果不传 callback 参数(我们只传了前三个参数)会直接 return this,而 this 就是 Query 对象;但同时 Query 对象又实现了 then 方法,而在 then 方法中又进行了 exec() 这个兜底逻辑

所以这体现出来的现象却是十分不可思议的:没加 await 数据没入库,加了 await 数据就入库了。通过这个现象,我们很容易会认为 await 影响了 findOneAndUpdate 方法的执行结果,但根据我们对于 JavaScript 语法的认知,这是绝对不可能的

不过总的来说,如果当时没有忘记加 .exec() 可能就不会发现这个问题了,哈哈哈。

相关推荐
Ticnix35 分钟前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人38 分钟前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl41 分钟前
OpenClaw 深度技术解析
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人1 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼1 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空1 小时前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust
Mr Xu_1 小时前
Vue 3 中计算属性的最佳实践:提升可读性、可维护性与性能
前端·javascript
jerrywus1 小时前
我写了个 Claude Code Skill,再也不用手动切图传 COS 了
前端·agent·claude
玖月晴空1 小时前
探索关于Spec 和Skills 的一些实战运用-Kiro篇
前端·aigc·代码规范