文章目录
基本介绍
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了
Promise
对象。
所谓
Promise
,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
-
由
new Promise
构造器返回的promise
对象具有以下内部属性: -
state
—— 最初是"pending"
,- 然后在
resolve
被调用时变为"fulfilled"
, - 或者在
reject
被调用时变为"rejected"
。
- 然后在
-
result
—— 最初是undefined
,- 然后在
resolve(value)
被调用时变为value
, - 或者在
reject(error)
被调用时变为error
。
- 然后在
let p = new Promise((resolve, reject)=>resolve(88))
console.log(p)
let b = new Promise((resolve, reject)=>reject("报错"))
console.log(b)
- Promise 优缺点:
1. 对象的状态不受外界影响:
- 它有三种状态:`pending`(进行中)、`fulfilled`(已成功)和`rejected`(已失败)
- 只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态
2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果
- 状态改变情况:从 `pending` 变为 `fulfilled` 和 从 `pending` 变为 `rejected`
- 改变情况发生,状态就不会再变,会一直保持这个结果,这时就称为 `resolved`(已定型)
- 如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果
1. 一旦新建它就会立即执行,无法中途取消 Promise
2. 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部
3. 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)
- 为了行文方便,
resolve
统一只指fulfilled
状态,reject
统一只指rejected
状态。
基本用法
- Promise 参数:
1. Promise 的两个参数分别是resolve 和 reject 函数
2. 可以用`then`方法分别指定`resolved`状态和`rejected`状态的回调函数
3. `resolve` 函数的作用是:
在异步操作成功时调用,并将异步操作的结果,作为参数传递出去
4. `reject` 函数的作用是:
在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去
5. `then`方法可以接受两个回调函数作为参数:
第一个回调函数是 Promise 对象的状态变为 resolved 时调用
第二个回调函数是 Promise 对象的状态变为 rejected 时调用
- 语法:
new Promise(function(resolve,reject){
// resolve 表示成功的回调
// reject 表示失败的回调
}).then(function (res){
// 成功的函数
},function(err){
// 失败的函数
})
- 下面是一个用Promise对象实现的 Ajax 操作的例子
/*
getJSON是对 XMLHttpRequest 对象的封装:
用于发出一个针对 JSON 数据的 HTTP 请求,并且返回一个Promise对象
*/
const getJSON = function(url) {
// 在 getJSON 内部,resolve 函数和 reject 函数调用时,都带有参数
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});
对象方法
1. then()
- then 方法的作用是为 Promise 实例添加状态改变时的回调函数
第一个参数是 resolved 状态的回调函数
第二个参数是 rejected 状态的回调函数
- then方法返回的是一个新的Promise实例
- 因此可以采用链式写法,即then方法后面再调用另一个then方法
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// 第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数
});
2. catch()
catch()
方法是.then(null, rejection)
或.then(undefined, rejection)
的别名- 用于指定发生错误时的回调函数
getJSON('/posts.json').then(function(posts) {
// 状态变为resolve,调用then()方法里的函数
}).catch(function(error) {
// 处理 getJSON 和 前一个 then 的回调函数运行时发生的错误
console.log('发生错误!', error);
});
- 写法对比:
// 不推荐使用
// 建议总是使用catch()方法,而不使用then()方法的第二个参数
promise
.then(function(data) {
// success
}, function(err) {
// error
});
// 推荐使用,可以捕获前面then方法执行中的错误
// 更接近同步的写法(try/catch)
// 如果没有使用catch()方法指定错误处理的回调函数,
// Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});
3. finally()
finally()
方法是 ES2018 引入标准的,即 ES9- 用于指定不管 Promise 对象最后状态如何,都会执行的操作
promise
.then(result => {···}) // 成功执行的回调函数
.catch(error => {···}) // 失败执行的回调函数
.finally(() => {···}); // 最后执行的回调函数
finally
方法的回调函数不接受任何参数,因为里面的操作与状态无关,不依赖于 Promise 的执行结果。finally
方法总是会返回原来的值
// resolve 的值是 undefined
Promise.resolve(2).then(() => {}, () => {})
// resolve 的值是 2
Promise.resolve(2).finally(() => {})
// reject 的值是 undefined
Promise.reject(3).then(() => {}, () => {})
// reject 的值是 3
Promise.reject(3).finally(() => {})
静态方法
Promise 并非一开始就必须处于待定状态,然后通过执行器函数才能转换为落定状态
1、成功状态:Promise.resolve()
Promise.resolve()
方法:可以实例化一个解决fulfilled
状态的Promise
(将现有对象转为Promise
对象)
// 将 jQuery 生成的deferred对象,转为一个新的 Promise 对象
const jsPromise = Promise.resolve($.ajax('/whatever.json'));
- 下面两个
Promise
实例实际上是一样的
const p = new Promise(resolve => resolve('返回的结果'))
// 等价于
const p = Promise.resolve('返回的结果')
- Promise.resolve()方法的参数
1. 参数是一个 Promise 实例
如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。
2. 参数是具有 then() 方法的对象
将这个对象转为 Promise 对象,然后就立即执行里面的 then()方法
3. 参数不是具有 then()方法的对象,或根本就不是对象
返回一个新的 Promise 对象,状态为resolved
4. 不带有任何参数
直接返回一个resolved状态的 Promise 对象。
2、失败状态:Promise.reject()
Promise.reject()
方法:可以实例化一个解决rejected
状态的Promise
(将现有对象转为Promise
对象),并抛出一个错误(这个错误不能通过try/catch
捕获,只能通过拒绝处理程序捕获)
- 下面两个
Promise
实例实际上是一样的
const p = new Promise((resolve, reject) => reject('抛出的错误'))
// 等同于
const p = Promise.reject('抛出的错误');
Promise.reject()
方法的参数,会被传递给实例的回调函数
1. 示例 1:
// 生成一个 Promise 对象的实例p,状态为rejected,回调函数会立即执行
const p = Promise.reject('出错了');
p.then(null, function (s) {
console.log(s) // 出错了
});
2. 示例 2:
// 此时参数是一个字符串
Promise.reject('错误').catch(e => {
// 后面catch()方法的参数e就是这个字符串
console.log(e === '错误') // true
})
3、全胜一败:Promise.all()
Promise.all()方法:用于将多个 Promise 实例,包装成一个新的 Promise 实例
const p = Promise.all([p1, p2, p3]);
// 接受一个数组作为参数,p1、p2、p3都是 Promise 实例
// 如果不是,可以用Promise.resolve方法,将参数转为 Promise 实例
// 参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例
- p 的状态:
1. 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled
此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数
2. 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected
此时第一个被reject的实例的返回值,会传递给p的回调函数
- 注意,如果作为参数的 Promise 实例:
- 自己定义了 catch 方法,就不会触发 Promise.all() 的 catch 方法
- 自己未定义 catch 方法,就会调用 Promise.all() 的 catch 方法
// 生成一个Promise对象的数组,包含 6 个 Promise 实例
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON('/post/' + id + ".json");
});
Promise.all(promises).then(function (posts) {
// 只有这 6 个实例的状态都变成 fulfilled 调用这个回调函数
}).catch(function(reason){
// 其中有一个变为 rejected 调用这个回调函数,且6 个 Promise 实例中没有自带catch
});
4、一胜全败:Promise.any()
Promise.any()
:ES2021 引入
- 该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。
Promise.any([
fetch('https://v8.dev/').then(() => 'home'),
fetch('https://v8.dev/blog').then(() => 'blog'),
fetch('https://v8.dev/docs').then(() => 'docs')
]).then((first) => { // 只要有一个 fetch() 请求成功,包装实例就会变成fulfilled状态
console.log(first);
}).catch((error) => { // 所有三个 fetch() 全部请求失败,包装实例就会变成rejected状态
console.log(error);
});
- Promise()与await命令结合使用的例子
// Promise.any()方法的参数数组包含三个 Promise 操作
const promises = [
fetch('/endpoint-a').then(() => 'a'),
fetch('/endpoint-b').then(() => 'b'),
fetch('/endpoint-c').then(() => 'c'),
];
// 其中只要有一个变成fulfilled,Promise.any()返回的 Promise 对象就变成fulfilled
// 如果所有三个操作都变成rejected,那么await命令就会抛出错误
try {
const first = await Promise.any(promises);
console.log(first);
} catch (error) {
console.log(error);
}
- Promise.any()抛出的错误,是一个 AggregateError 实例,相当于一个数组,每个成员对应一个被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]
});
5、最快结束:Promise.race()
Promise.race()方法:同样是将多个 Promise 实例,包装成一个新的 Promise 实例
const p = Promise.race([p1, p2, p3]);
// Promise.race()方法的参数与Promise.all()方法一样
- p 的状态:
1. 只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变
2. 那个率先改变的 Promise 实例的返回值,就传递给p的回调函数
3. 意思就是说,p1, p2, p3里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态
- 一般情况下用不到这个方法
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
},1000)
})
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('failed')
}, 500)
})
Promise.race([p1, p2]).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error) // 打开的是 'failed'
})
6、全部结束:Promise.allSettled()
Promise.allSettled()
:ES2020 引入,Settled(固定的),包含了”fulfilled
“和”rejected
“两种情况。
Promise.allSettled()
方法:用来确定一组异步操作是否都结束了(不管成功或失败)。
- Promise.allSettled()方法接受一个数组作为参数,数组的每个成员都是一个 Promise 对象,并返回一个新的 Promise 对象。只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled还是rejected),返回的 Promise 对象才会发生状态变更
// 数组promises包含了三个请求
const promises = [
fetch('/api-1'),
fetch('/api-2'),
fetch('/api-3'),
];
await Promise.allSettled(promises);
// 只有等到这三个请求都结束了(不管请求成功还是失败),removeLoadingIndicator()才会执行
removeLoadingIndicator();
- 该方法返回的新的 Promise 实例,一旦发生状态变更,状态总是
fulfilled
,不会变成rejected
。状态变成fulfilled
后,它的回调函数会接收到一个数组作为参数,该数组的每个成员对应前面数组的每个 Promise 对象。
const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);
// allSettledPromise,状态只可能变成 fulfilled
const allSettledPromise = Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function (results) {
// 它的回调函数接收到的参数是数组results
console.log(results);
});
// 该数组的每个成员都是一个对象,对应传入Promise.allSettled()的数组里面的两个 Promise 对象。
// [
// { status: 'fulfilled', value: 42 },
// { status: 'rejected', reason: -1 }
// ]
- results的每个成员是一个对象,对象的格式是固定的,对应异步操作的结果。
/*
成员对象的status属性的值只可能是字符串fulfilled或字符串rejected
用来区分异步操作是成功还是失败
如果是成功(fulfilled),对象会有value属性,
如果是失败(rejected),会有reason属性,
对应两种状态时前面异步操作的返回值。
*/
// 异步操作成功时
{status: 'fulfilled', value: value}
// 异步操作失败时
{status: 'rejected', reason: reason}
- 返回值的用法例子
const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.allSettled(promises);
// 过滤出成功的请求
const successfulPromises = results.filter(p => p.status === 'fulfilled');
// 过滤出失败的请求,并输出原因
const errors = results
.filter(p => p.status === 'rejected')
.map(p => p.reason);
案例分析
-
promise
是承诺的意思,是一个专门用来解决异步 回调地狱 的问题 -
我们的每一个异步事件,在执行的时候都会有三个状态:执行中、成功、失败
-
因为他包含了成功的回调函数
-
所以我们就可以使用 promise 来解决多个 AJAX 发送的问题
-
设置3个PHP文件
# a.php
<?php
echo json_encode(array('n1'=>12,'n2'=>56));
?>
# b.php
<?php
$n1 = $_GET['n1'];
$n2 = $_GET['n2'];
$result = $n1+$n2;
echo json_encode(array('n1'=>$result,'n2'=>4));
?>
# c.php
<?php
$n1 = $_GET['n1'];
$n2 = $_GET['n2'];
$result = $n1*$n2;
echo json_encode(array('n1'=>$result,'n2'=>78));
?>
- ajax发送请求
/*
需求:
1 发送一个请求到a.php
==> 能得到一个结果
==> 有两个数字
2 在第一个请求结束之后,发送第二个请求到b.php
==> 需要携带第一个请求返回的结果: "./b.php?n1=12&n2=56"
==> 能得到一个结果是两个数字的和,和另外一个数字
3 在第二个请求结束之后,发送第三个请求到c.php
==> 需要携带第二个请求返回的结果: "./c.php?n1=68&n2=4"
==> 能得到一个结果是两个数字的乘积,和另外一个数字
*/
new Promise(function (resolve) {
// 1 发送一个请求到a.php
var xhr = new XMLHttpRequest();
xhr.open('get','./a.php');
xhr.onreadystatechange = function(){
if(xhr.readyState==4&&xhr.status==200){
resolve(xhr.response);
}
}
xhr.send();
}).then(function(data){
// 第一个请求a.php成功的回到函数
// data:xhr.response
var json = JSON.parse(data);
return new Promise(function(resolve){
// 2 发送第二个请求到b.php
var xhr = new XMLHttpRequest();
xhr.open('GET',`./b.php?n1=${json.n1}&n2=${json.n2}`);
xhr.onreadystatechange = function(){
if(xhr.readyState==4&&xhr.status==200){
// 要执行成功的回调函数
resolve(xhr.response);
}
}
xhr.send();
})
}).then(function(data){
// data就是第二次请求到b.php返回的内容:xhr.response
var json = JSON.parse(data);
return new Promise(function(resolve){
// 3 发送第三个请求到c.php
var xhr = new XMLHttpRequest();
xhr.open('get',`./c.php?n1=${json.n1}&n2=${json.n2}`);
xhr.onreadystatechange = function(){
if(xhr.status==200&&xhr.readyState==4){
// 执行成功的回调函数
resolve(xhr.response);
}
}
xhr.send();
})
}).then(function(data){
// data就是第三次请求到的c.php返回的内容:xhr.response
console.log(data); // {"n1":272,"n2":78}
})
- 封装函数:ajax-promise.js
// GET 方式发送的请求
function getSend(url){
var p = new Promise(function(resolve){
var xhr = new XMLHttpRequest();
xhr.open('GET',url);
xhr.onreadystatechange = function(){
if(xhr.readyState==4&&xhr.status==200){
resolove(xhr.responseText);
}
}
xhr.send();
});
return p;
}
// POST 方式发送的请求
function postSend(url,params){
var p = new Promise(function(resolve){
var xhr = new XMLHttpRequest();
xhr.open('POST',url);
xhr.onreadystatechange = function(){
if(xhr.readyState==4&&xhr.status==200){
resolve(xhr.responseText);
}
}
xhr.setRequsetHeader('content-type','application/x-www-form-urlencoded');
xhr.send(params);
});
return p;
}
- 利用封装函数发送请求
// 1 发送一个请求到a.php
getSend('./a.php').then(function(data){
var json = JSON.parse(data);
// 2 发送一个请求到b.php
return getSend(`./b.php?n1=${json.n1}&n2=${json.n2}`);
}).then(function(data){
var json = JSON.parse(data);
// 3 发送一个请求到c.php
return getSend(`./c.php?n1=${json.n1}&n2=${json.n2}`);
}).then(function(data){
console.log(data); // {"n1":272,"n2":78}
});
- 这个时候,我们的代码已经改观了很多了
- 基本已经可以维护了
- 但是对于一个程序员来说,这个样子是不够的
- 我们还需要更加的简化代码,怎么简单怎么来
- 所以我们就需要用到一个 es8 的语法:async/await
- async/await 是回调地狱的终极解决方案
- 把异步代码写的看起来像同步代码
- 只要是一个 promiser 对象,那么我们就可以使用
async/await
来书写
async function fn() {
// 1 发送一个请求到a.php
var data = await getSend('./a.php');
var json = JSON.parse(data);
// 2 发送第二个请求到b.php
data = await getSend(`./b.php?n1=${json.n1}&n2=${json.n2}`);
json = JSON.parse(data);
// 3 发送第三个请求到c.php
data = await getSend(`./c.php?n1=${json.n1}&n2=${json.n2}`);
console.log(data); // {"n1":272,"n2":78}
}
fn();
- 这样的异步代码写的就看起来像一个同步代码了