但是,是否有一种更安全的方式来处理这种不确定性呢?在我们的代码中,我们不需要挂起无需完成或拒绝的执行函数。让我们来实现一个执行器包装函数,像一个安全网那样让过长时间还没完成的promises执行拒绝回调函数。这将揭开解决不好处理的promise场景的神秘面纱:
//promise执行器函数的包装器, //在给定的超时时间后抛出错误。 function executorWrapper(func, timeout) { //这是实际调用的函数。 //它需要解析器函数和拒绝器函数作为参数。 return function executor(resolve, reject) { //设置我们的计时器。 //当时间到达时,我们可以使用超时消息拒绝promise。 var timer = setTimeout(() => { reject('Promise timed out after $ {timeout} MS'); }, timeout); //调用我们原来的执行器包装函数。 //我们实际上也包装了完成回调函数 //和拒绝回调函数,所以当 //执行者调用它们时,会清除定时器。 func((value) => { clearTimeout(timer); resolve(value); }, (value) => { clearTimeout(timer); reject(value); }); }; } //这个promise执行后超时, //超时错误消息传递给拒绝回调。 new Promise(executorWrapper((resolve, reject) => { setTimeout(() => { resolve('done'); }, 2000); }, 1000)).catch((reason) => { console.error(reason); }); //这个promise执行后按预期运行, //在定时结束之前调用“resolve()”。 new Promise(executorWrapper((resolve, reject) => { setTimeout(() => { resolve(true); }, 500); }, 1000)).then((value) => { console.log('resolved', value); }); 对promises作出改进既然我们已经很好地理解了promises的执行机制,本节将详细介绍如何使用promises来解决特定问题。通常,这意味着当promises完成或被拒绝时,我们会达到我们某些目的。
我们将首先查看JavaScript解释器中的任务队列,以及这些对我们的解析回调函数的意义。然后,我们将考虑使用promise的结果数据,处理错误,创建更好的抽象来响应promises,以及thenables。让我们开始吧。
处理任务队列JavaScript任务队列的概念在“第2章,JavaScript运行模型”中提到过。它的主要职责是初始化新的执行上下文堆栈。这是常见的任务队列。然而,还有另一种队列,这是专用于执行promises回调的。这意味着,如果他们都存在时,算法会从这些队列中选择一个任务执行。
Promises具有内置的并发语义,而且有充分的理由。如果一个promise被用来确保某个值最终被解析,那么为对其作出响应的代码赋予高优先级是有意义的。否则,当值到达时,处理它的代码可能还要在其他任务后面等待很长的时间才能执行。让我们编写一些代码来演示下这些并发语义:
//创建5个promise,记录它们的执行时间, //以及当他们对返回值做出响应的时间。 for (let i = 0; i < 5; i++) { new Promise((resolve) => { console.log('execting promise'); resolve(i); }).then((value) => { console.log('resolved', i); }); } //在任何promise完成回调之前,这里会先被调用, //因为堆栈任务需要在解释器进入promise解析回调队列之前完成, //当前5个“then()”回调将被置后。 console.log('done executing'); //→ //execting promise //execting promise // ... //done executing //resolved 1 //resolved 2 // ...拒绝回调也遵循同样的语义。
使用promise的返回数据到目前为止,我们已经在本章中看到了一些示例,其中解析器函数完成promise后并返回值。传递给此函数的值是最终传递给完成回调函数的值。通过让执行程序设置任何异步操作的方法,例如setTimeout(),延时传递该值调用解析程序。但在这些例子中,调用者实际上并没有等待任何值;我们只使用setTimeout()作为示例异步操作。让我们看一下我们实际上没有值的情况,异步网络请求需要获取到它:
//用于从服务器获取资源的通用函数, //返回一个promise。 function get(path) { return new Promise((resolve, reject) => { var request = new XMLHttpRequest(); //promise解析数据加载后的JSON数据。 request.addEventListener('load', (e) => { resolve(JSON.parse(e.target.responseText)); }); //当请求出错时,promise执行拒绝回调函数。 request.addEventListener('error', (e) => { reject(e.target.statusText || '未知错误'); }); //如果请求被中止时,我们调用完成回调函数 request.addEventListener('abort', resolve); request.open('get', path); request.send(); }); } //我们可以直接附加我们的“then()”处理程序 //到“get()”,因为它返回一个promise。 //在解析之前,这里使用的值是一个真正的异步操作, //因为必须发请求远程获取值。 get('api.json').then((value) => { console.log('hello', value.hello); });