遇到这个问题,让我怀疑了一下我所理解的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() 可能就不会发现这个问题了,哈哈哈。

相关推荐
烛阴12 分钟前
UV Coordinates & Uniforms -- OpenGL UV坐标和Uniform变量
前端·webgl
姑苏洛言16 分钟前
扫码小程序实现仓库进销存管理中遇到的问题 setStorageSync 存储大小限制错误解决方案
前端·后端
烛阴27 分钟前
JavaScript 的 8 大“阴间陷阱”,你绝对踩过!99% 程序员崩溃瞬间
前端·javascript·面试
lh_12541 小时前
ECharts 地图开发入门
前端·javascript·echarts
jjw_zyfx1 小时前
成熟的前端vue vite websocket,Django后端实现方案包含主动断开websocket连接的实现
前端·vue.js·websocket
Mikey_n2 小时前
前台调用接口的方式及速率对比
前端
周之鸥2 小时前
使用 Electron 打包可执行文件和资源:完整实战教程
前端·javascript·electron
我爱吃朱肉2 小时前
HTMLCSS模板实现水滴动画效果
前端·css·css3
机器视觉知识推荐、就业指导2 小时前
开源QML控件:进度条滑动控件(含源码下载链接)
前端·qt·开源·qml
前端snow2 小时前
前端全栈第二课:用typeorm向数据库添加数据---一对多关系
前端·javascript