检测异步功能的“仅已兑现承诺”状态

时间:2019-03-15 16:11:41

标签: javascript promise async-await es6-promise

我遇到这样的情况:

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()的诺言是否用作异步函数的返回诺言。这是在这种特定类型的异步函数的调用上下文中使用的唯一合法方法。不能等待它,也不能在它位于此函数内时附加.then()子句。

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

(注意:此问题的奇怪局限性是使用Emscripten Emterpreter的副产品。当simulated pthreads可通过WebAssembly worker获得时,这些限制可能不需要(?)/ SharedArrayBuffer等。但是在编写本文时,这些尖端的浏览器功能默认未启用...因此,这种不寻常的愿望来自于希望兼容的代码子集合法。)

2 个答案:

答案 0 :(得分:1)

  

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

RE:“如果有一种方法可以知道'异步功能的主体何时实际上完成了'”

异步函数的返回承诺解析后,它们“实际上已完成”。如果您控制调用上下文和myPromiser(),那么您(er,me)可以选择使myPromiser()不直接返回promise-而是类似于Promise的对象,用于记录您的工作打算在通话结束后进行。

使该回忆成为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()会留意该标志,以了解是否必须做出虚假承诺:

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

答案 1 :(得分:1)

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

想法1

从“然后”中,您可以使用代理立即调用“处理程序”,该代理几乎禁止对其执行任何操作。 完成此操作后,您只需要等待函数退出或抛出错误即可。 这样,您就可以跟踪返回的值是否实际以任何方式使用。

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

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

如果这对您来说是个问题,则此方法只能部分帮助。 但是,如果这是一个问题,那么比您上一次调用(让e = myPromiser(...))还没有用,因为之后可以忽略“ e”。

下面,此答案的javascript代码可成功区分您的三种情况

想法2

您可以在调用Babel之前对其“ thirdPartyCode”代码进行检测。 如果需要,Babel也可以在运行时使用。 有了它,您可以: 2.1只需查找myPromise的所有用法,然后检查其是否合法。 2.2在每次等待或'.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

这告诉我们,解析代码始终在当前调用上下文之外执行。 即我们可能一直期望从Promise内部调用“ resolve”会导致立即调用所有已订阅的函数, 但这不是-v8会等到当前函数执行结束后才执行,然后执行处理程序。

想法4(部分)

如果您想截获对SystemPromise.then的所有调用,然后决定是否调用了Promiser,则有一种方法:您可以使用实现覆盖Promise.then。

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


答案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);
    })();