有没有办法同步解决承诺? (或可以的替代库)

Is there any way to resolve a promise synchronously? (or an alternative library that can)

我有一个验证字符串的方法,我希望该方法 return Promise 因为验证是 运行 可能 是异步的。然而,我遇到的问题是性能之一,我希望承诺尽可能在同一个事件循环中解决(例如:当没有异步验证要完成时)但我希望接口保持一致(例如:始终return 一个承诺)。

下面的简化代码示例说明了我正在尝试做的事情,但它会导致上述性能损失,因为即使可以同步执行验证,它仍然会等待下一个事件循环来处理结果。

在我的特定用例中,此性能损失太高。

下面是我正在做的一个简化(最小)示例

// Array containing validation methods
const validations = [
  (value) => true, // Some validation would happen here
];
// Array containing asynchronous validation methods
const asyncValidations = []; // No async validations (but there could be)
const validate(value){
  // Run synchronous validations
  try {
    validations.forEach(validation => validation(value));
  catch(error){
    // Synchronous validation failed
    return Promise.reject();
  }
  if(asyncValidations){
    return Promise.all(asyncValidations.map(validation => validation(value));
  }
  // Otherwise return a resolved promise (to provide a consistent interface)
  return Promise.resolve(); // Synchronous validation passed 
}

// Example call
validate('test').then(() => {
  // Always asynchronously called
});

你提到了两件事:

  1. I want the interface to remain consistent

  2. [I want to] always return a Promise

如果你想避免不需要的异步行为,你可以这样做并保持 API 一致。但是你不能做的是 "always return a Promise" 因为不可能 "resolve a promise synchronously".

您的代码目前 return 是一个在不需要异步验证时解析的 Promise:

// Otherwise return a resolved promise (to provide a consistent interface)
return Promise.resolve(); // Synchronous validation passed

您可以用以下代码替换该代码:

return {then: cb => cb()};

请注意,这只是 return 一个对象文字,它是 "thenable"(即它有一个 then 方法)并且将 同步 执行你传递给它的任何回调。然而,它不是return一个承诺。

您还可以通过实施 then method and/or the the catch 方法的可选 onRejected 参数来扩展此方法。

从技术上讲,当 returns 承诺或其他内容时,可以以完全相同的方式访问函数:

function test(returnPromise=false) {
    return returnPromise ? new Promise(resolve=>resolve('Hello asynchronous World!')) : 'Hello synchronous World!'
}

async function main() {
    const testResult1 = await test(false)
    console.log(testResult1)
    const testResult2 = await test(true)
    console.log(testResult2)
}

main().catch(console.error)

尽管如此,您必须将所有代码放入任何异步函数中。但是你可以只使用 await,不管函数 returns 是否是一个 promise。

承诺异步解决的原因是它们不会炸毁堆栈。考虑以下使用承诺的堆栈安全代码。

console.time("promises");

let promise = Promise.resolve(0);

for (let i = 0; i < 1e7; i++) promise = promise.then(x => x + 1);

promise.then(x => {
    console.log(x);
    console.timeEnd("promises");
});

如您所见,即使它正在创建 1000 万个中间承诺对象,它也不会炸毁堆栈。但是,因为它在下一个报价时处理每个回调,所以在我的笔记本电脑上大约需要 5 秒来计算结果。您的里程可能会有所不同。

你能在不影响性能的情况下实现堆栈安全吗?

是的,你可以但不能承诺。 Promise 不能同步解决,期间。因此,我们需要一些其他的数据结构。以下是一种此类数据结构的实现。

// type Unit = IO ()

// data Future a where
//     Future       :: ((a -> Unit) -> Unit) -> Future a
//     Future.pure  :: a -> Future a
//     Future.map   :: (a -> b) -> Future a -> Future b
//     Future.apply :: Future (a -> b) -> Future a -> Future b
//     Future.bind  :: Future a -> (a -> Future b) -> Future b

const Future =     f  => ({ constructor: Future,          f });
Future.pure  =     x  => ({ constructor: Future.pure,     x });
Future.map   = (f, x) => ({ constructor: Future.map,   f, x });
Future.apply = (f, x) => ({ constructor: Future.apply, f, x });
Future.bind  = (x, f) => ({ constructor: Future.bind,  x, f });

// data Callback a where
//     Callback       :: (a -> Unit) -> Callback a
//     Callback.map   :: (a -> b) -> Callback b -> Callback a
//     Callback.apply :: Future a -> Callback b -> Callback (a -> b)
//     Callback.bind  :: (a -> Future b) -> Callback b -> Callback a

const Callback =     k  => ({ constructor: Callback,          k });
Callback.map   = (f, k) => ({ constructor: Callback.map,   f, k });
Callback.apply = (x, k) => ({ constructor: Callback.apply, x, k });
Callback.bind  = (f, k) => ({ constructor: Callback.bind,  f, k });

// data Application where
//     InFuture :: Future a -> Callback a -> Application
//     Apply    :: Callback a -> a -> Application

const InFuture = (f, k) => ({ constructor: InFuture, f, k });
const Apply    = (k, x) => ({ constructor: Apply,    k, x });

// runApplication :: Application -> Unit
const runApplication = _application => {
    let application = _application;
    while (true) {
        switch (application.constructor) {
            case InFuture: {
                const {f: future, k} = application;
                switch (future.constructor) {
                    case Future: {
                        application = null;
                        const {f} = future;
                        let async = false, done = false;
                        f(x => {
                            if (done) return; else done = true;
                            if (async) runApplication(Apply(k, x));
                            else application = Apply(k, x);
                        });
                        async = true;
                        if (application) continue; else return;
                    }
                    case Future.pure: {
                        const {x} = future;
                        application = Apply(k, x);
                        continue;
                    }
                    case Future.map: {
                        const {f, x} = future;
                        application = InFuture(x, Callback.map(f, k));
                        continue;
                    }
                    case Future.apply: {
                        const {f, x} = future;
                        application = InFuture(f, Callback.apply(x, k));
                        continue;
                    }
                    case Future.bind: {
                        const {x, f} = future;
                        application = InFuture(x, Callback.bind(f, k));
                        continue;
                    }
                }
            }
            case Apply: {
                const {k: callback, x} = application;
                switch (callback.constructor) {
                    case Callback: {
                        const {k} = callback;
                        return k(x);
                    }
                    case Callback.map: {
                        const {f, k} = callback;
                        application = Apply(k, f(x));
                        continue;
                    }
                    case Callback.apply: {
                        const {x, k} = callback, {x: f} = application;
                        application = InFuture(x, Callback.map(f, k));
                        continue;
                    }
                    case Callback.bind: {
                        const {f, k} = callback;
                        application = InFuture(f(x), k);
                        continue;
                    }
                }
            }
        }
    }
};

// inFuture :: Future a -> (a -> Unit) -> Unit
const inFuture = (f, k) => runApplication(InFuture(f, Callback(k)));

// Example:

console.time("futures");

let future = Future.pure(0);

for (let i = 0; i < 1e7; i++) future = Future.map(x => x + 1, future);

inFuture(future, x => {
    console.log(x);
    console.timeEnd("futures");
});

如您所见,性能比使用 promises 好一点。在我的笔记本电脑上大约需要 4 秒。你的旅费可能会改变。不过更大的好处是每个回调都是同步调用的。

解释这段代码的工作原理超出了这个问题的范围。我尽量把代码写得干净利落。阅读它应该会提供一些见解。

至于我是怎么想到写这样的代码的,我是从下面的程序开始的,然后手工进行了一堆编译器优化。我执行的优化是 defunctionalization and tail call optimization via trampolining.

const Future = inFuture => ({ inFuture });
Future.pure = x => Future(k => k(x));
Future.map = (f, x) => Future(k => x.inFuture(x => k(f(x))));
Future.apply = (f, x) => Future(k => f.inFuture(f => x.inFuture(x => k(f(x)))));
Future.bind = (x, f) => Future(k => x.inFuture(x => f(x).inFuture(k)));

最后,我鼓励您查看 Fluture 库。它做类似的事情,具有实用功能来转换承诺和从承诺中转换,允许您取消期货,并支持顺序期货和并行期货。