深入Promise-2:Promise的进阶使用

在上一篇深入Promise-1(juejin.cn/post/732822...)中,讲解了Promise的基础概念和使用方法,这一篇让我们接着了解Promise的一些进阶用法吧。

Promise的其他方法

ES6为Promise提供了很多内置的方法,为Promise的操作提供了更多的操作空间。

我们先整一个用来示例的异步方法:

javascript 复制代码
// 模拟异步
function sync(log, time = 500, res = true) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (res) {
                resolve(log);
                console.log(log);
            } else {
                reject(log);
                console.log('error', log);
            }
        }, time);
    })
}

Promise.all

all方法可以接收一个由多个promise实例组成的参数,并返回一个新的promise实例,all会并发执行这些promise实例,获取所有的返回值,返回给新的promise实例。

参数

all的参数是由多个promise实例组成。一般都是用一个数组包裹,但是在all方法的规范中,参数其实是一个IteratorPromise.all(Iterator)),一个可迭代的对象。所以除了传入数组,也可以传入迭代器对象。

javascript 复制代码
// 传入数组
const p1 = Promise.all([
  sync(1),
  sync(2),
  sync(3)
]);
// 传入迭代器对象
const myIterable = {};
myIterable[Symbol.iterator] = function () {
  return {
    i: 1,
    next: function() {
      if (this.i < 4) {
        return {done: false, value: sync(this.i++)}; // 必须这么写,这是iterator返回值的规范
      }

      return {done: true};
    }
  }
};
const p2 = Promise.all(myIterable);

// 传入生成器函数
function* generate() {
  yield sync(1);
  yield sync(2);
  yield sync(3);
}

const p3 = Promise.all(generate());

当然,除非是特殊情况,还是建议传入数组。

需要注意的是传入的Iterator的元素必须是Promise实例,而像Map这样的,虽然也是可迭代,但是每次迭代返回的并非Promise实例是不生效的。

javascript 复制代码
// 传入Map
const pMap = Promise.all(new Map([
  [1, sync(1)],
  [2, sync(2)],
  [3, sync(3)],
]));

pMap.then(res => {
  console.log('pMap res', res);
});

console.log('pMap', pMap);

结果为:

可见Map被当做非Promise来处理。

如果传入的参数中,含有非promise的实例,会将其使用Promise.resolve包裹(关于Promise.resolve可以查看上篇文章)。

ini 复制代码
// 传入非promise
Promise.all([
    sync(1),
    { isSync: false},
    sync(2)
]).then(res => {
  console.log(res[1]); // {isSync: false}
});

那如果是传入一个函数呢?

javascript 复制代码
// 传入非promise函数
Promise.all([
  (function(){return 123})()
]).then(res => {
  console.log(res[0]); // 123
});

表现与Promise.resolve一样,但是如果在函数中抛出错误的话,也是一样,程序执行会被中断:

javascript 复制代码
Promise.all(
    [
        (function(){
            throw new Error('失败');  // Uncaught Error: 失败
        })()
    ]
).catch(res => {
    console.log('res', res);
});

假如我们传入空的 Iterator参数,all也会将其使用Promise.resolve包裹,并立即返回。

javascript 复制代码
Promise.race([])
 .then(res => {
    console.log(res); // []
  });

const myIterable = {};
myIterable[Symbol.iterator] = function () {
  return {
    next: function() {
      return {done: true};
    }
  }
};

Promise.all(myIterable)
  .then(res => {
    console.log(res); // [] 打印出来是[],但其实是一个可迭代对象
  });

function* generate() {}

Promise.all(generate())
  .then(res => {
    console.log(res); // []
  });

状态

Promise.all会返回一个新的promise,该promise的状态与传入的多个promise的状态有关。all是并发执行传入的promise,当所有promise的状态都发生变化后,其状态才会从pending变更。如果有一个promise是失败,则状态为rejected,如果都是成功,则为fulfilled

这里有一个疑问,正常情况下(全部promise成功的情况),所有promise都会被执行,但是如果中间有某些promise执行失败,那后续的promise还会执行吗?还未执行完的promise会继续执行吗?简单做个实验:

scss 复制代码
Promise.all([
    sync(1, 1000), 
    sync(2, 1000),
    sync(3, 500, false), // 第三个抛出错误
    sync(4, 600)   
]);

在第三个promise抛出错误,返回的promise的状态是rejected,第一个和第二个promise并没有被停止,第四个promise也被正常执行。

结论:传入的promise并行执行,即使中间的有promise执行失败,其他promise也会正常执行,但是返回的promise的状态还是会被置为失败。失败的错误也是通过catch捕获。

返回结果

在全部promise执行成功后,Promise.all会将所有promise的结果包装成数组返回,如果没有返回的结果的会使用undefined占位。结果的顺序与参数中的顺序一致。如果执行失败,会返回第一个失败的原因。

scss 复制代码
// 成功
Promise.all([
    sync([1]), 
    sync({log:2}),
    sync(3)
]).then(res => {
  console.log(res); // [[1],{log:2},3]
});

// 失败,执到catch
Promise.all([
    sync([1]), 
    sync({log:2}, 500, false),
    sync(3)
]).catch(err => {
  console.log(err); // {log:2}
});

// 多个失败,执到catch,返回第一个错误
Promise.all([
    sync([1]), 
    sync({log:2}, 500, false),
    sync({log:3}, 600, false)
]).catch(err => {
  console.log(err); // {log:2}
});

使用建议

all是并行执行,不是按顺序执行,如果执行的异步操作没有前后依赖的话,可以考虑使用all来做并行执行。基于all的特点,可以将需要一同获取并且不容纳失败的异步操作(例如:用户登录后获取用户基础信息和权限)包装到一起执行。假如,某些promise是允许失败的(例如那些对主流程无影响的),可以在promise上把错误catch掉,避免影响。

scss 复制代码
Promise.all([
    sync(1, 1000), 
    sync(2, 1000),
    sync(3, 500, false).catch(err => {console.log(err);}), // 将错误拦截掉
    sync(4, 600)   
]);

Promise.race

race方法同样可以接收一个由多个promise实例组成的参数,并返回一个新的promise实例。

scss 复制代码
const p = Promise.race([
  sync(1),
  sync(2),
  sync(3)
]);

all不同的是,race虽然也是并行运行,但当一个promise实例率先改变状态,返回的promise实例的状态会随之改变,并将返回其返回值。

race的参数all也是一样,规则也一样,这里就不赘述了。有一点不同的是,如果传入一个空数组[]all会将空的Iterator作为promise的返回值,但是race不会,返回的promise会处于pending状态。

javascript 复制代码
const p2 = Promise.race([]);
console.log(p2); // Promise { <pending> }

const myIterable = {};
myIterable[Symbol.iterator] = function () {
  return {
    next: function() {
      return {done: true};
    }
  }
};

const p3 = Promise.race(myIterable);
console.log(p3); // Promise { <pending> }

function* generate() {}
  
const p4 = Promise.race(generate());
console.log(p4); // Promise { <pending> }

状态&返回

前面说过,race的状态会跟随第一个改变状态的promise:

ini 复制代码
const p = Promise.race([
    sync(1, 600),
    sync(2),
    sync(3, 700)
]);

p.then(res => {
    console.log('res', res);
});

可以看到,第二个promise是最先完成的,p的状态也被置为fulfilled,返回值也置为2。而且与all一样,并不会停止其他promise,其他promise还是会执行完成。

如果有一个promise报错呢?

ini 复制代码
const p = Promise.race([
    sync(1, 600, false),
    sync(2),
    sync(3, 700)
]);

p.then(res => {
    console.log('res', res);
});

还是获取到第二个promise的结果。

如果第一个改变的状态的promise是失败的呢?

ini 复制代码
const p = Promise.race([
    sync(1, 600, false),
    sync(2, 500, false),
    sync(3, 700)
]);

p.catch(err => {
    console.log('err', err);
});

抛出一个失败的错误。其他promise继续执行。

如果所有的promise都是同时返回的呢?

ini 复制代码
const p = Promise.race([
    sync(1),
    sync(2),
    sync(3)
]);

p.then(res => {
    console.log('res', res);
});

按照执行机制(后面的文章聊),如果promise都是同时完成的话,理论上应该都是获取到第一个。

使用建议

利用race的特点,我们可以实现一些需要优先处理或者竞速的场景:

  • 优先渲染/加载,在并发获取数据或资源的场景,通过race可以判断第一个异步完成的时间点,这时候就可以做针对性的优先处理,然后其他的请求还会继续进行,可以继续完成后续的操作。
  • 超时处理,在一些对时间比较敏感的场景,可以这样来实现
javascript 复制代码
function timeout(time) {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject('请求超时'), time);
  });
}

Promise.race(
  [
    ajax('xxxx'),
    timeout(500)
  ]
).then(() => {
  // 接口未超时
}).catch(() => {
  // 接口超时
})

上面的代码定义了一个timeout的函数,在到达一定的时间后会返回错误,通过race可以在超时时做出处理。

Promise.any

any方法也是接收多个promise实例,如果有一个promise率先成功,那么返回的promise的状态则跟随该promise的状态。这里跟race是不一样的,如果率先完成的promise是失败的,any会继续等待下一个成功的promise。也就是说,any会获取第一个成功的promise,并返回对应的值。但是如果没有一个成功的promise,那就返回失败。

ini 复制代码
Promise.any([
   sync(1, 500, false),
   sync(2, 700),
   sync(3, 600)
]).then(res => {
  console.log(res);  // 3
}).catch(err => {
  console.log(err);
});

上面的代码,第一个promise返回错误,第二第三个返回成功,但是因为第三个promise率先返回状态,所以结果返回3。

状态&返回

any很好理解,就是返回第一个成功的promise的内容,值得注意的是,如果传入的promise全部错误或者传入空的Iterator,会返回一个特定的错误,而不是某个promise的错误。

AggregateError

any返回的特定错误是一个AggregateError类型的错误,

javascript 复制代码
// 传入空
Promise.any([])
  .catch(err => {
    console.log(err); // AggregateError: All promises were rejected
    console.log(err.errors); // []
  });
// 全部错误
Promise.any([
    sync(1, 500, false),
    sync({error: true}, 700, false),
    sync({message: 'error'}, 600, false)
 ]).catch(err => {
     console.log(err); // AggregateError: All promises were rejected
     console.log(err.errors); // [1, { error: true }, { message: 'error' } ]
 });

从上面的例子可以看到,在全部失败的情况下,any会等待全部promise执传完再返回,并将所有错误都收集到一个AggregateError 对象中。直接访问这个错误对象,会告知All promises were rejected,这是一个固定的错误信息,同时可以通过AggregateError.errors来获取所有promise返回的错误。

关于AggregateError,可以参考MDN的说明:developer.mozilla.org/zh-CN/docs/...

使用建议

any比较适合一些竞速和容错的场景:

  • 容错: 为了提升用户体验,使用any可以在多个服务器获取同一数据或资源,可以避免单个请求失败后无法获取数据或资源的情况。这不仅提高了数据获取的效率,也增加了系统的容错能力,使得用户在使用应用时获得更加流畅和稳定的体验。此外,这种方法也有助于分散对单一服务器的依赖,减少单点故障的风险。例如实现一个在线播放器,可以从多个资源服务器获取同一个音频,即可保证最快的加载,又可以避免单个资源服务器失败后复杂的重试机制。
  • 竞速:race的类似的方式,但是any的好处就是具备容错性,除了全部失败,总能获得结果。

Promise.allSettled

allSettled同样是接收Iterator参数,该方法会在所有的promise状态都发生变化后(无论成功和失败)返回一个成功的promise实例,并将所有的promise实例的结果包装成一个数组返回。

ini 复制代码
const p1 = Promise.allSettled([
  sync(1),
  sync(2),
  sync(3, 500, false)
]);

p1.then(res => {
  console.log(res); 
});

上面的代码得到的res为:

lua 复制代码
[
  { status: 'fulfilled', value: 1 },
  { status: 'fulfilled', value: 2 },
  { status: 'rejected', reason: 3 }
]

p1则为fulfilled的promise实例。

状态

allSettled返回的promise实例的状态只会是fulfilled,无论参数是空的Iterator还是全部失败的promise实例,都是fulfilled

scss 复制代码
Promise.allSettled([
  sync(1, 500, false),
  sync(2, 500, false),
  sync(3, 500, false)
]);   // fulfilled

Promise.allSettled([]); // fulfilled

返回

allSettled回调的返回值则是固定的格式,会将所有promise实例的返回值包装进一个数组,数组的元素为固定格式的对象:

  • 成功:{ status: 'fulfilled', value: value }
  • 失败:{ status: 'rejected', reason: reason }

对象的属性意义如下:

  • status 对应的promise的状态,成功为fulfilled,失败为rejected
  • value 成功的promise会返回该属性,值为promise的返回值
  • reason 失败的promise会返回该属性,只为promise失败的原因

返回复杂结构的值或者错误对象,结果也是一样的:

less 复制代码
Promise.allSettled([
  sync({result:'success', data: [{id:1, value:1}]}),
  sync(new Error('失败'), 500, false)
]).then(res => {
  console.log(res);
}); 

使用建议

allSettled比较适合需要容错的情况:

  • 多并发容错 ,在一个需要多异步并发的场景,通过allSettled可以在then中统一处理
  • 初始化时的资源加载 ,在程序初始化时可能需要加载多个多种资源(图片、音频、配置文件等),这些资源即使加载失败也不应该影响程序的继续执行,可以使用allSettled来处理。
  • 回退或者重试 ,例如在node中做些数据库的写入操作,在批量写入或者是事务性操作中,使用allSettled可以获得所有异步读写操作的结果,通过对结果判断是否需要回退数据或者重试读写(使用all无法实现这种效果,如果出现错误只能得到一个错误)。

其他用法

Promise进阶用法,最基础就是链式调用,另外就是前面聊得内置方法的使用。哪还有其他使用方法吗?

Deferred式外部控制

Deferred是早期Promise还未纳入ES规范中,第三方库实现Promise提供的一种使用方法,可以在Promise外部控制Promise的状态:

ini 复制代码
function defered() {
  const dfd = {
    promsie: null,
    resolve: null,
    reject: null
  };
  dfd.promise = new Promise(function(resolve, reject) {
    dfd.resolve = resolve;
    dfd.reject = reject;
  });
  return dfd;
}

function demo() {
  const dfd = defered();
  // dosomething
  if (success) {
    dfd.resolve();
  } else {
    dfd.reject();
  }
  return dfd.promise;
}

上面的代码应该是最简单的用法defered函数返回了一个包含了空的Promise实例(没有包含任何异步操作)、resolve方法、reject方法的对象,在demo函数中返回Promise实例,在某些的操作后执行再改变其状态。这种写法可以将demo内部的异步操作与返回Promise隔离开来,这样通过demo得到的就是一个属于demo的promise,调用方不会感知到demo内部的异步操作也不会受其影响。另外就是可以根据需要在未来某个时间点去修改返回的promise的状态,已达到根据需要修改状态的需求。

在Promise纳入ES规范后,上面的方法可以直接使用Promise.resovle()Promise.reject()来实现,比Deferred更简洁理解难度更低。

当然,Deferred还是可用的地方,例如在页面上的一些回调,无法只直接传递Promise,可以用Deferred来实现。例如在Vue中实现一个全局确认弹窗:

弹窗有两个按钮:确认和取消,希望实现这样的效果,通过一个全局的方法来唤醒弹窗,方法返回一个Promise,在点击确定后通过then回调,取消通过catch回调。这样就可以直接在js中调用又不需要写一堆监听方法。\

dialog.vue代码

xml 复制代码
<template>
  <div
    class="dialog"
    v-show="visible"
  >
    <div>
      {{ message }}
    </div>

    <button @click="cancel">取消</button>
    <button @click="submit">确认</button>
  </div>
</template>

<script setup>
defineProps({
  visible: Boolean,
  message: String,
  cancel: Function,
  submit: Function
})
</script>

入口文件index.js代码

ini 复制代码
import { createApp } from 'vue'
import MessageDialog from './dialog.vue';

function defered() {
    const dfd = {
      promsie: null,
      resolve: null,
      reject: null
    };
    dfd.promise = new Promise(function(resolve, reject) {
      dfd.resolve = resolve;
      dfd.reject = reject;
    });
    return dfd;
  }
  

export default (message) => {
    const dfd = defered();
    const mountNode = document.createElement('div');
    const Instance = createApp(MessageDialog, {
        visible: true,
        message,
        cancel: () => {
            dfd.reject();
        },
        submit: () => {
            dfd.resolve();
        }
    });

    document.body.appendChild(mountNode);
    Instance.mount(mountNode);
    return dfd.promise;
}

使用

javascript 复制代码
import messageDialog from '@/components/message-dialog';

messageDialog('测试dialog')
  .then(() => {
    console.log('submit')
  })
  .catch(() => {
    console.log('cancel')
  })

OK,上面的代码只是一个简单的示例,展示下这种外部修改Promise状态的用法,提供一些参考。

异步结果共享

在项目运行中,可能会出现多个调用方调用同一个异步操作获取相同的结果的情况,例如token过期后,同一时间有多个曹邹调用刷新token的接口。这种情况会造成大量的资源浪费,我们可以利用Promise状态确认后不会改变的特性来实现一定时间内的异步结果共享。

ini 复制代码
function ajaxShare(url = '', data = {}, type = 'GET', duration = 0) 
{
    const key = JSON.stringify(url, data, type);
    const curPromise = promises[key];
    if (curPromise) {
        if (curPromise.expiration > +new Date()) {
            return curPromise.promise;
        }

        delete promises[key];
    }

    promises[key] = {
        promise: ajax(url, data, type),
        expiration: +new Date() + duration
    };

    return promises[key].promise;
}

上面的代码对上篇的ajax方法做了简单的封装,对于同一个请求,可以在指定的时间内都是返回同一个promise,实现同一请求的结果共享:

ini 复制代码
const p1 = ajaxShare('url', {}, 'GET', 500);
const p2 = ajaxShare('url', {}, 'GET', 500);
console.log(p1 === p2); // true

总结

本文总结了一些Promise的进阶方法,结合个人的经验给了一些使用的建议。希望对大家有帮助。接下来会研究下Promise的原理,敬请期待。

相关推荐
陈随易42 分钟前
农村程序员-关于小孩教育的思考
前端·后端·程序员
云深时现月43 分钟前
jenkins使用cli发行uni-app到h5
前端·uni-app·jenkins
昨天今天明天好多天1 小时前
【Node.js]
前端·node.js
亿牛云爬虫专家1 小时前
Puppeteer教程:使用CSS选择器点击和爬取动态数据
javascript·css·爬虫·爬虫代理·puppeteer·代理ip
2401_857610031 小时前
深入探索React合成事件(SyntheticEvent):跨浏览器的事件处理利器
前端·javascript·react.js
雾散声声慢1 小时前
前端开发中怎么把链接转为二维码并展示?
前端
熊的猫1 小时前
DOM 规范 — MutationObserver 接口
前端·javascript·chrome·webpack·前端框架·node.js·ecmascript
天农学子1 小时前
Easyui ComboBox 数据加载完成之后过滤数据
前端·javascript·easyui
mez_Blog1 小时前
Vue之插槽(slot)
前端·javascript·vue.js·前端框架·插槽
爱睡D小猪1 小时前
vue文本高亮处理
前端·javascript·vue.js