到目前为止,我们在本书中已看到的XHR请求具有中止请求的处理程序。这是因为我们可以手动中止请求并阻止任何load回调函数运行。需要此功能的典型场景是用户单击取消按钮,或导航到应用程序的其他部分,从而使请求变得毫无意义。
如果我们是要在抽象promise上更上一层楼,在同样的原则也适用。而一些可能发生的并发操作的执行让promise变得毫无意义。promises和XHR请求的过程中之间的区别,是前者没有abort()方法。最后我们要做的一件事是在我们的promise回调中开始引入可能并不必要的取消逻辑。
Promise.race()方法在这里可以帮助我们。顾名思义,该方法返回一个新的promise,它由第一个要解析的输入promise决定。这可能你听的不多,但实现Promise.race()的逻辑并不容易。它实际上是同步原则,隐藏了应用程序代码中的并发复杂性。我们来看看这个方法是怎么可以帮助我们处理因用户交互而取消的promise:
//用于取消数据请求的解析器函数。 var cancelResolver; //一个简单的“常量”值,用于处理取消promise var CANCELED = {}; //我们的UI组件 var buttonLoad = document.querySelector('button.load'), buttonCancel = document.querySelector('button.cancel'); //请求数据,返回一个promise。 function getDataPromise() { //创建取消promise。 //执行器传入“resolve”函数为“cancelResolver”, //所以它稍后可以被调用。 var cancelPromise = new Promise((resolve) => { cancelResolver = resolve; }); //我们实际想要的数据 //这通常是一个HTTP请求, //但我们在这里使用setTimeout()简单模拟一下。 var dataPromise = new Promise((resolve) => { setTimeout(() => { resolve({hello: 'world'}); }, 3000); }); //“Promise.race()”方法返回一个新的promise, //并且无论输入promise是什么,它都可以完成处理 return Promise.race([cancelPromise, dataPromise]); } //单击取消按钮时,我们使用 //“cancelResolver()”函数来处理取消promise buttonCancel.addEventListener('click', () => { cancelResolver(CANCELLED); }); //单击加载按钮时,我们使用 //“getDataPromise()”发出请求获取数据。 buttonLoad.addEventListener('click', () => { buttonLoad.disabled = true; getDataPromise().then((value) => { buttonLoad.disabled = false; //promise得到了执行,但那是因为 //用户取消了请求。所以我们这里 //通过返回CANCELED “constant”退出。 //否则,我们有数据可以使用。 if (Object.is(value, CANCELED)) { return value; } console.log('loaded data', value); }); });作为练习,尝试想象一个更复杂的场景,其中dataPromise是由Promise.all()创建的promise。我们的
cancelResolver()函数可以一次取消许多复杂的异步操作。
在最后一节中,我们将介绍Promise.resolve()和Promise.reject()方法。我们已经在本章前面看到Promise.resolve()如何处理thenable对象。它还可以直接处理值或其他promises。当我们实现一个可能同步也可能异步的函数时,这些方法会派上用场。这不是我们想要使用具有模糊并发语义函数的情况。
例如,这是一个可能同步也可能异步的函数,让人感到迷惑,几乎肯定会在以后出现错误:
//一个示例函数,它可能从缓存中返回“value”, //也可能通过“fetchs”异步获取值。 function getData(value) { //如果它存在于缓存中,我们直接返回这个值 var index = getData.cache.indexOf(value); if(index > -1) { return getData.cache[index]; } //否则,我们必须通过“fetch”异步获取它。 //这个“resolve()”调用通常是会在网络发起请求的回调函数 return new Promise((resolve) => { getData.cache.push(value); resolve(value); }); } //创建缓存。 getData.cache = []; console.log('getting foo', getData('foo')); //→getting foo Promise console.log('getting bar', getData('bar')); //→getting bar Promise console.log('getting foo', getData('foo')); //→getting foo foo我们可以看到最后一次调用返回的是缓存值,而不是一个promise。这很直观,因为我们不需要通过promise获取最终的值,我们已经拥有这个值!问题是我们让使用getData()函数的任何代码表现出不一致性。也就是说,调用getData()的代码需要处理并发语义。此代码不是并发的。让我们通过引入Promise.resolve()来改变它:
//一个示例函数,它可能从缓存中返回“value”, //也可能通过“fetchs”异步获取值。 function getData(value) { var cache = getData.cache; //如果这个函数没有缓存, //那就拒绝promise。 if(!Array.isArray(cache)) { return Promise.reject('missing cache'); } //如果它存在于缓存中, //我们直接使用缓存的值返回完成的promise var index = getData.cache.indexOf(value); if (index > -1) { return Promise.resolve(getData.cache[index]); } //否则,我们必须通过“fetch”异步获取它。 //这个“resolve()”调用通常是会在网络发起请求的回调函数 return new Promise((resolve) => { getData.cache.push(value); resolve(value); }); } //创建缓存。 getData.cache = []; //每次调用“getData()”返回都是一致的。 //甚至当使用同步值时, //它们仍然返回得到解析完成的promise。 getData('foo').then((value) => { console.log('getting foo', `“${value}”`); }, (reason) => { console.error(reason); }); getData('bar').then((value) => { console.log('getting bar', `“${value}”`); }, (reason) => { console.error(reason); }); getData('foo').then((value) => { console.log('getting foo', `“${value}”`); }, (reason) => { console.error(reason); });这样更好。使用Promise.resolve()和Promise.reject(),任何使用getData()的代码默认都是并发的,即使数据获取操作是同步的。
小结