【问题标题】:Detecting "returned promise only" status of an async function检测异步函数的“仅返回承诺”状态
【发布时间】:2019-08-06 18:33:30
【问题描述】:

我有这样的情况:

async function thirdPartyCode(a) {
    if (a == ...) {
        return myPromiser(...)  // can allow and act on this
    }
    let b = await someoneElsesPromiserB(...)
    if (b == ...) {
        let c = await myPromiser(...)  // must error on this
        ...
    }
    let d = await someoneElsesPromiserD(...)
    let e = myPromiser(...)  // note no await
    return e  // can allow and act on this
}

作为 myPromiser() 的作者和这个 thirdPartyCode() 的调用者,我想检测 myPromiser() 的 promise 是否被用作异步函数的返回 promise。这是在这种特殊类型的异步函数调用上下文中使用它的唯一合法方式。当它在这个函数中时,它不能被等待,也不能附加 .then() 子句。

如果有办法知道“异步函数的主体何时真正完成”,那将是解决它的一个楔子。

(注意:这个问题中的奇怪限制是使用Emscripten Emterpreter 的副产品。当simulated pthreads 可通过 WebAssembly 工作人员/SharedArrayBuffer / 等获得时,这些限制可能(?)不需要适用。但在撰写本文时,默认情况下并未启用这些前沿的浏览器功能......所以这种不同寻常的愿望来自希望兼容的代码子集是合法的。)

【问题讨论】:

  • return myPromiser()return await myPromiser() 之间的语义差异不大。究竟是什么限制,在myPromiser() 之后 绝对不能发生什么?我认为唯一的可能性是出错,而不是打电话给myPromiser()
  • “return myPromiser() 和 return await myPromiser() 之间没有太大的语义差异” 我同意——正如我所说,理想情况下两者都可以——但这是在新旧浏览器中为 WASM 库的客户端精巧兼容的编码样式子集的尝试。 到底有什么限制。 会是一个冗长的解释,需要彻底了解emscripten_sleep_with_yield(),然后了解它的非常复杂的用法。
  • 试试我 :-) 你的用例是什么?
  • 好的,让我回顾一下:有一个库可以让您在浏览器中运行同步 Rebol 脚本,使用 webworkers 或异步解释器。它可以同步使用 (reb.Spell) 或异步使用 (reb.Promise),具体取决于 Rebol 脚本的功能。现在你正在为运行时编写一个扩展,它允许你从同步 Rebol 脚本回调 javascript(主要用于 IO),并且这个 js 可能是异步的(返回一个承诺),因此有必要暂停运行时。到目前为止,哇。我说对了吗?
  • 现在你想允许内部 javascript 在实际挂起时使用运行时(如reb.Spellreb.ArgRreb.Textreb.Buffer),所以只有同步函数将工作。 (我很难过任何功能都可以工作)。但是,您仍然希望允许从异步调用返回单个结果,您当前的解决方法是返回执行调用的函数?

标签: javascript promise async-await es6-promise


【解决方案1】:

更新 这种方法可以机械地工作,但不能在他们使用then()catch()await 时直接抛出自定义错误。他们只会得到一个更神秘的错误,例如object has no method .then()。请参阅来自@Bergi 的 cmets 建议没有办法给出“类似外观的承诺”,并且仍然能够从结果中看出承诺的起源。但是在答案中留下一些最初的注释以帮助说明实际的愿望是什么......

RE:“如果有办法知道‘异步函数的主体实际上何时完成’”

当返回的 promise 解决时,异步函数“实际上已完成”。如果您控制调用上下文和 myPromiser(),那么您 (er, me) 可以选择让 myPromiser() 不直接返回一个 Promise,而是一个类似 Promise 的对象,它可以记住您的工作打算在通话结束后执行。

将 memoization 设为 Error 子类似乎是一件好事——因此它可以识别调用堆栈,并可能暗示违规调用站点,例如示例中的 await myPromiser(...)

class MyFakePromise extends Error {
   memo  // capture of whatever MyPromiser()'s args were for
   constructor(memo) {
       super("You can only use `return myPromiser()` in this context")
       this.memo = memo
   }
   errorAndCleanup() {
       /* this.memo.cleanup() */  // if necessary
       throw this  // will implicate the offending `myPromiser(...)` callsite
   }
   // "Fake promise interface with .then() and .catch()
   // clauses...but you can still recognize it with `instanceof`
   // in the handler that called thirdPartyCode() and treat it
   // as an instruction to do the work." -- nope, doesn't work
   //
   then(handler) {  // !!! See UPDATE note, can't improve errors via .then()
       this.errorAndCleanup()
   }
   catch(handler) {  // !!! See UPDATE note, can't improve errors via .catch()
       this.errorAndCleanup()
   }
}

这为尝试实际使用它的任何人提供了所需的错误属性:

 > let x = new MyFakePromise(1020)
 > await x
 ** Uncaught (in promise) Error: You can only use `return myPromiser()` in this context

但如果它没有被使用而只是传递,你可以把它当作数据来对待。因此,您将在必须使用虚假承诺的调用上下文中执行类似的操作:

fake_promise_mode = true

thirdPartyCode(...)
   .then(function(result_or_fake_promise) {
       fake_promise_mode = false
       if (result_or_fake_promise instanceof MyFakePromise) {
          handleRealResultMadeFromMemo(result_or_fake_promise.memo)
       else
          handleRealResult(result_or_fake_promise)
   })
   .catch(function(error)) {
       fake_promise_mode = false
       if (error instanceof MyFakePromise)
           error.errorAndCleanup()
       throw error
   })

而 myPromiser() 会留意 flag 以知道它是否必须做出虚假的承诺:

function myPromiser(...) {
    if (fake_promise_mode) {
        return new MyFakePromise(...memoize args...)
    return new Promise(function(resolve, reject) {
        ...safe context for ordinary promising...
    })
}

【讨论】:

  • 我怀疑这行得通。当你 return MyFakePromiseasync function 中时,它仍然会尝试用它解析返回的 Promise 并调用 then 方法,就像你 await 它时一样。
  • @Bergi 啊,很好的观察。 :-/ 我在脑海中将 .then() 建模为仅限 Promise 的东西。但是您已经仔细阅读了它以理解这一点,那么这可以根据愿望进行调整吗?一件事是 .then() 可以知道我控制下的特殊调用站点正在传入的处理程序身份,并仅以某种方式允许 that 函数......处理程序上的某种白名单方法。
  • 不,async function 总是返回本机 Promise,我想不出一种方法来识别呼叫站点。也不能使用then 回调标识,你的假承诺永远不会看到。
  • 但是,由于您是thirdPartyCode 的调用者,因此您的一般方法非常好:只需让myPromiser() 返回某种代理值 - 它根本不需要是一个承诺- 当thirdPartyCode() 承诺以该代理值实现时,你运行你想要的东西。这也保证了只有一个值 - 第三方可以根据需要随时调用myPromiser(),但这毫无意义,因为它只能返回其中一个。
  • @Bergi 但是let good = function(x) {} 然后class Fake extends Error { constructor() { super("bad") } then(f) { if (f == good) {console.log("good")} else { throw this }}} 呢?如果你说 let p = new Fake; p.then(good) 没关系,而所有其他 then(function(x) {...}) 子句都会出错。由于我的呼叫站点是唯一进行此处理的呼叫站点,因此它似乎可以正常工作,并且您在等待/然后为其他所有人(如预期的那样)时会遇到错误(带有解释和适当的呼叫站点含义)。
【解决方案2】:

你的问题很复杂,我可能会在某些方面弄错。 但这里有 3-4 个想法可能会有所帮助。

想法 1

从“then”开始,您可以立即使用 Proxy 调用“handler”,它几乎禁止所有操作。 完成此操作后,您只需注意函数退出或抛出错误。 通过这种方式,您可以跟踪返回的值是否以任何方式实际使用。

但是,如果不使用返回的值 - 您将看不到它。 所以这允许这种用途:

    ... some code ...
    await myPromiser();         // << notice the return value is ignored
    ... some more code ...

如果这对您来说是个问题,那么这种方法只能起到部分作用。 但如果这是一个比你上次调用的问题 (let e = myPromiser(...)) 也没有用,因为之后可以忽略“e”。

下面,在此答案的末尾,javascript 代码成功区分了您的三种情况

想法 2

您可以在调用“thirdPartyCode”代码之前使用 Babel 对其进行检测。 如果需要,Babel 也可以在运行时使用。 有了它,您可以: 2.1 找出myPromise的所有用法并检查它是否合法。 2.2 在每次 await 或 '.then' 之后添加对某些标记函数的调用 - 这样您就可以使用选项 1 检测所有情况。

答案 3

如果您正在寻找一种方法来了解 Promise 是属于您的还是已解决 - 那么答案是“没有这样的方法”。 证明(以Chrome为例):

    let p = new Promise((resolve, reject)=>{
        console.log('Code inside promise');
        resolve(5);
    });
    p.then(()=>{
        console.log('Code of then')
    })
    console.log('Code tail');

    // Executed in Chrome:
    // Code inside promise
    // Code tail
    // Code of then

这告诉我们解析代码总是在当前调用上下文之外执行。 IE。我们可能一直期望从 Promise 内部调用 'resolve' 会导致立即调用所有订阅的函数, 但事实并非如此 - v8 将等到当前函数执行结束,然后才执行 then 处理程序。

想法 4(部分)

如果您想拦截对 SystemPromise.then 的所有调用并确定您的 Promiser 是否被调用 - 有一种方法:您可以用您的实现覆盖 Promise.then。

不幸的是,这不会告诉您异步功能是否结束。我已经尝试过使用它 - 请参阅下面我的代码中的 cmets。


答案 1 的代码:

    let mySymbol = Symbol();
    let myPromiserRef = undefined;

    const errorMsg = 'ANY CUSTOM MESSAGE HERE';
    const allForbiddingHandler = {
        getPrototypeOf:                 target => { throw new Error(errorMsg); },
        setPrototypeOf:                 target => { throw new Error(errorMsg); },
        isExtensible:                   target => { throw new Error(errorMsg); },
        preventExtensions:              target => { throw new Error(errorMsg); },
        getOwnPropertyDescriptor:       target => { throw new Error(errorMsg); },
        defineProperty:                 target => { throw new Error(errorMsg); },
        has:                            target => { throw new Error(errorMsg); },
        get:                            target => { throw new Error(errorMsg); },
        set:                            target => { throw new Error(errorMsg); },
        deleteProperty:                 target => { throw new Error(errorMsg); },
        ownKeys:                        target => { throw new Error(errorMsg); },
        apply:                          target => { throw new Error(errorMsg); },
        construct:                      target => { throw new Error(errorMsg); },
    };


    // We need to permit some get operations because V8 calls it for some props to know if the value is a Promise.
    // We tell it's not to stop Promise resolution sequence.
    // We also allow access to our Symbol prop to be able to read args data
    const guardedHandler = Object.assign({}, allForbiddingHandler, {
        get: (target, prop, receiver) => {
            if(prop === mySymbol)
                return target[prop];

            if(prop === 'then' || typeof prop === 'symbol')
                return undefined;

            throw new Error(errorMsg);
        },
    })

    let myPromiser = (...args)=> {
        let vMyPromiser = {[mySymbol]:[...args] };
        return new Proxy(vMyPromiser,guardedHandler);
        // vMyPromiser.proxy = new Proxy(vMyPromiser,guardedHandler);
        // vMyPromiser.then = ()=> {
        //     myPromiserRef = vMyPromiser;
        //     console.log('myPromiserThen - called!');
        //     return vMyPromiser.proxy;
        // }
        // return vMyPromiser;
    };

    let someArg = ['someArgs1', 'someArgs2'];

    const someoneElsesPromiserB = async(a)=>{
        return a;
    }

    const someoneElsesPromiserD = async(a)=>{
        return a;
    }

    async function thirdPartyCode(a) {
        console.log('CODE0001')
        if (a == 1) {
            console.log('CODE0002')
            return myPromiser(a, someArg)  // can allow and act on this
        }

        console.log('CODE0003')
        let b = await someoneElsesPromiserB(a)
        console.log('CODE0004')
        if (b == 2) {
            console.log('CODE0005')
            let c = await myPromiser(a, someArg)  // must error on this
            console.log('CODE0006')
            let x = c+5;    // <= the value should be used in any way. If it's not - no matter if we did awaited it or not.
            console.log('CODE0007')
        }
        console.log('CODE0008')
        let d = await someoneElsesPromiserD(a);
        console.log('CODE0009')
        let e = myPromiser(a, someArg)  // note no await
        console.log('CODE0010')
        return e  // can allow and act on this
    };


    // let originalThen = Promise.prototype.then;
    // class ReplacementForPromiseThen {
    //     then(resolve, reject) {
    //         //  this[mySymbol]
    //         if(myPromiserRef) {
    //             console.log('Trapped then myPromiser - resolve immediately');
    //             resolve(myPromiserRef.proxy);
    //             myPromiserRef = undefined;
    //         } else {
    //             console.log('Trapped then other - use System Promise');
    //             originalThen.call(this, resolve, reject);
    //         }
    //     }
    // }
    //
    // Promise.prototype.then = ReplacementForPromiseThen.prototype.then;

    (async()=>{
        let r;
        console.log('Starting test 1');
        r = await thirdPartyCode(1);
        console.log('Test 1 finished - no error, args used in myPromiser = ', r[mySymbol]);
        console.log("\n\n\n");

        console.log('Starting test 3');
        r = await thirdPartyCode(3);
        console.log('Test 3 finished - no error, args used in myPromiser = ', r[mySymbol]);
        console.log("\n\n\n");

        console.log('Starting test 2 - should see an error below');
        r = await thirdPartyCode(2);
    })();

【讨论】:

  • 嘿——据我现在看,方法#1 似乎按照你描述的方式工作!也许@Bergi 会觉得它很有趣或者有 cmets。鉴于我的一些函数没有返回结果......在平衡的情况下,最好放弃 .then()await 的专门错误,以便在 @ 的情况下得到错误987654326@...而且使用更简单的对象具有“更少的移动部件”。我会考虑一下,但你肯定提供了一个彻底的答案,值得为此付出代价!谢谢!
猜你喜欢
  • 2022-01-26
  • 1970-01-01
  • 1970-01-01
  • 2016-06-22
  • 1970-01-01
  • 2021-07-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多