为何从async函数返回无需等待的类Promise对象会被自动await?(Kysely场景求助)
兄弟我太懂你这个困惑了!我当初用Kysely的时候也踩过一模一样的坑,看到那个“don't await SelectQueryBuilder instances directly”的提示差点挠破头,给你好好掰扯清楚这事儿:
问题根源:async函数的thenable自动展开机制
JavaScript里的async函数有个很容易被忽略的特性:只要你返回的是一个带有.then()方法的对象(也就是thenable,不管是不是原生Promise),async函数都会自动把它当成Promise来“展开”——相当于在内部自动await这个对象,然后把它的resolve结果作为自己返回的Promise的最终值。
而Kysely的SelectQueryBuilder为了防止用户误操作,故意实现了thenable接口,但在它的.then()方法里直接抛出了你看到的那个错误,就是为了提醒你别直接await查询构建器,得调用.execute()或者.executeTakeFirst()来执行查询。
你第一个写法里,async函数返回了QueryBuilder实例(它是thenable),async函数就自动触发了它的.then()方法,直接触发了那个错误提示:
async function paginate() { const recs = await db.selectFrom().execute() // 返回的QueryBuilder是thenable,被async函数自动await return db.selectFrom().select().where(recs.map(a => a.id)) } // 调用时触发错误 const builder = await paginate()
为啥包个普通对象就没事?
因为你返回的是一个普通JavaScript对象,它本身没有.then()方法,不属于thenable。这时候async函数就不会多管闲事,直接把这个普通对象作为返回Promise的resolve值,你解构出.builder的时候拿到的就是原汁原味的QueryBuilder实例,自然不会触发那个错误:
async function paginate() { const recs = await db.selectFrom().execute() // 返回普通对象,非thenable,不会被自动展开 return { builder: db.selectFrom().select().where(recs.map(a => a.id)) } } const builder = (await paginate()).builder utilFunction(builder)
不用包对象的解决方案
如果你实在不想多套一层对象,给你两个可行的办法:
办法1:把async函数改成手动Promise链式调用
放弃async/await语法,用原生Promise的链式写法,这样可以精准控制返回值,避免自动展开thenable:
function paginate() { return db.selectFrom().execute().then(recs => { // 这里直接返回QueryBuilder,Promise会把它作为resolve值,不会自动展开 return db.selectFrom().select().where(recs.map(a => a.id)) }) } // 调用时正常await,拿到的就是QueryBuilder const builder = await paginate() utilFunction(builder)
办法2:用Promise.resolve()包装后返回
在async函数里,把QueryBuilder用一个已经resolved的Promise包一下,这样async函数会等待这个Promise完成,返回它的resolve值(也就是QueryBuilder),而不会去触发QueryBuilder自身的.then()方法:
async function paginate() { const recs = await db.selectFrom().execute() const builder = db.selectFrom().select().where(recs.map(a => a.id)) // 用Promise.resolve()包装后返回,避免async函数自动展开thenable return Promise.resolve(builder) } const builder = await paginate() utilFunction(builder)
额外小提醒
其实Kysely这么设计是为了帮你避免误操作——毕竟如果不小心await了QueryBuilder,你以为执行了查询,但其实啥都没干(还会抛出错误)。所以不管用哪种方法,记得最终还是要通过你的utilFunction或者直接调用.execute()来执行查询哦。
内容来源于stack exchange

