promise是什么
promise 是异步编程的一种解决方案:
从语法上,promise是一个对象,从它可以获取异步操作的消息;
从功能上,promise对象是用来封装一个异步操作并可以获取其结果数据
从意义上,它是承诺,承诺过一段时间会给你一个结果;
promise的优点
解决回调导致的代码难以维护
在使用js时,为了实现某些逻辑经常会写出层层嵌套的回调函数,如果嵌套过多,会极大影响代码可读性和逻辑,这种情况也被称为回调地狱:
// 优化前
var sayhello = function (name, callback) {
setTimeout(function () {
console.log(name);
callback();
}, 1000);
};
sayhello("first", function () {
sayhello("second", function () {
sayhello("third", function () {
console.log("end");
});
});
});
// 优化后
var sayhello = function (name) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log(name);
resolve(); //在异步操作执行完后执行 resolve() 函数
}, 1000);
});
};
sayhello("first")
.then(function () {
return sayhello("second"); //仍然返回一个 Promise 对象
})
.then(function () {
return sayhello("third");
})
.then(function () {
console.log("end");
});
可读性
promise规范了这种异步场景的代码,相对于callback的方式,更加清晰易懂,语义化更强
可靠性
promise是原生支持的API,在各大浏览器中的运行机制是相同的,这保证了它的可靠性
信任问题
只能决议一次,决议值只能有一个,决议后无法改变;
任何then中的回调也只能被调用一次
promise的三种状态
- pending:初始状态,既不是成功,也不是失败状态
- fulfilled:操作成功
- rejected:操作失败
pending 状态的 Promise 对象可能会变为 fulfilled 状态并传递一个值给相应的状态处理方法,也可能变为失败状态 (rejected) 并传递失败信息。当其中任一种情况出现时,Promise 对象的 then 方法绑定的处理方法就会被调用
兼容性
对低版本不支持的浏览器可以使用开源的 polyfill 解决
基础使用方式
const p = new Promise((resolved, reject) => {
setTimeout(function () {
// resolved('成功'); //promise变为resolve成功状态
// or
reject("失败"); //promise变为rejected失败状态
}, 1000);
});
p.then(
(value) => {},
(reason) => {
console.log("reason", reason);
}
);
Promise接口
prmose.prototype.then
返回一个新的Promise,它最多需要两个参数:Promise 的成功和失败情况的回调函数
基础使用方式
var p1 = new Promise((resolve, reject) => {
resolve("成功!");
// or
// reject(new Error("出错了!"));
});
p1.then(
(value) => {
console.log(value); // 成功!
},
(reason) => {
console.error(reason); // 出错了!
}
);
返回新的Promise
每次执行then之后都将返回新的Promise对象
回调没有定义,那么这个新的Promise状态将为原Promise的状态
var promise = new Promise(function(resolve, reject){
reject('error')
})
var promise1 = promise
promise1.then(
function(){
console.log('success')
},
function(){
console.log('error')
}
);//输出error
回调中返回了新的状态,那么以新的状态为准
var promise = new Promise(function (resolve, reject) {
reject("error");
});
var promise2 = promise.then(null, function () {
return Promise.resolve("success");
});
promise2.then(
function () {
console.log("success");
},
function () {
console.log("error");
}
);
没有定义回调,那么以原来Promise状态为
var promise3 = Promise.reject("error");
promise3
.then(() => {// 不会执行}, null)
.then(
() => {// 不会执行},
(rs) => {
console.log(rs); // error
}
);
回调中如果返回了Error,那么新的Promise状态为rejected
var promise = Promise.resolve("succes");
var promise4 = promise.then(function () {
throw new Error("error");
});
promise4.then(null, function (reject) {
console.log(reject);
});
//输出 Error: error
Promise可重复执行 then 或者 catch
var promise = Promise.resolve("succes");
promise
.then(function () {
console.log("one");
})
.then(function () {
console.log("two");
})
.then(function () {
console.log("three");
}); // one two three
Promise.prototype.catch
添加一个拒接(rejection)回调到当前promise,返回一个新的promise。当这个回调函数被调用,新的promise将以它的返回值来 resolve,否则如果当前promise 进入 fulfilled 状态,则以当前promise的完成结果作为新promise的完成结果
使用链式语句的catch方法
var p1 = new Promise(function (resolve, reject) {
resolve("succes");
});
p1.then(function (value) {
console.log(value); //'success'
return Promise.reject("oh,no!");
})
.catch(function (e) {
console.log(e); //'oh,no!'
})
.then(function () {
console.log("success callback"); //'success callback'
}, null);
捕获抛出的错误
// 拋出一个错误,大多数时候都将调用catch方法
var p1 = new Promise(function (resolve, reject) {
throw "Un-oh!";
});
p1.catch(function (e) {
console.log(e); //'Un-oh'
});
// 在异步函数中抛出的错误不会被catch捕获到
var p2 = new Promise(function (resolve, reject) {
setTimeout(function () {
throw "Error";
}, 1000);
});
p2.catch(function (e) {
console.log(e); //不会执行
});
// 在resolve()后面抛出的错误会被忽略
var p3 = new Promise(function (resolve, reject) {
resolve();
throw "Error";
});
p3.then(function (e) {
console.log(e); //不执行
});
如果已解决错误
// 创建一个新的 Promise,且已解决
var p1 = Promise.resolve("success");
var p2 = p1.catch(function (res) {
// 这个方法医院不会调用
console.log(res);
});
p2.then(function (value) {
console.log(value);//'succes'
}, null);
Promise.prototype.allSettled
返回一个promise,该promise在所有给定的promise已被解析或被拒绝后解析,并且每个对象都描述每个promise的结果
注意:兼容性并不是很好,polyfill 也并未支持
var resolved = Promise.resolve(42);
var rejected = Promise.reject(-1);
var setTime = new Promise(function (resolve) {
setTimeout(function () {
resolve(111);
}, 1000);
});
const pArr = Promise.allSettled([resolved, rejected]);
pArr.then(function (reason) {
console.log(reason); // [{status: 'fulfilled', value: 42},{status: 'rejected', reason: -1}]
});
Promise.prototype.finally
返回一个Promise。在promise结束时,无论结果是 fulfilled 或者是 rejected,都会执行指定的回调函数。这为在Promise是否成功完成后都需要执行的代码提供了一种方式
var p1 = new Promise(function(resolve, reject){
resolve('success')
})
var p2 = new Promise(function(resolve, reject){
reject('error')
})
p1.finally(function(){
console.log('one')
})
p2.finally(function(){
console.log('two')
}) // one two
注意:由于无法知道promise的最终状态,所以finally的回调函数中不接收任何参数,它仅用于无论最终结果如何都要执行的情况
Promise.all
这个方法返回一个新的promise对象,该promise对象在参数对象中所有的promise对象都成功的时候才会触发成功,一旦有任何一个参数里面的promise对象失败则立即触发该promise对象的失败。
这个新的promise对象在触发成功状态以后,会把一个包含参数所有的 promise 返回值的数组作为成功回调的返回值,顺序跟参数的顺序保持一致。
如果这个新的 promise 对象触发了失败状态,它会把参数里第一个触发失败的 promise 对象的错误信息作为它的失败信息。
Promise.all 方法常被用于处理多个 promise 对象的状态合集
var p1 = Promise.resolve(3)
var p2 = 42
var p3 = new Promise(function(resolve,reject){
setTimeout(resolve, 100,'foo')
})
Promise.all([p1,p2,p3]).then(function(reason){
console.log(reason)
})// [3, 42,'foo']
Promise.reject
返回一个带有拒绝原因 reason 参数的 promise对象
Promise.reject('error').then(null,function(error){
console.log(error) // 'error'
})
Promise.reject(new Error('fail')).then(null,function(error){
console.log(error) // Error:fail
})
Promise.resolve
返回一个以给定值解析后的Promise对象。如果该值为 promise,返回这个 promise;
如果这个值是带有 "then"方法,返回的 promise 会采用它的最终状态;否则返回的 promise 将以此值完成
Promise.resolve('success').then(function(value){
console.log(value)//'success'
},null)
resolve 另一个 promise
var original = promise.resolve(33)
var cast = Promise.resolve(original)
cast.then(function(value){
console.log(value)
})
console.log(original === cast)//true 33
resolve 包含 then的对象参数
// resolve
var p1 = Promise.resolve({
then:function(onFulfill, onReject){
onFulfill('fulfilled!')
}
})
p1.then(
function(v){
console.log(v)//'fulfilled!'
},null
)
// reject
var p2 = Promise.resolve({
then:function(onFulfill, onReject){
onReject('rejected!')
}
})
p2.then(null,function(e){
console.log(e)//'rejected!'
})
Promise.race
当参数里的任意一个 子promise 被成功或失败后,父promise 马上也会用 子promise 的成功返回值或失败详情作为参数调用 父promise 绑定的相应句柄,并返回该 promise对象
var p1 = new Promise(function(resolve,reject){
setTimeout(resolve,500,'one')
})
var p2 = new Promise(function(resolve,reject){
setTimeout(resolve,100,'two')
})
Promise.race([p1,p2]).then(function(value){
console.log(value)
}) // two
Promise事件
使用Promise编写异步代码时,使用 reject 来处理错误。有时,开发者通常会忽略这一点,导致一些错误没有得到处理。例如
new Promise((resolve, reject) => {
reject("error");
})
.then(function () {})
.then(function () {});
由于没有使用catch方法捕获错误,当前rejection 时,会派发此事件
unhandlerdrejection
当 Promise 被拒绝,但没有提供 reject 函数来处理 rejection 时,会派发此事件
window.addEventListener("unhandledrejection", (e) => {
console.log(e.reason); // 打印'error'
});
new Promise((resolve, reject) => {
reject("error");
});
rejectionhandled
当一个 Promise错误最初未被处理,但是稍后又得到了处理,则会派发此事件
window.addEventListener("unhandledrejection", (e) => { // 有promise reject未被处理
console.log(e.reason); // 打印'error'
});
window.addEventListener("rejectionhandled", (e) => { // 有promise reject 已被被处理
console.log(e.reason, "1111"); // 1秒后打印'error'
});
let foo = new Promise((resolve, reject) => {
reject("error");
});
setTimeout(() => {
foo.catch((e) => {});
}, 1000);
我们可以通过以上事件为 Promise 失败是提供补偿处理,也有利于调试 Promise 相关的问题。在每一个上下文中,该处理都是全局的。因此不管源码如何,所有的错误都会在同一个 handler 中被铺抓处理。
window.addEventListener(
"unhandledrejection",
(e) => {
// 可以在这检查 e.promise 和 e.reason
console.log(e.reason,e.promise)
e.promise.catch((e)=>{})
// 阻止默认处理(例如将错误输出到控制台)
e.preventDefault();
},
false
);
高级用法
链式调用
连续执行两个或者多个异步操作是一个常见的需求 ,在上一个操作执行成功之后,开始下一个操作,并带着上一步操作所返回的结果。我们可以通过创造一个 Promise 链来实现这种需求。
var promise = new Promise(function (resolve, reject) {
resolve("promise");
});
promise
.then(function (res) {
console.log(res);
return "success";
})
.then(function (res) {
console.log(res);
return new Promise(function (resolve, reject) {
setTimeout(function () {
reject("error");
}, 3000);
});
})
.then(null, function (reason) {
console.log(reason);
return "finish";
})
.then(function (res) {
console.log(res);
});
// 输出 'promise' 'success' 'error' 'finish'
注意:返回值默认为undefined,否则,callback 将无法获取上一个 Promise 的结果
Catch 的后续链式操作
有可能会在一个回调失败之后继续使用链式操作,即 使用一个catch,这对于链式操作中抛出一个失败之后,再次进行新的操作很有用。
new Promise((resolve, reject) => {
console.log("初始化");
resolve();
})
.then(() => {
throw new Error("有哪里不对了");
console.log("执行[这个]");
})
.catch(() => {
console.log("执行[那个]");
})
.then(() => {
console.log("执行最后一个");
}); //输出结果: 初始化 执行[那个] 执行最后一个
注意:因为抛出了错误,'执行[这个]'没有被输出
执行顺序
为了避免意外,即使是一个已经变成 resolve 状态的 Promise,传递给 then() 的函数也总是会被异步调用
Promise.resolve().then(function () {
console.log(2);
});
console.log(1); // 1 2
传递到 then() 中的函数被置入了一个微任务队列,而不是立即执行,这意味着他是在 JavaScript 事件对了的所有运行时结束了,事件队列被清空之后,才开始执行
var loading = new Promise(function (resolve, reject) {
setTimeout(resolve);
});
loading.then(function () {
console.log(4);
});
Promise.resolve()
.then(function () {
console.log(2);
})
.then(function () {
console.log(3);
});
console.log(1);
嵌套
then 回调函数中如果返回了新的异步对象,那么后续链式调用的 then 都会等待新的异步对象完成才会继续向下执行
var p1 = new Promise(function (resolve, reject) {
console.log("p1");
resolve();
});
p1.then(function () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("p2");
resolve();
}, 1000);
});
})
.then(function () {
console.log("p3");
})
.then(function () {
console.log("p4");
})
.then(function () {
console.log("p5");
}); // p1 p2 p3 p4 p5
简便的 Promise 链式编程最好保持扁平化,不要嵌套 Promise,因为嵌套会导致可读性降低,代码也不容易排除,优化
Promise的缺点
- 代码有风险造成未解决的promise
- 无法取消Promise,一旦新建他就会立即执行,无法中途取消
- 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部
- 当处于 pending 状态时,无法得知目前进展到哪一个阶段 (刚刚开始还是即将完成)
async await
async / await 是ES7 引入的异步代码规范。它提供了一种新的编写异步的方式,这种方式在语法层面提供了一种非常接近于同步代码的异步非阻塞代码风格,在此之前我们使用的多是异步回调,Promise模式
使用方法
async关键字
async function 用来定义一个返回 AsyncFunction 对象的异步函数。异步函数是指通过事件循环异步执行的函数,他会通过一个隐式的promise 返回其结果
async function name([param, [param, [...param]]]) {
statements;
}
name 函数每次 param 要传递给函数的参数 statements 函数体语法。 返回值 返回的 Promise对象会运行(resolve)异步函数的返回结果,或者运行拒绝(reject) ----- 如果异步函数抛出异常的话
await 关键字
await 操作符用于等待一个 Promise 对象。它只能在异步函数 async function 中使用。
await 会暂停当前 async function 的执行,等待 Promise 处理完成。
若 Promise 正常处理(fulfilled),其回调的 resolve 函数参数作为 await 表达式的值,继续执行 async function
若 Promise 处理异常(rejected),await 表达式会把 Promise 的异常原因抛出。
如果 await 操作符后的表达式的值不是一个 Promise,则返回该值本身。
[return_value] = await expression
表达式 一个Promise 对象或者任何要等待的值 返回值 返回 Promise对象的处理结果,如果等待的不是 Promise对象,则返回它本身
async function f1(){
var x = await 10
console.log(x) // 10
}
f1()
并联的await
async / await 语法的确好用简单,但在现实场景中也容易出现一些问题
async function retreProfile(email) {
const user = await getUser(email);
const roles = await getRoles(user);
const level = await getLevel(user);
return [user, roles, level];
}
上面代码实现了获取用户基本信息,然后通过基本信息获取用户角色、级别信息的功能,其中 getRoles 与 getLevel 两者之间并无依赖,是两个并联的异步操作,但代码中 getLevel 却需要等待 getRoles resolve之后才能执行。
并不是所有人都会犯这种错误,而是同步风格很容易诱惑我们忽略掉真正的异步调佣次序,而陷入过于简化的同步思维中。
注意:async 只是形式上的同步,根本上还是异步的,请注意不要让使用者把事件浪费在无谓的等待上。优化
async function retriveProfile(email) {
const user = await getUser(email);
const p1 = getRoles(user);
const p2 = getLevel(user);
const [roles, levels] = await Promise.all([p1, p2]);
return [user, roles, levels];
}
注意,代码中的 getRoles、getLevel 函数都没有跟在 await关键字之后,而是把函数返回的 Promise 存放在变量 p1,p2 中,后续才对p1、p2 执行 await 声明,getRoles、getLevel 就能同事执行,不需要等待另一方的完成
错误处理
使用 try…catch
async function asyncCall() {
try {
// await asyncFunc();
throw new Error("error");
} catch (e) {
console.log(e.message); //'error'
}
}
asyncCall();
包装promise,使其返回统一的格式代码
// 包裝promise,使其返回统一的错误格式
// @param {Promise} promise
function to(promise) {
// 第一个标识异常数据
return promise.then((res) => [null, res]).catch((err) => [err]);
}
const [err, res] = await to(fetchUser(true));
if (err) {
console.log("touser err", err);
}
继续使用catch
// 因为 async 返回的 promise对象,所以可以使用 catch
const user = await fetchUser(true).catch((err)=>{
console.log(err)
})
优点
- async / await 从上到下,顺序执行,就像写同步代码一样。更符合人编写代码的习惯
缺点
- 编译后增加了代码的体积
- 编译后的代码不容易理解,会给调试带来一直的困扰
jQuery异步对象
jquery 中通过 $.Deferred 来实例化异步对象,deferred对象就是 jQuery 的回调函数解决方案。
在英文中,defer的意思是’延迟’,所以 deferred 对象的含义就是 “延迟” 到未来
简单示例
在jQuery中
var deferred = $.Deferred()
var promise = deferred.promise()
在ES6中
var promise = new Prmose(function (resolve, reject) {});
处理状态
一个 deferred 对象,能做的和一个 promise 对象差不多,它也有 resolve 和 reject 两个函数来触发 done() 和 fail() 函数
在jQuery中
var deferred = $.Deferred();
setTimeout(function () {
deferred.resolve("success");
}, 2000);
var promise = deferred.promise();
在ES6中
var promise = new Promise(function (resolve, reject) {
resolve("sucess");
});
jquery的延时对象可以再外面解决,promise的异步对象在实例化的回调中解决,相比promise更加安全
等待多个异步完成
在jQuery中
var deferred1 = $.Deferred();
deferred1.resolve("promise1");
var p1 = deferred1.promise();
var deferred2 = $.Deferred();
deferred2.resolve("promise2");
var p2 = deferred2.promise();
$.when(p1, p2).then(function (res1, res2) {
console.log(res1, res2);
}); // promise1 promise2
在 ES6中
var p1 = Promise.resolve("promise1");
var p2 = Promise.resolve("promise2");
Promise.all([p1, p2]).then(function (value) {
console.log(value);
}); // ['promise1', 'promise2']
成功或失败都触发
在jQuery中
var deferred1 = $.Deferred();
deferred1.reject("promise1");
var p1 = deferred1.promise();
p1.always(function (res) {
console.log(res);
}); // 'promise1'
var deferred2 = $.Deferred();
deferred2.resolve("promise2");
var p2 = deferred2.promise();
p2.always(function (res) {
console.log(res);
}); // 'promise2'
在 ES6中
var p1 = Promise.reject('promise1')
var p2 = Promise.resolve('promis2')
p1.finally(function(res){
console.log(res)//undefined
})
p2.finally(function(res){
console.log(res)//undefined
})
ES6的Promise的 finally 回调中并不会有解决的结果