如何在恢复函数之前等待 JavaScript 承诺解析



我正在做一些单元测试。测试框架将页面加载到 iFrame 中,然后针对该页面运行断言。 在每个测试开始之前,我创建一个Promise,它将 iFrame 的 onload 事件设置为调用resolve(),设置 iFrame 的src,并返回承诺。

因此,我可以调用 loadUrl(url).then(myFunc) ,它将等待页面加载,然后再执行任何myFunc

我在测试中到处使用这种模式(不仅仅是为了加载 URL),主要是为了允许对 DOM 进行更改(例如,模仿单击按钮,并等待div 隐藏和显示)。

这种设计的缺点是,我经常编写带有几行代码的匿名函数。 此外,虽然我有一个解决方法(QUnit 的assert.async()),但定义承诺的测试函数在承诺运行之前完成。

我想知道是否有任何方法可以从Promise中获取值或等待(块/睡眠)直到它得到解决,类似于.网IAsyncResult.WaitHandle.WaitOne() . 我知道 JavaScript 是单线程的,但我希望这并不意味着函数不能产生。

本质上,有没有办法让以下内容以正确的顺序吐出结果?

function kickOff() {
  return new Promise(function(resolve, reject) {
    $("#output").append("start");
    
    setTimeout(function() {
      resolve();
    }, 1000);
  }).then(function() {
    $("#output").append(" middle");
    return " end";
  });
};
function getResultFrom(promise) {
  // todo
  return " end";
}
var promise = kickOff();
var result = getResultFrom(promise);
$("#output").append(result);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="output"></div>

我想知道是否有任何方法可以从承诺或等待(阻塞/睡眠)直到它解决,类似于 。网的IAsyncResult.WaitHandle.WaitOne().我知道 JavaScript 是单线程,但我希望这并不意味着函数不能屈服。

浏览器中的当前一代Javascript没有允许其他东西运行的wait()sleep()。 所以,你根本无法做到你的要求。 相反,它具有异步操作,这些操作将完成它们的事情,然后在完成后调用您(就像您一直在使用 promise 一样)。

部分原因是因为Javascript的单线程性。 如果单个线程正在旋转,则在该旋转线程完成之前,没有其他 Javascript 可以执行。 ES6 引入了yield和生成器,它们将允许一些这样的合作技巧,但我们距离能够在广泛的已安装浏览器中使用这些技巧还有很长的路要走(它们可以用于某些服务器端开发中,您可以控制正在使用的 JS 引擎)。

<小时 />

仔细管理基于 promise 的代码可以控制许多异步操作的执行顺序。

不确定我是否确切地理解您尝试在代码中实现的顺序,但是您可以使用现有的kickOff()函数执行类似操作,然后在调用它后向其附加一个.then()处理程序:

function kickOff() {
  return new Promise(function(resolve, reject) {
    $("#output").append("start");
    
    setTimeout(function() {
      resolve();
    }, 1000);
  }).then(function() {
    $("#output").append(" middle");
    return " end";
  });
}
kickOff().then(function(result) {
    // use the result here
    $("#output").append(result);
});

这将以保证的顺序返回输出 - 如下所示:

start
middle
end

2018年更新(本答案撰写三年后):

如果您转译代码或在支持 ES7 功能(如 asyncawait)的环境中运行代码,您现在可以使用 await 使代码"出现"以等待承诺的结果。 它仍在有希望地编程。 它仍然没有阻止所有的Javascript,但它确实允许你用更友好的语法编写顺序操作。

而不是 ES6 的做事方式:

someFunc().then(someFunc2).then(result => {
    // process result here
}).catch(err => {
    // process error here
});

您可以这样做:

// returns a promise
async function wrapperFunc() {
    try {
        let r1 = await someFunc();
        let r2 = await someFunc2(r1);
        // now process r2
        return someValue;     // this will be the resolved value of the returned promise
    } catch(e) {
        console.log(e);
        throw e;      // let caller know the promise was rejected with this reason
    }
}
wrapperFunc().then(result => {
    // got final result
}).catch(err => {
    // got error
});

async函数在其函数体内击中第一个await后立即返回承诺,因此对于调用方来说,异步函数仍然是非阻塞的,调用方仍然必须处理返回的承诺并从该承诺中获取结果。 但是,在 async 函数中,您可以使用 await on promise 编写更类似于顺序的代码。 请记住,await只有在等待承诺时才做一些有用的事情,因此为了使用async/await,您的异步操作必须都是基于承诺的。

如果使用 ES2016,您可以使用 asyncawait 并执行以下操作:

(async () => {
  const data = await fetch(url)
  myFunc(data)
}())

如果使用ES2015,则可以使用生成器。如果您不喜欢该语法,可以使用 async 实用程序函数将其抽象出来,如此处所述。

如果使用 ES5,您可能需要像 Bluebird 这样的库来为您提供更多控制。

最后,如果您的运行时支持 ES2015,则可以使用提取注入并行保留执行顺序。

另一种选择是使用 Promise.all 等待一系列承诺解决,然后对这些承诺采取行动。

下面的代码显示了如何等待所有承诺解决,然后在它们全部准备就绪后处理结果(因为这似乎是问题的目标);同样出于说明目的,它显示了执行期间的输出(在中间之前结束)。

function append_output(suffix, value) {
  $("#output_"+suffix).append(value)
}
function kickOff() {
  let start = new Promise((resolve, reject) => {
    append_output("now", "start")
    resolve("start")
  })
  let middle = new Promise((resolve, reject) => {
    setTimeout(() => {
      append_output("now", " middle")
      resolve(" middle")
    }, 1000)
  })
  let end = new Promise((resolve, reject) => {
    append_output("now", " end")
    resolve(" end")
  })
  Promise.all([start, middle, end]).then(results => {
    results.forEach(
      result => append_output("later", result))
  })
}
kickOff()
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Updated during execution: <div id="output_now"></div>
Updated after all have completed: <div id="output_later"></div>

我制作了一个很小的包装器/内核,以事件时尚的方式处理异步/非异步回调依赖项(这对我的自我理解非常有趣)。

在引擎盖下,内核执行小但不太容易理解的 Promise 链接逻辑。

它创建一个事件对象,该对象可用于沿(屏蔽的)承诺链传递应用上下文。

function timeout(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}
// your start
qk.addEventListener('my_kickoff_event', async (e) => {
    $("#output").append("start");
    await timeout(1000);
}, 'start');
// your middle
qk.addEventListener('my_kickoff_event', (e) => {
    $("#output").append(" middle");
}, 'middle', 'start'); // <<< dependency: middle needs start
// kickoff and end
qk.dispatchEvent(new QKE('my_kickoff_event')).then((e) => {
   // it's still only Promise in the end
   $("#output").append(" end");
});
// or await qk.dispatchEvent(new QKE('my_kickoff_event'));

这样做并不是为了成为最好的性能解决方案,而是在具有许多组件的典型应用程序中易于使用,这些组件无法轻松共享承诺变量。

更多 这里

尝试实时摘要

最新更新