最近在回顾ES6的知识,想整理下跟Promise相关的内容。我准备整一个Promise解读的系列,看看能深入到什么程度吧。由浅入深,先认识下Promise。
痛苦的回忆:回调地狱
假如现在让你维护一个“古老”的项目,缺少脚手架的加持,只能使用es5的语法来开发,我相信你一定会深刻的体会到es6添加Promise是多么伟大的行为。在es5的时代,异步编程需要依赖回调函数来实现,这就会导致一个问题:回调地狱(callback hell),代码结构会变成复杂,可读性、可维护性差到极致。例如:如果有一个业务需要连续调用三个接口,那么就需要这么写:
ajax('api1', function(res1){if(res1.success) {ajax('api2', function(res2) {if (res2.success) {ajax('api3', function(res3){if(res3.success) {// dosomething....}});}})}
})
如果场景更复杂的话,可能就会变成这样:
除了接口调用,事件、异步加载等等需要等待的操作基本都需要通过回调来实现。整个项目中充斥着这样的代码基本就只能靠记忆去维护。
Promise是什么?
es6的规范中新增Promise对象,是异步编程的一个解决方案。Promise相当于提供一个容器,这个容器会封装一个单独的“时间线”,这个独立的“时间线”与主线程的执行是并行,当容器内发生变化的时候,容器会更新其状态,外部可以通过API获取到状态变更的结果,并且这个状态会被固定在容器中,保持不变,无论什么时候都可以获取。
Promise对象提供了更清晰的异步操作处理,可以将异步操作封装到Promise内,通过统一的方法来处理异步操作。当包装在Promise中的异步操作有结果时,Promise对象会根据异步操作的结果(正确或是错误)更新状态,并返回结果,我们可以通过Promise的then
或者catch
方法来获取异步操作的结果。针对上面的多个异步串联的操作,Promise对象提供链式调用的方式,通过then方法将多个异步操作串联起来,形成一个操作序列。这种方式可以避免回调地狱,将多层嵌套变成使得代码更加清晰和易于理解。针对其他多个异步操作的场景,Promise对象还提供了一些静态方法,如Promise.all和Promise.race,用于处理多个异步操作的结果。这些功能使得Promise对象成为处理异步操作的强大工具,能够大大简化异步操作的处理流程,提高代码的可维护性和可读性。
Promise的状态
Promise通过状态来定义包裹的异步操作的目前所处的阶段,Promise提供了三个互斥的状态:fulfilled(已成功) 、rejected(已失败)、pending(进行中),任何Promise对象都处于这三种状态之一。
当我们创建一个Promise对象时,这个Promise对象处于pending状态,再异步操作产生结果后,会有以下两种状态的变化:
- 异步操作成功:pending转换到fulfilled
- 异步操作失败:pending转换到rejected
这个状态一旦变更,就不会再改变,而且无论是什么时候都可以获取到对应的结果。就是说在Promise对象的状态发生变化后,在任何时候都可以为Promise添加回调函数都可以会立即得到这个结果。这与事件是完全不同,在事件的模式下,如果你错过了它,就无法在获取结果。
Promise的基础用法
按照ES6的规范,Promise对象内置的Promise
构造函数来实例化:
let promise = new Promise(function(resolve, reject) {// do sync resolve(); // 将promise的状态置为fulfilled(已成功)reject(); // 将promise的状态置为rejected(已失败)
});
这时,我们就得到一个`Promise`对象的实例:`promise`,然后通过这个实例,可以在任何时候取得异步操作的结果:
promise.then(() => {// do something
}).catch((error) => {console.error(error);
});
通过then
和catch
两个方法分别捕获Promise对象中包裹的异步操作的结果和错误。
这里需要注意的是,异步操作结束后会通过resolve
和reject
来修改实例的状态。那么这里就不一定是真正反应异步操作的实际结果,而是异步操作想要外界知道的结果。这样说可能有点绕,举个例子,在请求一个接口时,请求是一个异步动作,按照ajax工具思路,应该是根据状态码来修改状态。但是在实际操作中,接口返回200,但是内容是操作失败,虽然这里的接口是请求成功(就http请求而言),但是在业务上是失败的。一般这里也会直接处理成失败的状态而不是成功状态(这就是异步操作真正想要给外界的结果)。
先整个例子
上面是Promise的基础用法,为了更好的说明,这里我先整个例子:现在假设在某个业务系统中,所有接口的请求的返回格式都是:
{"errCode": 0, // 0 成功 1 失败"data": [], // 数据"message": '' // 信息
}
我们通过Promise
+ XmlHttpRequest
简单封装一个ajax方法:
function createUrlParams(data) {return Object.keys(data).map((v) => {return `${v}=${data[v]}`;}).join('&');
}function ajax(url = '', data = {}, type = 'GET') {return new Promise((resolve, reject) => {const xhr = new XMLHttpRequest();type = type.toUpperCase();if (type === 'GET') {url += '?' + createUrlParams(data);data = null;}xhr.open(type, url, true);xhr.onload = function() {if (xhr.status >= 200 && xhr.status < 300) {let res = JSON.parse(xhr.response);if (res.errCode !== 0) {reject({status:'error', result: res});} else {resolve({status:'success', result: res})}} else {reject(`请求失败:${xhr.status}`);}}xhr.onerror = function () {reject(`请求失败:${xhr.status}`);};xhr.send(data);});
}
上面的ajax方法最终会返回一个Promise实例,在请求完成的时候根据请求的状态和结果修改Promise的状态。例如获取用户的列表可以这么写:
ajax('http://xxxx.com/user/get-list')
.then(res => {console.log(res);
}).catch(err => {console.error(err);
});
基础方法说明
then方法
上面讲到我们可以通过then
方法来获取异步的成功的结果,但其实then
方法是接收两个回调方法,如果Promise状态为fulfilled
则执行第一个回调方法,失败则执行第二个:
ajax('http://xxxx.com/user/get-list').then(() => {console.log('success');},() => {console.log('error');});
不管成功还是失败,then
都会返回一个新的promise:
我们先修改下上面的代码,以便更好的观察:
const pAjax = ajax('http://xxxx.com/user/get-list');
const p1 = pAjax.then(() => {console.log('success');},() => {console.log('error');}
);
如果没有特殊操作(这个等会讲),不管成功失败,这个新的promise的状态会被置为fulfilled:
const p2 = p1.then(() => {console.log('success 2');
});
从上面的例子可以看出,在then
的回调函数中我们并没有返回任何东西,但是then
依然会返回一个promise。
例如这样:
const p2 = p1.then().then(() => {console.log('success');});
这是then
的机制,如果回调函数中没有return
的话,则会默认返回一个成功的promise,如果回调函数中有return
,则会判断return
是否为一个promise,如果不是promise,则会将其包装成promise返回,并且状态为成功。如果是promise,则直接返回,链式调用后面的方法会根据这个promise的状态触发对应的回调函数。
返回非promise
const p1 = pAjax.then(() => {return {result: 'success'};},() => {return {result: 'error'};}
);const p2 = p1.then((res) => {console.log(res);
});
返回promise
可以看到,如果then
中返回的是promise,链式后面的函数会等待这个promise的状态变更,再根据promise的状态来执行对应的回调。
状态传递
从上面的例子也可以看到,then
主要是受到上一个promise的影响,在pAjax
错误的时候,p1
会触发错误回调,而p2
触发的时候成功的回调。那么如果p1
没有捕获错误的话,错误会在p2
被捕获到吗?
const p1 = pAjax.then(() => {console.log('success');}
);const p2 = p1.then(() => {console.log('success');}, () => {console.log('error');}
)
可以看到,p2
捕获到错误,说明错误是会向下传递的。
总结
then
函数支持传入两个回调方法,分别为成功的回调和失败的回调,then
会根据回调函数的返回值返回一个新的promise,如果返回值不是promise,封装成promise返回,如果是promise则直接返回。如果then
没有捕获错误,则会将错误向下传递,这是新的promise的状态为失败。
catch方法
catch
方法其实是then(null/undefined, reject())
的变相写法,在指定发生错误的时候执行回调函数。
const pAjax = ajax('http://xxxx.com/user/get-list');
const p1 = pAjax.then(() => {console.log('success');}).catch((err) => {console.log(err);});
因为catch
是then
捕获错误的变相写法,所以then
具备的特性catch
都具备,这里就不多赘述了。
值得一提的是,跟传统的try/catch不一样,在promise中产生的错误,只能在promise的链路上捕获到,不会扩散到外部:
function err() {throw new Error('error');
}try {err(); // 抛出错误console.log('next'); // 不执行,被错误阻塞
} catch(err) {console.log(err); // error对象
}
try {promise = ajax('http://xxxx.com/user/get-list'); // 抛出错误setTimeout(() => {console.log('next');}, 5000); // 5s后仍执行
} catch(err) {console.log(err); // 无法捕获到错误
}
另外,如果在promise中抛出错误,但是没有执行reject
方法,promise还是会将状态处理成rejected
:
function err(){return new Promise((resolve, reject) => {throw new Error('promise error');});
}err().catch(err => {console.log(err); // Error: promise error
});
但是,如果已经修改了promise的状态,再抛出错误,则是无效的:
function err(){return new Promise((resolve, reject) => {resolve('success');throw new Error('promise error'); // 无效});
}err().then((res) => {console.log(res); // success
}).catch(err => {console.log(err); // 无
});
需要注意的,如果已经使用catch
捕获了错误,那么错误就不会在向下传递,除非前面的catch
将错误重新抛出。
ajax('http://xxxx.com/user/get-list').catch(err => {console.log(err); // error信息}).catch(err => {console.log(err); // undefined});
ajax('http://xxxx.com/user/get-list').catch(err => {throw err; // 也可以写成return Promise.reject(err); }).catch(err => {console.log(err); // error信息});
编写建议
前面讲到,promise的链路上错误是会向下传递的,所以呢,我建议promise的写法是按照需要在指定位置使用 catch
捕获错误,不要再then
中使用rejected
的回调去捕获错误,这样整体的可读性更高:
ajax('http://xxxx.com/user/get-list').then(() => {// 步骤1}).then(() => {// 步骤2}) // ....catch((err) => {console.log(err);});
这样看起来更像同步的写法,更清晰可读性更高。
如果是多个promise嵌套调用的话,建议只在最外层捕获错误,这样可以在统一的地方处理错误,埋日志,错误调试等操作都会简化。当然如果不希望中间某些操作的错误终端整个promise,可以在这些操作上添加catch拦截掉错误。
这里假设是一个添加用户的场景,有几个步骤:
- 获取录入的用户名(正常是同步的,这里假设是一个promise)
- 调用接口校验用户名是否重复
- 提交用户名
- 刷新用户列表
function getForm().then(() => {}){return form();
}function ApiCheckName(name) {return ajax('http://xxxx.com/user/check-name', {name}).then(isExists => {if (isExists) {throw new Error('用户名已存在');} else {return true;}})
}function ApiAddUser(data) {return ApiCheckName(data.name).then(() => {return ajax('http://xxxx.com/user/add', data, 'POST');});
}function ApiGetUserList() {return ajax('http://xxxx.com/user/get-list');
}// 封装添加用户的方法
function addUser() {return getForm().then((form) => {return ApiAddUser({name: 'skkk'});}).then(() => {return getUserList();});
}// 外部调用
addUser()
.catch((err) => {console.log('失败', err);
})
finally方法
finally
方法是在ES2018规范引入的,所以在一些版本较低的浏览器是不支持的。finally
方法是不管promise最终是成功还是失败,都会执行该方法。
ajax('http://xxxx.com/user/get-list').then(() => {console.log('success');}).catch(() => {console.log('error');}).finally(() => {console.log('finally');})
如上面的代码,无论接口请求是否成功,都会执行finally
。
finally
本质上还是then
的变种,其实现的效果等于与在then
的成功失败回调中分别添加相同的代码:
ajax('http://xxxx.com/user/get-list').finally(() => {dosomething();
});
等同于:
ajax('http://xxxx.com/user/get-list').then(() => {dosomething();}, () => {dosomething();}
)
明显finally
更简洁。
与then
不同的是finally
是不接收参数的。
ajax('http://xxxx.com/user/get-list').then(() => { return 1;}).catch(() => { return 2; }).finally((res) => {console.log(res);}) // undefined
这样我们无法通过传参来判断promise最终的状态,从而进行一些针对的操作。但是还是可以用不过来做一些终结操作。例如:实现一个带进度条的下载功能,不管下载成功或失败都要关闭重置进度条,就可以在finally
中实现。
同样的,finally
也是返回一个promise
const pAjax = ajax('http://xxxx.com/user/get-list');
const p1 = pAjax.then(() => { return 1;}).catch(() => { return 2; });
const p2 = p1.finally(res => {console.log(res);});
const p3 = p2.then(res => {console.log(res);});
可以看到返回值会越过finally
传递。
同样,状态也是会向下传递的
const pAjax = ajax('http://xxxx.com/user/get-list');
console.log('pAjax ====>', pAjax);
const p1 = pAjax.finally(res => {console.log(res);});
console.log('p1 ====>', p1);
const p2 = p1.then(() => {console.log('success');},() => {console.log('error');}
);
console.log('p2 ====>', p2);
除了不接收参数,finally
里返回的参数或成功的promise都也不会被传递下去:
const pAjax = ajax('http://localhost:3010');
const p1 = pAjax.finally(() => {return 'finally';
});
const p2 = p1.then((res) => {console.log('p2 res', res);},(res) => {console.log('p2 res', res);}
);
const p3 = pAjax.finally(() => {return Promise.resolve('finally');
});
const p4 = p3.then((res) => {console.log('p4 res', res);},(res) => {console.log('p4 res', res);}
);
p2
和p4
都获取到pAjax
的结果。
但是如果finally
返回了Promise或抛出错误就会向下传递:
const p1 = pAjax.finally(() => {throw new Error('finally error'); // 抛出错误
});
const p2 = p1.then((res) => {console.log('p2 res',res);},(res) => {console.log('p2 res',res);}
);
const p3 = pAjax.finally(() => {throw Promise.reject('finally'); // 返回成功的promise
});
const p4 = p3.then((res) => {console.log('p4 res',res);},(res) => {console.log('p4 res',res);}
);
可以看到,无论pAjax
是否成功的,但是p2
和p4
都是捕获到p1
和p3
抛出的错误。
当然,实际的开发中千万别这么写,finally
就做一些统一的终结操作就行了。如果统一操作也是promise的话,建议单独处理。
Promise.resolve() & Promise.reject()
如果想要返回一个promise,但又不想使用new Promise
的方式来实现,就可以使用这两个方法。基本上其功能与字面意义一致,Promise.resolve()
会直接返回一个状态为fulfilled
的promise,Promise.reject()
则返回一个状态为rejected
的promise。
Promise.resolve
上面说的是基本功能,不带任何参数会返回一个成功的promise。但是resolve
方法可以接受一个参数,其会根据这个参数的不同返回不同的promise。
- 传入promise实例
传入promise实例,会直接返回这个实例,不做任何修改。
const p = Promise.resolve(ajax('http://xxxx.com/user/get-list'));
p.then(res => {console.log(res); // {status: 'success', result: {…}}
});
- 传入thenable对象
thenable对象就是带then
方法的对象,像这样的:
{then: function(resolve, reject){setTimeout(() => {resolve('then');}, 500);}
}
将这样的对象传给Promise.resolve()
,它会将其转换为promise对象,并立刻执行then函数。像这样:
const p = Promise.resolve({then(resolve, reject) {setTimeout(() => {resolve('thenable');}, 500);}
});
p.then(res => {console.log(res); // thenable
});
那then
没有resolve或reject,只是一个方法甚至不是一个方法会是什么情况呢?
const p1 = Promise.resolve({then() {console.log('then function');return 'then';}
});
p1.then(res => {console.log('p1', res);
});const p2 = Promise.resolve({then: {a: 1}
});
p2.then(res => {console.log('p2', res);
});const p3 = Promise.resolve({then: '1'
});
p3.then(res => {console.log('p3', res);
});
可以看到,如果then
方法没有修改promise的状态的话,then
方法会被执行,但是返回的promise会处于pending
的状态。而then
是非方法的话,会返回一个fulfilled
的promise,对象被当做返回值返回。
- 其他类型的参数
传入对象、数组、字符串、数字等非方法类的参数,Promise.resolve
会立即返回一个fulfilled
的promise,并且将参数作为返回值。
Promise.resolve([1,2,3]).then(res => {console.log(res); // [1,2,3]})
传入的参数是一个非promise的方法,会获取该方法的返回值(如果有)做为返回值,同样也是返回一个状态为
fulfilled
的promise。
function p1() {console.log('function p1'); // function p1return 'p1';
}Promise.resolve(p1()).then(res => {console.log(res); // p1})function p2() {console.log('function p2'); // function p2
}Promise.resolve(p2()).then(res => {console.log(res); // undefined})
如果在方法中抛出错误呢?会不会走到catch呢?
function p1() {throw new Error('p1 error');
}Promise.resolve(p1()).then(res => {console.log(res);}).catch(err => {console.log(err);});
答案是啥都没执行,因为p1
在执行是就抛出错误了,后续代码终止执行。
Promise.reject
reject
无论传不传入都是返回一个状态为rejected
的promise。传入的任何参数都会做为失败的理由返回。
// 传入对象
Promise.reject({name: 'reject'}).catch(err => {console.log('object', err); // object {name: 'reject'}});
// 传入字符串
Promise.reject('reject').catch(err => {console.log('string', err); // string reject});function p1() {console.log('function p1'); // function p1return 'p1';
}// 有返回值的方法
Promise.reject(p1()).catch(err => {console.log('p1', err); // p1 p1});function p2() {console.log('function p2'); // function p2
}// 无返回值的方法
Promise.reject(p2()).catch(err => {console.log('p2', err); // p2 undefined});// promise
Promise.reject(ajax('http://xxxx.com/user/get-list')).catch(err => {console.log('ajax', err); // ajax promise});
这里跟resolve
不一样的是,传入的参数是promise,也会将promise实例理由返回。而不是返回promise实例的结果。