likes
comments
collection
share

Promise大杂烩🚀

作者站长头像
站长
· 阅读数 16

Promise简介

是啥?

Promise 是异步编程的一种解决方案:从语法上讲,promise是一个对象,从它可以获取异步操作的消息;Promise 提供了统一的 API ,各种异步操作都可以用同样的方法进行处理。

多年来,promise 已成为语言的一部分(在 ES2015 中进行了标准化和引入),并且最近变得更加集成,在 ES2017 中具有了 async 和 await。

异步函数 在底层使用了 promise,要了解 asyncawait 之前,我们要先了解 promise 的工作方式。

Promise对象有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)

一个 Promise 必然处于以下几种状态之一:

待定 (pending): 初始状态,既没有被兑现,也没有被拒绝。

已成功 (fulfilled): 成功状态,意味着操作成功完成。

已拒绝 (rejected):失败状态,意味着操作失败。

状态一旦改变,就不会再变。无论失败还是成功都会有一个结果数据,执行回调时成功的结果一般是value,失败的结果一般是reason。

解决啥问题

Promise可以将异步操作以同步操作的流程表达出来,能有效的解决 js 异步回调地狱问题,且将业务逻辑与数据处理分隔开使代码更优雅,方便阅读,更有利于代码维护;

此外,Promise对象提供统一的接口,使得控制异步操作更加容易。

但是,Promise 新建后就会立即执行,且无法中途取消;如果不设置回调函数,Promise内部抛出的错误不会反应到外部;再者,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

如何运作?

当 promise 被调用后,它会以初始状态 (pending) 开始。 这意味着调用的函数会继续执行,而 promise 仍处于处理中直到解决为止,从而为调用的函数提供所请求的任何数据。

被创建的 promise 最终会以成功状态 (fulfilled)失败状态 (rejected) 结束,并在完成时调用相应的回调函数(传给 thencatch

基本用法

ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。我们可以使用 new Promise() 对其进行初始化。

let state='success';
const promise = new Promise(function(resolve, reject) {
    if (state=='success'){
        /* 异步操作成功 */
        const value="成功的结果数据" ;
        resolve(value);
    } else {
        const error="没有成功" 
        reject(error);
    }
});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject

上面代码我们先用state的值来代替异步操作。

如果异步操作成功,我们用value作为异步操作的结果,作为resolve函数的参数传递出去,此时Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved);

否则用error作为异步操作失败时报的错误,作为reject函数的参数传递出去。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

promise.then(function(value) {
    // success
}, function(error) {
   // failure
});

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。这两个函数都是可选的,不一定要提供。它们都接受Promise对象传出的值作为参数。

第一次接触Promise的时候,便是封装AJAX

Promise封装AJAX

首先我们创建一个功能函数,可以传入需要的参数,做成一个模块可以引入,具体可以查看Module 的语法 - ECMAScript 6入门

export function request(url, data, header = null, method = "get") {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.responseType = 'json';
        xhr.onreadystatechange = function () {
            if (xhr.readyState === 4) {
                if (xhr.status >= 200 && xhr.status < 300) {
                    resolve(xhr.response);//成功
                } else {
                    reject(xhr.status);//失败
                }
            }
        };
        //判断data是否有数据
        var param = '';
        //这里使用stringify方法将js对象格式化为json字符串
        if (JSON.stringify(data) != '{}') {
            url += '?';
            for (var i in data) {
                // i为属性名称,data[i]为对应属性的值 
                param += i + '=' + data[i] + '&';   //将js对象重组,拼接成url参数存入param变量中
            }
            //使用slice函数提取一部分字符串,这里主要是为了去除拼接的最后一个&字符
            //slice函数:返回一个新的字符串。包括字符串从 start 开始(包括 start)到 end 结束(不包括 end)为止的所有字符。
            param = param.slice(0, param.length - 1);
        };
        //判断method是否为get
        if (method == "get") {
            //是则将数据拼接在url后面
            url = url + param;
        }
        //初始化请求
        xhr.open(method, url, true);

        if (method == 'post') {
            xhr.setRequestHeader("content-Type", "application/x-www-form-urlencoded");
            //发送请求
            xhr.send(param);
        }
        else {
            xhr.send();
        }
    });
}

//引入示范
// function login(no, pwd) {
//     return request('url', { no, pwd }, { token: 'aaa' }, 'post');
// }
// 引入功能函数并应用
import { request } from './request.js'

const address = '****';

// 从第一次请求找到用户信息id
function getAuthor(authorId) {
    return request(address + 'getUserServlet', { "id": authorId });
}

// 统一暴露
export {getAuthor};

使用上面封装好的ajax发起一个请求:

import {getAuthor } from './api.js'
let address="****";//请求地址
let param=3;
//获取用户id为3包含的所有信息
 getAuthor(param).then(res => {
        let idData = res.data;
        console.log(idData);
 });

方法

Promise.resolve()

有些时候我们需要将现有对象转换成Promise对象(比如对Promise.all的实现等),这个时候Promise.resolve()方法就起到这个作用。

Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))

Promise.resolve方法的参数分成四种情况。

  • 参数是一个 Promise 实例

如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。

  • 参数是一个thenable对象

thenable对象指的是具有then方法的对象,比如下面这个对象。

let thenable = {
  then: function(resolve, reject) {
    resolve(hello);
  }
};

Promise.resolve方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then方法。

let thenable = {
  then: function(resolve, reject) {
    resolve("hello");
  }
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
  console.log(value);  // hello
});

上面代码中,thenable对象的then方法执行后,对象p1的状态就变为resolved,从而立即执行最后那个then方法指定的回调函数,输出 42。

  • 参数不是具有then方法的对象,或根本就不是对象

如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的 Promise 对象,状态为resolved

const p = Promise.resolve('Hello');
p.then(function (s){
  console.log(s)
});
// Hello

上面代码生成一个新的 Promise 对象的实例p。由于字符串Hello不属于异步操作(判断方法是字符串对象不具有 then 方法),返回 Promise 实例的状态从一生成就是resolved,所以回调函数会立即执行。Promise.resolve方法的参数,会同时传给回调函数。

  • 不带有任何参数

Promise.resolve()方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。

所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve()方法。

const p = Promise.resolve();
p.then(function () {
  // ...
});

上面代码的变量p就是一个 Promise 对象。

需要注意的是,立即resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。

setTimeout(function () {
  console.log('three');
}, 0);
Promise.resolve().then(function () {
  console.log('two');
});
console.log('one');
// one
// two
// three

上面代码中,setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.resolve()在本轮“事件循环”结束时执行,console.log('one')则是立即执行,因此最先输出。

这个部分可以了解一下事件循环(我也好想整理)。

Promise.reject()

Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected

const p = Promise.reject('error');
// 等同于
const p = new Promise((resolve, reject) => reject('error'))
p.then(null, function (s) {
  console.log(s)
});
// error

上面代码生成一个 Promise 对象的实例p,状态为rejected,回调函数会立即执行。

注意,Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致。

const thenable = {
  then(resolve, reject) {
    reject('出错了');
  }
};
Promise.reject(thenable)
.catch(e => {
  console.log(e === thenable)
})
// true

上面代码中,Promise.reject方法的参数是一个thenable对象,执行以后,后面catch方法的参数不是reject抛出的“出错了”这个字符串,而是thenable对象。

Promise.prototype.then()

Promise 实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。

then方法的第一个参数是resolved状态的回调函数。如果该参数不是函数,则会在内部被替换为 (x) => x,即原样返回 promise 最终结果的函数;

第二个参数(可选)是rejected状态的回调函数。如果该参数不是函数,则会在内部被替换为一个 "Thrower" 函数 (it throws an error it received as argument)。

then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // ...
});

上面的代码使用then方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。

采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。

getJSON("/post/1.json").then(function(post) {
  return getJSON(post.commentURL);
}).then(function (comments) {
  console.log("resolved: ", comments);
}, function (err){
  console.log("rejected: ", err);
});

上面代码中,第一个then方法指定的回调函数,返回的是另一个Promise对象。这时,第二个then方法指定的回调函数,就会等待这个新的Promise对象状态发生变化。如果变为resolved,就调用第一个回调函数,如果状态变为rejected,就调用第二个回调函数。

如果采用箭头函数,上面的代码可以写得更简洁。

getJSON("/post/1.json").then(
  post => getJSON(post.commentURL)
).then(
  comments => console.log("resolved: ", comments),
  err => console.log("rejected: ", err)
);

前面介绍了 then 方法的参数和链式调用,下面详细介绍一下 then 方法的返回值,也就是 then 方法中的 return

当一个 Promise 完成(fulfilled)或者失败(rejected)时,返回函数将被异步调用(由当前的线程循环来调度完成)。具体的返回值 return 依据以下规则返回。如果 then 中的回调函数:

  • 返回 (return) 了一个值,那么 then 返回的 Promise 将会成为接受状态,并且将返回的值作为接受状态的回调函数的参数值。
  • 没有返回 (return) 任何值,那么 then 返回的 Promise 将会成为接受状态,并且该接受状态的回调函数的参数值为 undefined
  • 抛出一个错误,那么 then 返回的 Promise 将会成为拒绝状态,并且将抛出的错误作为拒绝状态的回调函数的参数值。
  • 返回 (return) 一个已经是接受状态的 Promise,那么 then 返回的 Promise 也会成为接受状态,并且将那个 Promise 的接受状态的回调函数的参数值作为该被返回的Promise的接受状态回调函数的参数值。
  • 返回 (return) 一个已经是拒绝状态的 Promise,那么 then 返回的 Promise 也会成为拒绝状态,并且将那个 Promise 的拒绝状态的回调函数的参数值作为该被返回的Promise的拒绝状态回调函数的参数值。
  • 返回 (return) 一个未定状态(pending)的 Promise,那么 then 返回 Promise 的状态也是未定的,并且它的终态与那个 Promise 的终态相同;同时,它变为终态时调用的回调函数参数与那个 Promise 变为终态时的回调函数的参数是相同的。

链式调用promise.then()

我们都知道,then 方法返回一个新的 promise 实例,这是实现链式调用的根本。

为了在 promise 状态发生变化时(resolve / reject 被调用时)再执行 then 里的函数,我们使用一个 callbacks 数组先把传给then的函数暂存起来,等状态改变时再调用。

那么,怎么保证后一个 then 里的方法在前一个 then(可能是异步)结束之后再执行呢?

var p1 = new MyPromise((resolve, reject) => {
      console.log('hhh')
      setTimeout(() => {
        resolve(1)
      }, 1000);
    }).then(res => {
      return new MyPromise((resolve, reject) => {
        setTimeout(() => {
          resolve(res+1)
        }, 1000);
      })
    }).then(res => {
      console.log(res);//2 秒后再打印,值为 2
      return res;
    })

实现链式调用的核心

将传给 then 的函数和新 promiseresolve 一起 push 到前一个 promisecallbacks 数组中,达到承前启后的效果

  • 承前:当前一个 promise 完成后,调用其 resolve 变更状态,在这个 resolve 里会依次调用 callbacks 里的回调,这样就执行了 then 里的方法了
  • 启后:上一步中,当 then 里的方法执行完成后,返回一个结果,如果这个结果是个简单的值,就直接调用新 promiseresolve,让其状态变更,这又会依次调用新 promisecallbacks 数组里的方法,循环往复。。如果返回的结果是个 promise,则需要等它完成之后再触发新 promiseresolve,所以可以在其结果的 then 里调用新 promiseresolve

Promise大杂烩🚀

Promise.prototype.catch()

catch() 方法返回一个Promise,并且处理拒绝的情况。它的行为与调用Promise.prototype.then(undefined, onRejected) 相同。

事实上, calling obj.catch(onRejected) 内部calls obj.then(undefined, onRejected)。(这句话的意思是,我们显式使用obj.catch(onRejected),内部实际调用的是obj.then(undefined, onRejected))

Promise.prototype.catch()方法是.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数。

getJSON('/posts.json').then(function(posts) {
  // ...
}).catch(function(error) {
  // 处理 getJSON 和 前一个回调函数运行时发生的错误console.log('发生错误!', error);
});

上面代码中,getJSON()方法返回一个 Promise 对象,如果该对象状态变为resolved,则会调用then()方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch()方法指定的回调函数,处理这个错误。另外,then()方法指定的回调函数,如果运行中抛出错误,也会被catch()方法捕获。

p.then((val) => console.log('fulfilled:', val))
  .catch((err) => console.log('rejected', err));
  
// 等同于
p.then((val) => console.log('fulfilled:', val))
  .then(null, (err) => console.log("rejected:", err));

下面是一个例子。

const promise = new Promise(function(resolve, reject) {
  throw new Error('test');
});
promise.catch(function(error) {
  console.log(error);
});
// Error: test

上面代码中,promise抛出一个错误,就被catch()方法指定的回调函数捕获。注意,上面的写法与下面两种写法是等价的。

// 写法一
const promise = new Promise(function(resolve, reject) {
  try {
    throw new Error('test');
  } catch(e) {
    reject(e);
  }
});
promise.catch(function(error) {
  console.log(error);
});
// 写法二
const promise = new Promise(function(resolve, reject) {
  reject(new Error('test'));
});
promise.catch(function(error) {
  console.log(error);
});

比较上面两种写法,可以发现reject()方法的作用,等同于抛出错误。

◾ 如果 Promise 状态已经变成resolved,再抛出错误是无效的。

const promise = new Promise(function(resolve, reject) {
  resolve('ok');
  throw new Error('test');
});
promise
  .then(function(value) { console.log(value) })
  .catch(function(error) { console.log(error) });
// ok

上面代码中,Promise 在resolve语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。

Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。

getJSON('/post/1.json').then(function(post) {
  return getJSON(post.commentURL);
}).then(function(comments) {
  // some code
}).catch(function(error) {
  // 处理前面三个Promise产生的错误
});

上面代码中,一共有三个 Promise 对象:一个由getJSON()产生,两个由then()产生。它们之中任何一个抛出的错误,都会被最后一个catch()捕获。

一般来说,不要在then()方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。

// bad
promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });
  
// good
promise
  .then(function(data) { //cb// success
  })
  .catch(function(err) {
    // error
  });

上面代码中,第二种写法要好于第一种写法,理由是第二种写法可以捕获前面then方法执行中的错误,也更接近同步的写法(try/catch)。因此,建议总是使用catch()方法,而不使用then()方法的第二个参数。

跟传统的try/catch代码块不同的是,如果没有使用catch()方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);
  });
};
someAsyncThing().then(function() {
  console.log('everything is great');
});
setTimeout(() => { console.log(123) }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined// 123

在浏览器中运行上面这段代码,等待两秒后,你会看到控制台正常打印"123",并没有因为someAsyncThing()方法里的错误阻塞代码运行。

Promise大杂烩🚀

上面代码中,someAsyncThing()函数产生的 Promise 对象,内部有语法错误。浏览器运行到这一行,会打印出错误提示ReferenceError: x is not defined,但是不会退出进程、终止脚本执行,2 秒之后还是会输出123。这就是说,Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。

级联错误

在上一章节的示例中,有个 catch 被附加到了 promise 链上。

当 promise 链中的任何内容失败并引发错误或拒绝 promise 时,则控制权会转到链中最近的 catch() 语句。

new Promise((resolve, reject) => {
  throw new Error('错误')
}).catch(err => {
  console.error(err)
})

// 或new Promise((resolve, reject) => {
  reject('错误')
}).catch(err => {
  console.error(err)
})

如果在 catch() 内部引发错误,则可以附加第二个 catch()来处理,依此类推。

new Promise((resolve, reject) => {
  throw new Error('错误')
})
  .catch(err => {
    throw new Error('错误')
  })
  .catch(err => {
    console.error(err)
  })

Promise.prototype.finally()

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

finally() 方法返回一个Promise。在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。这为在Promise是否成功完成后都需要执行的代码提供了一种方式。

这避免了同样的语句需要在then()catch()中各写一次的情况。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代码中,不管promise最后的状态,在执行完thencatch指定的回调函数以后,都会执行finally方法指定的回调函数。

如果你想在 promise 执行完毕后无论其结果怎样都做一些处理或清理时,finally() 方法可能是有用的。

由于无法知道promise的最终状态,所以finally的回调函数中不接收任何参数,它仅用于无论最终结果如何都要执行的情况。

Promise.resolve(2).then(() => {}, () => {}) (resolved的结果为undefined)不同,Promise.resolve(2).finally(() => {}) resolved的结果为 2

Promise大杂烩🚀

同样,Promise.reject(3).then(() => {}, () => {}) (fulfilled的结果为undefined), Promise.reject(3).finally(() => {}) rejected 的结果为 3

Promise大杂烩🚀

Promise.all

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例,它发起多个并发请求,然后在所有 promise 都被解决后执行一些操作。

const p = Promise.all([p1, p2, p3]);

Promise.all()方法接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。

只有所有的 Promise 都成功了,才会执行 resolve 并返回是所有成功的返回值数组,状态为成功状态;如果有一个 Promise 失败了,则会直接执行 reject 返回失败的返回值,状态为失败状态;有多个 Promise 失败,则返回第一个失败的返回值;

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values);
});
// expected output: Array [3, 42, "foo"]

Promise.race

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。传入多个promise实例,谁跑的快(第一个完成),就以谁的结果执行回调,第一个是失败这个Promise就失败,如果第一个是成功就是成功。

const p = Promise.race([p1, p2, p3]);

上面代码中,只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

Promise.race()方法的参数与Promise.all()方法一样,如果不是 Promise 实例,就会调用Promise.resolve()方法,将参数转为 Promise 实例,再进一步处理。

const first = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, '第一个')
})
const second = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, '第二个')
})

Promise.race([first, second]).then(result => {
  console.log(result) // 第二个
})

Promise.any

ES2021 引入了Promise.any()方法。该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。

只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。

Promise.any()Promise.race()方法很像,只有一点不同,就是Promise.any()不会因为某个 Promise 变成rejected状态而结束,必须等到所有参数 Promise 变成rejected状态才会结束。Promise.any 会把所有的失败/错误集合在一起,返回一个失败的 promise 和AggregateError类型的实例。

const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);

const allSettledPromise = Promise.any([resolved, rejected]);

allSettledPromise.then(function (results) {
    console.log(results);
});

Promise大杂烩🚀

Promise.allSettled

ES2020 引入了Promise.allSettled()方法,用来确定一组异步操作是否都结束了(不管成功或失败)。所以,它的名字叫做”Settled“,包含了”fulfilled“和”rejected“两种情况。

Promise 实例无论成功与否,都等它们异步操作结束了在继续执行下一步操作,与 Promise.any 类似,Promise.allSettled 也给所有收集到的结果打上了标记。而且 Promise.allSettled 是不会变成 rejected 状态的,不管一组 Promise 实例的各自结果如何,Promise.allSettled 都会转变为 fulfilled 状态。

Promise.allSettled()方法接受一个数组作为参数,数组的每个成员都是一个 Promise 对象,并返回一个新的 Promise 对象。只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled还是rejected),返回的 Promise 对象才会发生状态变更。

const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);

const allSettledPromise = Promise.allSettled([resolved, rejected]);

allSettledPromise.then(function (results) {
  console.log(results);
});
// [
//    { status: 'fulfilled', value: 42 },
//    { status: 'rejected', reason: -1 }
// ]

简单实现Promise某些方法

promise.all

我们先简单理一下promise.all需要什么?

  • Promise.all的返回值是一个新的Promise实例

  • Promsie.all接受一个可遍历的数据容器,容器的每个元素都应该是Promise实例(假设这个容器是数组

  • 数组中的每个Promise实例都成功时(由pendding状态转化成fulfilled状态),Promise.all才成功。这些Promise实例所有的resolve结果会按照原来的顺序集合在一个数组作为Promise.all的resolve结果。

万一数组里面有元素不是 Promise咋办?那就得用 Promise.resolve() 把它办了(可以看上文 Promise.resolve()的作用)。这里还有一个问题,Promise 实例是不能直接调用 resolve 方法的,咱得在 .then() 中去收集结果。注意要保持结果的顺序。

将收集到的结果(数组arr)作为参数传给外层的 resolve 方法。这里咱们肯定是有一个判断条件的,如何判断所有 Promise 实例都成功了呢?(收集成功的个数) 创建一个计数器,每有一个 Promise 实例成功,计数器加一。

  • 数组中只要有一个Promise实例失败(由pendding状态转化成rejected状态),Promise.all就失败。Promise.all的.catch()会捕获到这个reject

  • 最后就是处理失败的情况了,这里有两种写法

第一种是用 .catch() 方法捕获失败(下面采用的)

第二种写法就是给 .then() 方法传入第二个参数,这个函数是处理错误的回调函数

Promise.MyAll=function(promises){
    let arr=[],count=0;
    return new Promise((resolve,reject)=>{
        promises.forEach((item,i)=>{
            Promise.resolve(item).then(res=>{
                arr[i]=res;
                count++;
                if(count===promises.length)resolve(arr)
            }).catch(reject)
        })
    })
}

promise.race

整体流程与 Promise.all差不多,只是对数组中的 Promise 实例处理的逻辑不一样,这里我们需要将最快改变状态的 Promise 结果作为 Promise.race 的结果。

Promise.MyRace=function(promises){
    return new Promise((resolve,reject)=>{
        for(const item of promsies){
            Promise.resolve(item).then(resolve,reject)
        }
    })
}

promise.any

Promise.any 与 Promise.all 可以看做是相反的。

Promise.any 中只要有一个 Promise 实例成功就成功,只有当所有的 Promise 实例失败时 Promise.any 才失败,此时Promise.any 会把所有的失败/错误集合在一起,返回一个失败的 promise 和AggregateError类型的实例。

如果 Promise.any 中有多个成功的 Promise 实例,则以最快成功的那个结果作为自身 resolve 的结果。

Promise.MyAny=function(promises){
    let arr=[],count=0;
    return new Promise((resolve,reject)=>{
        promises.forEach((item,i)=>{
            Promise.resolve(item).then(resolve,err=>{
                arr[i]={status:'rejected',val:err};
                count++;
                if(count===promises.length)reject(new Error("没有promise成功"))
            })
        })
    })
}

promise.allSettle

Promise 实例无论成功与否,都等它们异步操作结束了再继续执行下一步操作。

与 Promise.any 类似,Promise.allSettled 也给所有收集到的结果打上了标记。

而且 Promise.allSettled 是不会变成 rejected 状态的,不管一组 Promise 实例的各自结果如何,Promise.allSettled 都会转变为 fulfilled 状态

Promise.MyAllSettle=function(promises){
    let arr=[],count=0;
    return new Promise((resolve,reject)=>{
        promises.forEach((item,i)=>{
            Promise.resolve(item).then(res=>{
                arr[i]={status:'fulfilled',val:res}
                count++;
                if(count===promises.lenght)resolve(arr)
            },(err)=>{
                arr[i]={status:'rejected',val:err}
                count++;
               if(count===promises.lenght)resolve(arr)
            })
        })
    })
}

//封装优化一下
Promise.MyAllSettled = function (promises) {
    let arr = [], count = 0;
    return new Promise((resolve, reject) => {
       const processResult=(res,index,status){
           arr[index]={status:status,val:res};;
           count++;
           if(count===promises.length)resolve(arr);
       }
       promises.forEacch((item,i)=>{
           Promise.resolve(item).then(res=>{
               processResult(res,i,'fulfilled')
           },(err)=>{
               processResult(err,i,'rejected')
           })
       })
    })
}

完整的 Promises/A+ 实现

class Promise {
    // 用static创建静态属性,用来管理状态
    static PENDING = 'pending';
    static FULFILLED = 'fulfilled';
    static REJECTED = 'rejected';

    // 构造函数:通过new命令生成对象实例时,自动调用类的构造函数
    constructor(func) { // 给类的构造方法constructor添加一个参数func
        this.PromiseState = Promise.PENDING; // 指定Promise对象的状态属性 PromiseState,初始值为pending
        this.PromiseResult = null; // 指定Promise对象的结果 PromiseResult
        this.onFulfilledCallbacks = []; // 保存成功回调
        this.onRejectedCallbacks = []; // 保存失败回调
        try {
            /**
             * func()传入resolve和reject,
             * resolve()和reject()方法在外部调用,这里需要用bind修正一下this指向
             * new 对象实例时,自动执行func()
             */
            func(this.resolve.bind(this), this.reject.bind(this));
        } catch (error) {
            // 生成实例时(执行resolve和reject),如果报错,就把错误信息传入给reject()方法,并且直接执行reject()方法
            this.reject(error)
        }
    }

    resolve(result) { // result为成功态时接收的终值
        // 只能由pending状态 => fulfilled状态 (避免调用多次resolve reject)
        if (this.PromiseState === Promise.PENDING) {
            this.PromiseState = Promise.FULFILLED;
            this.PromiseResult = result;
            /**
             * 在执行resolve或者reject的时候,遍历自身的callbacks数组,
             * 看看数组里面有没有then那边 保留 过来的 待执行函数,
             * 然后逐个执行数组里面的函数,执行的时候会传入相应的参数
             */
            this.onFulfilledCallbacks.forEach(callback => {
                callback(result)
            })
        }
    }

    reject(reason) { // reason为拒绝态时接收的终值
        // 只能由pending状态 => rejected状态 (避免调用多次resolve reject)
        if (this.PromiseState === Promise.PENDING) {
            this.PromiseState = Promise.REJECTED;
            this.PromiseResult = reason;
            this.onRejectedCallbacks.forEach(callback => {
                callback(reason)
            })
        }
    }

    /**
     * [注册fulfilled状态/rejected状态对应的回调函数] 
     * @param {function} onFulfilled  fulfilled状态时 执行的函数
     * @param {function} onRejected  rejected状态时 执行的函数 
     * @returns {function} newPromsie  返回一个新的promise对象
     */
    then(onFulfilled, onRejected) {
        // 2.2.7规范 then 方法必须返回一个 promise 对象
        let promise2 = new Promise((resolve, reject) => {
            if (this.PromiseState === Promise.FULFILLED) {
                /**
                 * 为什么这里要加定时器setTimeout?
                 * 2.2.4规范 onFulfilled 和 onRejected 只有在执行环境堆栈仅包含平台代码时才可被调用 
                 * 注1
                 * 这里的平台代码指的是引擎、环境以及 promise 的实施代码。
                 * 实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。
                 * 这个事件队列可以采用“宏任务(macro-task)”机制,比如setTimeout 或者 setImmediate; 也可以采用“微任务(micro-task)”机制来实现, 比如 MutationObserver 或者process.nextTick。
                 */
                setTimeout(() => {
                    try {
                        if (typeof onFulfilled !== 'function') {
                            // 2.2.7.3规范 如果 onFulfilled 不是函数且 promise1 成功执行, promise2 必须成功执行并返回相同的值
                            resolve(this.PromiseResult);
                        } else {
                            // 2.2.7.1规范 如果 onFulfilled 或者 onRejected 返回一个值 x ,则运行下面的 Promise 解决过程:[[Resolve]](promise2, x),即运行resolvePromise()
                            let x = onFulfilled(this.PromiseResult);
                            resolvePromise(promise2, x, resolve, reject);
                        }
                    } catch (e) {
                        // 2.2.7.2规范 如果 onFulfilled 或者 onRejected 抛出一个异常 e ,则 promise2 必须拒绝执行,并返回拒因 e
                        reject(e); // 捕获前面onFulfilled中抛出的异常
                    }
                });
            } else if (this.PromiseState === Promise.REJECTED) {
                setTimeout(() => {
                    try {
                        if (typeof onRejected !== 'function') {
                            // 2.2.7.4规范 如果 onRejected 不是函数且 promise1 拒绝执行, promise2 必须拒绝执行并返回相同的据因
                            reject(this.PromiseResult);
                        } else {
                            let x = onRejected(this.PromiseResult);
                            resolvePromise(promise2, x, resolve, reject);
                        }
                    } catch (e) {
                        reject(e)
                    }
                });
            } else if (this.PromiseState === Promise.PENDING) {
                // pending 状态保存的 onFulfilled() 和 onRejected() 回调也要符合 2.2.7.1,2.2.7.2,2.2.7.3 和 2.2.7.4 规范
                this.onFulfilledCallbacks.push(() => {
                    setTimeout(() => {
                        try {
                            if (typeof onFulfilled !== 'function') {
                                resolve(this.PromiseResult);
                            } else {
                                let x = onFulfilled(this.PromiseResult);
                                resolvePromise(promise2, x, resolve, reject);
                            }
                        } catch (e) {
                            reject(e);
                        }
                    });
                });
                this.onRejectedCallbacks.push(() => {
                    setTimeout(() => {
                        try {
                            if (typeof onRejected !== 'function') {
                                reject(this.PromiseResult);
                            } else {
                                let x = onRejected(this.PromiseResult);
                                resolvePromise(promise2, x, resolve, reject);
                            }
                        } catch (e) {
                            reject(e);
                        }
                    });
                });
            }
        })

        return promise2
    }
    // 用于promise方法链时 捕获前面onFulfilled/onRejected抛出的异常
    catch = function (onRejected) {
        return this.then(null, onRejected);
    }
}

/**
 * 对resolve()、reject() 进行改造增强 针对resolve()和reject()中不同值情况 进行处理
 * @param  {promise} promise2 promise1.then方法返回的新的promise对象
 * @param  {[type]} x         promise1中onFulfilled或onRejected的返回值
 * @param  {[type]} resolve   promise2的resolve方法
 * @param  {[type]} reject    promise2的reject方法
 */
function resolvePromise(promise2, x, resolve, reject) {
    // 2.3.1规范 如果 promise 和 x 指向同一对象,以 TypeError 为据因拒绝执行 promise
    if (x === promise2) {
        throw new TypeError('Chaining cycle detected for promise');
    }

    if (x instanceof Promise) {
        /**
         * 2.3.2 如果 x 为 Promise ,则使 promise2 接受 x 的状态
         *       也就是继续执行x,如果执行的时候拿到一个y,还要继续解析y
         */
        x.then(y => {
            resolvePromise(promise2, y, resolve, reject)
        }, reject);
    } else if (x !== null && ((typeof x === 'object' || (typeof x === 'function')))) {
        // 2.3.3 如果 x 为对象或函数
        try {
            // 2.3.3.1 把 x.then 赋值给 then
            var then = x.then;
        } catch (e) {
            // 2.3.3.2 如果取 x.then 的值时抛出错误 e ,则以 e 为据因拒绝 promise
            return reject(e);
        }

        /**
         * 2.3.3.3 
         * 如果 then 是函数,将 x 作为函数的作用域 this 调用之。
         * 传递两个回调函数作为参数,
         * 第一个参数叫做 `resolvePromise` ,第二个参数叫做 `rejectPromise`
         */
        if (typeof then === 'function') {
            // 2.3.3.3.3 如果 resolvePromise 和 rejectPromise 均被调用,或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用
            let called = false; // 避免多次调用
            try {
                then.call(
                    x,
                    // 2.3.3.3.1 如果 resolvePromise 以值 y 为参数被调用,则运行 [[Resolve]](promise, y)
                    y => {
                        if (called) return;
                        called = true;
                        resolvePromise(promise2, y, resolve, reject);
                    },
                    // 2.3.3.3.2 如果 rejectPromise 以据因 r 为参数被调用,则以据因 r 拒绝 promise
                    r => {
                        if (called) return;
                        called = true;
                        reject(r);
                    }
                )
            } catch (e) {
                /**
                 * 2.3.3.3.4 如果调用 then 方法抛出了异常 e
                 * 2.3.3.3.4.1 如果 resolvePromise 或 rejectPromise 已经被调用,则忽略之
                 */
                if (called) return;
                called = true;

                // 2.3.3.3.4.2 否则以 e 为据因拒绝 promise
                reject(e);
            }
        } else {
            // 2.3.3.4 如果 then 不是函数,以 x 为参数执行 promise
            resolve(x);
        }
    } else {
        // 2.3.4 如果 x 不为对象或者函数,以 x 为参数执行 promise
        return resolve(x);
    }
}
/**
 * Promise.all Promise进行并行处理
 * 参数: promise对象组成的数组作为参数
 * 返回值: 返回一个Promise实例
 * 当这个数组里的所有promise对象全部变为resolve状态的时候,才会resolve。
 */

Promise.all = function (list) {
    return new Promise((resolve, reject) => {
        //返回值的集合
        let values = [];
        let count = 0;//负责计数
        for (let [i, p] of list.entries()) {
            //数组参数如果不是Promise实例,先调用promise.resolve
            this.resolve(p).then(res => {
                values[i] = res;
                count++;
                // 所有状态都变成fulfilled时返回的Promise状态就变成fulfilled
                if (count == list.length) resolve(values);
            }, err => {
                // 有一个被rejected时返回的Promise状态就变成rejected
                reject(err);
            })
        }
    })
}

/**
 * Promise.race
 * 参数: 接收 promise对象组成的数组作为参数
 * 返回值: 返回一个Promise实例
 * 只要有一个promise对象进入 FulFilled 或者 Rejected 状态的话,就会继续进行后面的处理(取决于哪一个更快)
 */
Promise.race = function (list) {
    return new Promise((resolve, reject) => {
        for (let p of list) {
            // 只要有一个实例率先改变状态,新的Promise的状态就跟着改变
            this.resolve(p).then(res => {
                resolve(res);
            }, err => {
                reject(err);
            })
        }
    })
}
//添加静态any方法
Promise.any = function (list) {
    let values = [];
    let count = 0;
    return new Promise((resolve, reject) => {
        for (let [i, p] of list.entries()) {
            this.resolve(p).then(res => {
                resolve(res);
            }, err => {
                values[i] = { status: REJECTED, val: err };
                count++;
                if (count === list.length) reject(new Error('没有promise成功'))
            })
        }
    })
}
//添加静态allSettle方法
Promise.allSettle = function (list) {
    let values = [], count = 0;
    return new Promise((resolve, reject) => {
        const processResult = (res, index, status) => {
            values[index] = { status: status, val: res };
            count++;
            if (count === list.length) resolve(values);
        }
        for (let [i, p] of list.entries()) {
            this.resolve(p).then(res => {
                processResult(res, i, FULFILLED);
            }, err => {
                processResult(err, i, REJECTED);
            })
        }
    })
}

Promise.resolve = function (value) {
    return new Promise(resolve => {
        resolve(value);
    });
}

Promise.reject = function (reason) {
    return new Promise((resolve, reject) => {
        reject(reason);
    });
}



/* var p1 = new Promise((resolve, reject) => {
    setTimeout(resolve, 1000, 'one');
});
var p2 = new Promise((resolve, reject) => {
    setTimeout(resolve, 2000, 'two');
});
var p3 = new Promise((resolve, reject) => {
    setTimeout(resolve, 3000, 'three');
});
var p4 = new Promise((resolve, reject) => {
    setTimeout(resolve, 4000, 'four');
});
var p5 = new Promise((resolve, reject) => {
    reject('reject');
});

// 测试all
Promise.all([p1, p2, p3, p4]).then(values => {
    console.log(values);
}, reason => {
    console.log(reason)
});

Promise.all([p1, p2, p3, p4, p5]).then(values => {
    console.log(values);
}, reason => {
    console.log(reason)
});

// 测试race
Promise.race([p1, p2, p3, p4]).then(values => {
    console.log(values);
}, reason => {
    console.log(reason)
});

// 测试any
Promise.any([p1, p2, p3, p4, p5]).then(values => {
    console.log(values);
}, reason => {
    console.log(reason)
});

//测试allSettle
Promise.allSettle([p1, p2, p3, p4, p5]).then(values => {
    console.log(values);
}, reason => {
    console.log(reason)
}); */

如何证明我们写的myPromise就符合 Promises/A+ 规范呢?测试!

  • 安装官方测试工具

我们使用Promises/A+官方的测试工具 promises-aplus-tests 来对我们的myPromise进行测试。

  • 到promise文件所在文件夹安装promises-aplus-tests:
npm install promises-aplus-tests
  • 使用 CommonJS 对外暴露 myPromise 类并且实现静态方法 deferred

要使用 promises-aplus-tests 这个工具测试,必须实现一个静态方法deferred(),官方对这个方法的定义如下:

Promise大杂烩🚀

意思就是:

我们要给自己手写的myPromise上实现一个静态方法deferred(),该方法要返回一个包含{ promise, resolve, reject }的对象:

  • promise 是一个处于pending状态的 Promsie。
  • resolve(value)value解决上面那个promise
  • reject(reason)reason拒绝上面那个promise
class Promise {
        ...
}

function resolvePromise(promise2, x, resolve, reject) { 
        ...
}

/**
 * 基于Promise实现Deferred的
 * Deferred和Promise的关系
 * - Deferred 拥有 Promise
 * - Deferred 具备对 Promise的状态进行操作的特权方法(resolve reject)
 *
 *参考jQuery.Deferred
 *url: http://api.jquery.com/category/deferred-object/
 */
Promise.deferred = function () { // 延迟对象
    let defer = {};
    defer.promise = new Promise((resolve, reject) => {
        defer.resolve = resolve;
        defer.reject = reject;
    });
    return defer;
}

/**
 * Promise/A+规范测试
 * npm i -g promises-aplus-tests
 * promises-aplus-tests Promise.js
 */

try {
    module.exports = Promise
} catch (e) {

}
  • 配置 package.json

我们实现了deferred 方法,也通过 CommonJS 对外暴露了myPromise,最后配置一下package.json就可以跑测试啦~😺

新建一个 package.json ,配置如下:

// package.json
{
  "scripts": {
    "test": "promises-aplus-tests Promise"
  },
  "dependencies": {
    "promises-aplus-tests": "^2.1.2"
  }
}

项目目录

Promise大杂烩🚀

就绪👏👏👏,出发!

执行测试命令:

npm run test

完美通过官方872个测试用例😜 快来看看我们的测试结果吧,走起 🚀

Promise大杂烩🚀

promise解决回调地狱

一开始我们就说promise可以解决回调地狱,那具体是怎么实现呢?

首先让我们举个实际例子:当我们发送网络请求的时候,需要拿到这次网络请求的数据,然后再发送新的网络请求,就这样重复三次,得到最终结果。

我们最先想到的可能是这样:

function requestData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (url.includes('iceweb')) {
        resolve(url)
      } else {
        reject('请求错误')
      }
    }, 1000);
  })
}


requestData('iceweb.io').then(res => {
  requestData(`iceweb.org ${res}`).then(res => {
    requestData(`iceweb.com ${res}`).then(res => {
      console.log(res)
    })
  })
})

//iceweb.com iceweb.org iceweb.io

但是这样的话虽然可以实现,但是已经形成多层代码的嵌套了,也就是我们一直说的回调地狱,可读性非常差。可能有的同学会说这样其实还行,但是如果需要请求的次数增加呢?只会嵌得更深。

我们也可以利用then的链式调用,返回一个新的promise。

function requestData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (url.includes('iceweb')) {
        resolve(url)
      } else {
        reject('请求错误')
      }
    }, 1000);
  })
}

requestData('iceweb.io').then(res => {
  return requestData(`iceweb.org ${res}`)
}).then(res => {
  return requestData(`iceweb.com ${res}`)
}).then(res => {
  console.log(res)
})

//iceweb.com iceweb.org iceweb.io

但是其实还有更好的方法,利用生成器与promise相结合!

function requestData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (url.includes('iceweb')) {
        resolve(url)
      } else {
        reject('请求错误')
      }
    }, 1000);
  })
}

function* getData(url) {
  const res1 = yield requestData(url)
  const res2 = yield requestData(res1)
  const res3 = yield requestData(res2)

  console.log(res3)
}

const generator = getData('iceweb.io')

generator.next().value.then(res1 => {
  generator.next(`iceweb.org ${res1}`).value.then(res2 => {
    generator.next(`iceweb.com ${res2}`).value.then(res3 => {
      generator.next(res3)
    })
  })
})

//iceweb.com iceweb.org iceweb.io

大家可以发现我们的getData已经变为同步的形式,可以拿到我最终的结果了。但是这样generator一直调用.next不是也产生了回调地狱吗?

其实这个问题我们可以解决,我们可以封装成一个自动化执行的函数,我们就不用关心内部是如何调用的了。

自动化执行函数封装

function requestData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (url.includes('iceweb')) {
        resolve(url)
      } else {
        reject('请求错误')
      }
    }, 1000);
  })
}

function* getData() {
  const res1 = yield requestData('iceweb.io')
  const res2 = yield requestData(`iceweb.org ${res1}`)
  const res3 = yield requestData(`iceweb.com ${res2}`)

  console.log(res3)
}

//自动化执行 async await相当于自动帮我们执行.next
function asyncAutomation(genFn) {
  const generator = genFn()

  const _automation = (result) => {
    let nextData = generator.next(result)
    if(nextData.done) return

    nextData.value.then(res => {
      _automation(res)
    })
  }

  _automation()
}

asyncAutomation(getData)

//iceweb.com iceweb.org iceweb.io

到这里,可能有人开始说了,利用promise+生成器的方式变相实现解决回调地狱问题,其实就是async await的一个变种而已。实际确实如此,async await核心代码内部主动帮我们调用.next方法。

那么我们可以直接用async await来解决回调问题

function requestData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (url.includes('iceweb')) {
        resolve(url)
      } else {
        reject('请求错误')
      }
    }, 1000);
  })
}

async function getData() {
  const res1 = await requestData('iceweb.io')
  const res2 = await requestData(`iceweb.org ${res1}`)
  const res3 = await requestData(`iceweb.com ${res2}`)

  console.log(res3)
}

getData()

//iceweb.com iceweb.org iceweb.io

一看,好像只要把getData生成器函数函数,改为async函数,yeild的关键字替换为await就可以实现异步代码同步写法了。

分片并发上传

上一个说的是后一个请求需要前一个请求的结果,那么连续发送多个请求可以怎么做呢?

那么假设:我需要同时发送10个请求,如何防止丢失请求或者请求阻塞问题的同时还能完成性能优化?

function axios() {
    return new Promise((resolve, reject) => {
        console.log('开始发送请求');
        setTimeout(() => {
            console.log('服务器响应了---------');
            resolve('结果')
        }, 1000);
    })
}

其实可以使用Promise的.then() 方法,但是这样的话请求会一个一个发送,会出现请求时间过长的问题。

axios().then(res=>{
    return axios()
}).then(res=>{
    return axios()
}).then(res=>{
    return axios()
}).then(res=>{
    return axios()
}).then(res=>{
    return axios()
}).then(res=>{
    return axios()
}).then(res=>{
    return axios()
}).then(res=>{
    return axios()
}).then(res=>{
    return axios()
}).then(res=>{
    return axios()
})

其实也可以用Promise.all,但是如果请求数量多,可能会出现请求丢失,请求阻塞的问题。

let tasts = [axios(),axios(),axios(),axios(),axios(),axios(),axios(),axios(),axios(),axios()];
// 使用这种方式,会出现请求丢失,请求堵塞的问题
Promise.all(tasts).then(res=>{
    console.log(res);
})

那,如果把Promise.all方法与.then()方法相结合呢?相对于前面两种,这个算是最优解。我们也可以叫做 分片并发上传

let arr= [
    [axios(),axios(),axios()],
    [axios(),axios(),axios(),axios()],
    [axios(),axios(),axios()],
]
 
let result = []
Promise.all(arr[0]).then(res=>{
    result = result.concat(res)
    return Promise.all(arr[1])
}).then(res=>{
    result = result.concat(res)
    return Promise.all(arr[2])
}).then(res=>{
    result = result.concat(res);
    console.log(result);
})

实现并发控制

在日常开发过程中,你可能会遇到并发控制的场景,比如控制请求并发数,当我们为了实现关注的人的个人主页秒开,就要在用户点击前要预请求关注者主页信息。假设有100个关注者。如果同时发起100个请求,可能会将带宽打满。部分设备可能还会有请求的限制,这样会阻塞原本页面的其他正常请求。所以我们得写一个池去维护最大并发量。

我们可以假设:有 10个待办任务要执行,而我们希望限制同时执行的任务个数,即最多只有 2 个任务能同时执行。当 正在执行任务列表 中的任何 1 个任务完成后,程序会自动从 待办任务列表 中获取新的待办任务并把该任务添加到 正在执行任务列表 中。

分析

任务调度器-控制任务的执行,当资源不足时将任务加入等待队列,当资源足够时,将等待队列中的任务取出执行。

在调度器中一般会有一个等待队列queue,存放当资源不够时等待执行的任务。

具有并发数据限制,假设通过max设置允许同时运行的任务,还需要count表示当前正在执行的任务数量。

当需要执行一个任务A时,先判断count==max 如果相等说明任务A不能执行,应该被阻塞,阻塞的任务放进queue中,等待任务调度器管理。

如果count<max说明正在执行的任务数没有达到最大容量,那么count++执行任务A,执行完毕后count--

此时如果queue中有值,说明之前有任务因为并发数量限制而被阻塞,现在count<max,任务调度器会将对头的任务弹出执行。

实现

阻塞的方法:采用new一个Promise对象,将resolve函数的引用推入队列queue中。只要resolve函数没有被执行,当前任务就会一直阻塞在这里


class Scheduler {
    constructor(max){
        this.max = max;
        this.count = 0 ;//用来记录当前正在执行的异步函数
        this.queue = new Array();//表示等待队列
    }
    async add(promiseCreator){
        /*
        此时count已经满了,不能执行本次add需要阻塞在这里,将resolve放入队列中等待唤醒,
        等到count<max时,从队列中取出执行resolve,执行,await执行完毕,本次add继续
        */
        if(this.count>=this.max) await new Promise((resolve,reject)=>this.queue.push(resolve))
           
        this.count ++;
        let res = await promiseCreator();
        this.count--;
        if(this.queue.length){//依次唤醒add
             // 若队列中有值,将其resolve弹出,并执行
            // 以便阻塞的任务,可以正常执行
            this.queue.shift()();
        }
        return res;
        
    }
  }
     
  const timeout = time => new Promise(resolve => {
    setTimeout(resolve, time);
  })
    
  const scheduler = new Scheduler(2);
    
  const addTask = (time,order) => {
    //add返回一个promise,参数也是一个promise
    scheduler.add(() => timeout(time)).then(()=>console.log(order))
  }
  
  addTask(1000, '1');
  addTask(500, '2');
  addTask(300, '3');
  addTask(400, '4');
  
// output: 2 3 1 4

其实有一个类似的库: async-pool ,以下是它的使用。

const timeout=i=>new Promsie(resolve=>setTimeout(()=>resolve(i),i))
await asyncPool(5,[1000,2000,3000,4000],timeout)

其中,asyncPool是实现并发控制的主体函数,形式如下

function asyncPool(poolLimit, array, iteratorFn){ ... }

参数介绍

  • poolLimit(Number):表示限制的并发数;
  • array(Array):表示任务数组;
  • iteratorFn(FunctionaAz):表示迭代函数,用于实现对每个任务项进行处理,该函数会返回一个 Promise 对象或异步函数。

async-pool的实现

实现的核心是通过Promise.all()Promise.race()配合

 async function asyncPool(poolLimit, array, iteratorFn) {
        const ret = []; // 存储所有的异步任务
        const executing = []; // 存储正在执行的异步任务
        for (const item of array) {
            // 调用iteratorFn函数创建异步任务
            const p = Promise.resolve().then(() => iteratorFn(item, array));
            ret.push(p); // 保存新的异步任务

            // 当poolLimit值小于或等于总任务个数时,进行并发控制
            if (poolLimit <= array.length) {
                // 当任务完成后,从正在执行的任务数组中移除已完成的任务
                const e = p.then(() => executing.splice(executing.indexOf(e), 1));
                executing.push(e); // 保存正在执行的异步任务
                if (executing.length >= poolLimit) {
                    await Promise.race(executing); // 等待较快的任务执行完成,才继续下一次循环
                }
            }
        }
        return Promise.all(ret);
    }
    const timeout = i => new Promise(resolve => setTimeout(() => resolve(i), i));
    // await asyncPool(2, [1000, 5000, 3000, 2000], timeout);
    const ans = await asyncPool(2, [1000, 5000, 3000, 2000], timeout);
    console.log(ans);

在以上代码中,充分利用了 Promise.all 和 Promise.race 函数特点,再结合 ES7 中提供的 async await 特性,最终实现了并发控制的功能。利用 await Promise.race(executing); 这行语句,我们会等待 正在执行任务列表 中较快的任务执行完成之后,才会继续执行下一次循环。

Promise异常能被 try catch捕获到吗?

不行!因为能捕捉到的异常必须是线程执行已经进入 try catch 但 try catch 未执行完的时候抛出来的!也就是说,异步无法捕获!

// 异步,微任务
try {
        new Promise(() => {
                throw new Error('new promise throw error');
        });
} catch (error) {
        console.log(error);
}

Promise大杂烩🚀

try-catch 主要用于捕获异常,注意,这里的异常,是指同步函数的异常,如果 try 里面的异步方法出现了异常,此时catch 是无法捕获到异常的。

原因:当异步函数抛出异常时,对于宏任务而言,执行函数时已经将该函数推入栈,此时并不在 try-catch 所在的栈,所以 try-catch 并不能捕获到错误。对于微任务而言,比如 promise,promise 的构造函数的异常只能被自带的 reject 也就是.catch 函数捕获到。

如果要捕获,我们要解决的问题就是,如何让promise的回调执行完的同时try,catch还没执行完

可以用async、await

async function fn() {
    try {
        await new Promise(() => {
            throw new Error('new promise throw error');
        });
    } catch (error) {
        console.log(error);
    }
}
fn()

Promise大杂烩🚀

这次Promise异常能被捕获到,是因为async和await

正常不加async,await的时候,执行promise后,在等待promise回调的时候,try,catch已经执行完了,所以捕获不到,然而加了async await后,try-catch必须等promise的回调执行完后,才能继续往下走,这个时候trycatch没执行完,promise抛出异常,自然而然能被catch捕获到。

不过,try,catch如果使用async和await,也是可以捕获到Promise异常的,但我们不推荐,Promise异常有它自己的.catch来捕获,而且更好用。

如下:

async function fn() {
    try {
        await new Promise(() => {
            throw new Error('new promise throw error');
        }).catch(error=>{
            console.log('.catch',error);
        })
    } catch (error) {
        console.log('try,catch',error);
    }
}
fn()

catch用来捕获promise异常,当我们有了.catch,错误在进入catch之前,就被.catch捕获到了,所以不会再往下走(.catch是Promise异常捕获机制,不带. 是try,catch) 代码运行结果如下

Promise大杂烩🚀

到这里,我们可以发现,这个错误被.catch捕获到了,那如果我们不抛出一个错误,直接返回一个reject 会怎么样呢

async function fn() {
    try {
        const res = await new Promise((resove,reject) => {
            return reject('捕获到了')
        }).catch(error=>{
            console.log('被promise.catch',error);
        })
    } catch (error) {
        console.log('被try,catch',error);
    }
}
fn()

Promise大杂烩🚀

Promise异常,如果写了.catch,就会被优先.catch捕获到,如果没有,那么就会被try,catch捕获到。

参考

Promise - JavaScript | MDN (mozilla.org)

JavaScript Promise (nodejs.cn)

阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版

看了就会,手写Promise原理,最通俗易懂的版本!!!

Promise.all和Promise.race的使用场景

Promise.then是如何实现链式调用的

字节飞书面试——请实现promise.all

Promise实现原理(附源码)

Promise详解与实现(Promise/A+规范)