JS基础系列之 —— Promise

·  阅读 284

什么是Promise?

Promise 是异步编程的一种解决方案

or

Promise 对象用于表示一个异步操作的最终完成 (或失败)及其结果值

异步方法并不会立即返回最终的值,而是会返回一个 promise,以便在未来某个时候把值交给使用者.

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

已兑现(fulfilled) [fʊlˈfɪld]: 意味着操作成功完成。

已拒绝(rejected)[rɪˈdʒektɪd]: 意味着操作失败。

Promise 本质是一个状态机。每个 promise 只能是 3 种状态中的一种:pending、fulfilled 或 rejected。状态转变只能是 pending -> fulfilled 或者 pending -> rejected。状态转变不可逆。

function timeout(ms) {
    return new Promise((resolve, reject) => {
      setTimeout(resolve('done'), ms);
    });
  }

  timeout(100).then((value) => {
    console.log(value);//done
  }); 
复制代码

而只能使用resolve()reject()来改变Promise对象的状态。

Promise解决了什么问题?

解决地狱回调: 可读性问题和信任问题

听起来很深奥,其实简单来说就是回调函数的嵌套

queryData('data1', function(ret) {
    console.log(ret);  
    queryData('data2', function(ret) {
        console.log(ret);
        queryData('data3', function(ret) {
            console.log(ret);
            queryData('data4', function(ret) {
                console.log(ret);  
                queryData('data5', function(ret) {
                    console.log(ret);
                    queryData('data6', function(ret) {
                        console.log(ret);
                        //此处省略无数嵌套
                    });
                });
            });
        });
    });
});
// 代码的耦合性就高了,一步错步步错
复制代码

使用Promise改进

queryData('data1')
    .then((ret) =>; {
        console.log(ret);
        return queryData('data2');
    })
    .then((ret) => {
        console.log(ret);
        return queryData('data3');
    })
    .then((ret) => {
        console.log(ret);
        return queryData('data4');
    })
复制代码

Promise有哪些API?

then()

then(成功调该函数, 失败调该函数)

getJSON("/post/1.json").then(function (comments) {
  console.log("resolved: ", comments);
}, function (err){
  console.log("rejected: ", err);
});
复制代码

then方法的第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数,它们都是可选的。也就是说,当Promise对象状态发生改变,也就是定型之后,then会立刻执行。

当Promise对象状态发生改变时,即resolvedrejected时,调用then()

还记得上面刚开始介绍Promise的时候我们做了一个setTimeout的例子吗,那里的resolve()里是传了参的,如果调用resolve函数和reject函数时带有参数,那么它们的参数会被传递给回调函数。

下面我们来思考一个问题,如果参数是另一个Promise对象的话,会怎么样呢?

const p1 = new Promise(function (resolve, reject) {
  // ...
});

const p2 = new Promise(function (resolve, reject) {
  // ...
  resolve(p1);
})
复制代码

上面代码中,p1p2都是 Promise 的实例,但是p2resolve方法将p1作为参数,即一个异步操作的结果是返回另一个异步操作。

const p1 = new Promise(function (resolve, reject) {
  setTimeout(() => reject(new Error('fail')), 3000)
})

const p2 = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(p1), 1000)
})

p2
  .then(result => console.log(result))
  .catch(error => console.log(error))
// Error: fail
复制代码

上面代码中,p1是一个 Promise,3 秒之后变为rejected。p2的状态在 1 秒之后改变,resolve方法返回的是p1。由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以,后面的then语句都变成针对后者(p1)。又过了 2 秒,p1变为rejected,导致触发catch方法指定的回调函数。

也就是说,p1的状态决定了p2的状态。如果p1的状态是pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是resolved或者rejected,那么p2的回调函数将会立刻执行。

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

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,就调用第二个回调函数。

我们再来思考一个问题,如果.then或者.catch的传参,不是函数而是非函数呢?

Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log) // 1
复制代码

.then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透。

catch()

catch()是then(null,失败调的函数)的别名,用于指定发生错误时的回调函数

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));
复制代码

不过它还有另外一个作用:在执行resolve 的回调(也就是上面then 中的第一个参数)时,如果抛出异常了(代码出错了),那么并不会报错卡死js,而是会进到这个catch方法中。

所以我们尽量使用catch代替then方法的第二个参数。

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

finally()

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

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
复制代码

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

all()

all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。

const p =Promise.all([p1,p2,p3]);
复制代码

(1)只有p1p2p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。 看谁跑得慢

(2)只要p1p2p3之中有一个变成rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。 躺地上不动了

// 假设我们创建了三个Promise对象,分别为promise1, promise2, promise3
// 并使用setTimeout返回了resolve(1)(2)(3)对应的等待ms为3000/2000/1000

Promise.all([promise1, promise2, promise3])
.then(res => {
    console.log('then'); // [1, 2, 3] 
})
.catch(error => {
    console.log('catch');
    console.log(error);
})
// 可以看到all()方法将传入的全部promise对象都执行了成功的返回值,通过数组的形式返回,
// 且只和传入的顺序有关,和其成功的顺序无关。
复制代码

看看失败的例子

// 这时候创建了Promise对象promise4,promise5并通过setTimeout返回了reject('失败')等待ms为1000/2000

Promise.all([promise1, promise2, promise3, promise4,promise5])
.then(res => {
    console.log('then');
    console.log(res);   
})
.catch(error => {
    console.log('catch');
    console.log(error);  // 失败
})
// 可以看到all()方法 只要有一个失败,那将返回失败。
// 如果有多个失败返回,那么哪个先传入返回哪个。
复制代码

当我们想等待全部Promise对象都完成后,再进行操作时,使用all方法非常合适。

race()

Promise.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。 最快结束比赛

let p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("success");
    }, 3000);
  });
  
  let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("failed");
    }, 2000);
  });
  
  Promise.race([p1, p2])
    .then((result) => {
      console.log(result);
    })
    .catch((error) => {
      console.log('网络状况不佳');
      console.log(error); // 打开的是 'failed'
    });
//   实际应用中的猜想就是,请求网路会因为网速问题,
//   响应时间会不同,如果设置定时器超过一定的时间,就显示网络不佳。
复制代码

allSettled()

用于接受一组 Promise 实例作为参数,该方法返回的新的 Promise 实例,一旦结束,状态总是成功的。新的promise实例是一个数组,里面包含了多个对象,只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。该方法由 ES2020 引入。 录入成绩

[
{status:'fulfilled',value:42},
{status:'rejected',reason:-1}
]
复制代码

当我们不关心异步操作的结果,只关心这些操作有没有结束时,all()方法就不能实现。

any()

ES2021 引入了Promise.any()方法。该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。

Promise.any() 方法依然是实验性的,尚未被所有的浏览器完全支持。它当前处于 TC39 第四阶段草案(Stage 4)

Promise.any()跟Promise.race()方法很像,只有一点不同,就是不会因为某个 Promise 变成rejected状态而结束。

var resolved = Promise.resolve(42);
var rejected = Promise.reject(-1);
var alsoRejected = Promise.reject(Infinity);

Promise.any([resolved, rejected, alsoRejected]).then(function (result) {
  console.log(result); // 42
});

Promise.any([rejected, alsoRejected]).catch(function (results) {
  console.log(results); // [-1, Infinity]
});
复制代码

Promise之微任务宏任务

我们先来熟悉一下JS的事件循环

我们都知道JavaScript是一个单进程的语言,同一时间不能处理多个任务,先执行同步在执行异步

微任务一定比宏任务先执行(微任务实际上是宏任务的其中一个步骤)

所以执行的步骤就是同步->异步(微任务->宏任务)

setTimeout 和 Promise 并不在一个异步队列中,前者属于宏任务(MacroTask),而后者属于微任务(MicroTask)

setTimeout(_ => console.log(4))//异步宏任务
new Promise(resolve => {
  console.log(1)//Promise在实例化的过程中所执行的代码都是同步进行的
  resolve()
}).then(_ => {
  console.log(3)//then则是具有代表性的微任务
})

console.log(2)//同步



// 这是一个同步任务
console.log('1')            --------> 直接被执行
                                      目前打印结果为:1

// 这是一个宏任务
setTimeout(function () {    --------> 整体的setTimeout被放进宏任务列表
  console.log('4')                    目前宏任务列表记为【c4】
});

new Promise(function (resolve) {
  // 这里是同步任务
  console.log('2');         --------> 直接被执行
  resolve();                          目前打印结果:12
  // then是一个微任务
}).then(function () {       --------> 整体的then[包含里面的setTimeout]被放进微任务列表
  console.log('3')                    目前微任务列表记为【c3】,目前打印结果 123
  setTimeout(function () {
    console.log('5')        -------->微任务里面如果有宏任务,那么会将宏任务放进任务列表
                                      目前宏任务列表记为【c4,c5】
  });
});
复制代码

async函数

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。

async 函数是什么?

  1. async/await是写异步代码的新方式,以前的方法有回调函数和Promise。

  2. async/await是基于Promise实现的,它不能用于普通的回调函数。

  3. async/await使得异步代码看起来像同步代码,这正是它的魔力所在。

  4. async/await解决了Promise可能出现的嵌套地狱。

async用于申明function异步,await用于等待一个异步方法执行完成

1、正常情况下,await命令后面是一个Promise对象。如果不是,会被转成一个立即resolve的Promise对象。

async function getData(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            var name = "syy";
            resolve(name)
        },1000)
    })
}
async function test(){
    var p = await getData();
    console.log(p);
};
test(); //syy



async function f() {
  return await 123;
}
f().then(v => console.log(v))//123
复制代码

2、await命令后面的 Promise 对象如果变为reject状态,则reject的参数会被.catch方法的回调函数接收到

//下面代码中,await语句前面没有return,但是reject方法的参数依然传入了catch方法的回调函数。这里如果在await前面加上return,效果是一样的。
async function f() {
  await Promise.reject('出错了');
}
f()
.then(v => 
    console.log(v)
)
.catch(e => 
    console.log(e)
)  //出错了
复制代码

3、Promise 对象的状态变化:async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

只要一个await语句后面的 Promise 变为reject,那么整个async函数都会中断执行

async function f() {  
    await Promise.reject('出错了');
    await Promise.resolve('hello world');   //不会执行}  
     //Promise {<rejected>: "出错了"}
复制代码

4、前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await放在try...catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行

 async function f() {
  try {
      await Promise.reject('出错了');
  } catch(e) {
  }
  return await Promise.resolve('hello world');
}
f()
.then(v => 
    console.log(v)
)  //hello world
 
 
//另一种方法:await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误
async function f() {
    await Promise.reject('出错了')
          .catch(e => 
              console.log(e)
          );  //出错了
    return await Promise.resolve('hello world');
}
f()
.then(v => 
    console.log(v)
)  //hello world
 
复制代码

5、如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject

async function f() {
    await new Promise(function (resolve, reject) {
        throw new Error('出错了');
    });
}
f()
.then(v => 
    console.log(v)
)
.catch(e => 
    console.log(e)
)  //Error:出错了
复制代码

6、多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发

let foo = await getFoo();
let bar = await getBar();
//上面代码中,getFoo和getBar是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo完成以后,才会执行getBar,完全可以让它们同时触发改进
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
//上面写法,getFoo和getBar都是同时触发,这样就会缩短程序的执行时间 
复制代码

当try...catch包裹在promise对象中的时候,使用.catch可以捕获到所有错误,当try...catch包裹在Promise对象外的时候,catch方法能够捕获全部,而.catch不能能捕获到reject之外的异常。

其实说白了就是try/catch捕获异步方法的话,它只会捕获第一次异常,其它的异步所抛出的异常它就捕获不到了。最好的办法就是在async/await中用.catch去捕获对应异步函数reject抛出的错误,然后用try/catch包裹整个代码执行逻辑,这样的话try/catch就可以捕获所有的异常了

const fn = (type, msg) => {
    return new Promise((resolve, reject) => {
        if (type) {
            resolve(`success:${msg}`)
        } else {
            reject(`fail:${msg}`)
        }
    })
}
// 捕获异步中的错误1
const asyncFn = async () => {
    try {
        let result1 = await fn(false, 'hello')
        console.log('中间内容输出')
        let result2 = await fn(false, 'world')
        console.log('result1' + result1)
        console.log('result2' + result2)
    } catch (error) {
        console.log('catch:' + error)
    }
}
// 捕获异步中的错误1
//catch:fail:hello

const asyncFn = async () => {
    try {
        await fn(false, 'hello').then(() => {}).catch(err => {
            console.log('result1:' + err)
        })
        console.log('中间内容输出')
        await fn(false, 'world').then(() => { }).catch(err => {
            console.log('result2:' + err)
        })
    } catch (error) {
        console.log('catch:' + error)
    }
}

asyncFn();
// 捕获异步中的错误2
//result1:fail:hello
//中间内容输出
//result2:fail:world
复制代码

参考文献

Promise到底解决了哪些问题?

Promise之Promise解决了什么问题

Promise - JavaScript | MDN

Promise 对象 - ECMAScript 6入门

JS事件循环

async...await用法全解_一只小绵羊-CSDN博客_async

async/await 异步方法使用.catch()还是try/catch捕获异常_Choicc的博客-CSDN博客

分类:
前端
收藏成功!
已添加到「」, 点击更改