0 关于
Promise是ES6的核心概念之一,多种前端库中的API中都会见到Promise身影,因此掌握Promise概念异常重要。然而:
(1) 网上的关于Promise资料杂乱繁多,而且多数是一堆API的罗列,甚至不知所云,导致学习起来耗时耗力。
(2) 官网规范逻辑严谨,但是由于原版为英文,多数人限时时间和精力,无暇研究
本文尝试重新梳理Promise概念,结合示例用例,力求以更明、简洁、直观的方式去说明Promise概念、原理、简单应用等。
基于以上原因,本文有如下特点:
(1) API介绍力求通俗,可能导致部分措辞不够严谨,如有错误请及时指正
(2) 配合实际业务场景进行说明,力求形象
(3) 示例较多,导致文章内容略显过多,但肯定不难,请细致阅读
因此,请耐心读完本文:)
郑重声明:文章所有示例、语言均为原创,如果转载,请著名出处!所有的参考资料,在文档末的"参考资料"单独说明。
1 引例
本文以一个简单的业务场景开始:小明是单位的普通员工,有事需要请假3天,需要向上级请假。
这里的上级包括项目经理、人事经理、老板。流程如下:
(1) 向主管的项目经理提交请假申请。项目经理进行批准或拒绝
(2) 项目经理申请通过后,向人事经理提交申请,请求审批通过
(3) 人事经理审批通过后,向公司老板提交申请,请求审批通过
其中任一环节失败后,都会导致请假失败。
JavaScript中通过代码描述请假过程,包括如下几个函数。(为便于理解,函数相当简单且雷同。请仔细看代码和注释)
向项目经理提交请假申请
// 向项目经理提交请求。下面的2个方法类似。
function applyToProjectManager(callback) {
// 通过setTimeout异步操作模拟ajax请求。下同。
// 异步过程完全可以用异步Ajax替换
setTimeout(function() {
// 返回随机的0或1,模拟审批通过或拒绝。
// 可直接设置1:let sucecess = 1;
let success = parseInt(Math.random() * 2);
// 根据结果,执行操作
if (!success) {
throw new Error("projectManager审批失败!");
}
console.log("projectManager审批通过!");
callback()
}, 1000);
// 项目经理过了1天进行了审批,此处用1秒(1000毫秒)表示。
// 如果程序中真的用1天,这个例子就类似无限等待啦:)
}
向人事经理提交请假申请
function applyToHrManager(callback) {
setTimeout(function() {
let success = parseInt(Math.random() * 2);
if (!success) {
throw new Error("HrtManager审批失败!");
}
console.log("HrManager审批通过!");
callback()
}, 2000); // 人事经理用了2天,进行行政审批
}
向老板提交请假申请
function applyToBoss(callback) {
setTimeout(function() {
let success = parseInt(Math.random() * 2);
if (!success) {
throw new Error("boss审批失败!");
}
console.log("boss审批通过!");
callback()
}, 3000); // 老板太忙,等了3天才出结果,悲剧啊:(
}
传统的调用过程形如:
applyToProjectManager(function() {
applyToHrManager(function() {
applyToBoss(function() {
// 层层审批成功了,想去干点啥吧...
console.log("请假成功了,太好了,搓一顿去!");
});
})
})
console.log("小明说:等着审批就行了,等几天吧。我先去搬砖了...");
传统调用存在多种缺点:
- 回调多,导致回调地狱。场景中,如果小明请假成功后,去"搓一顿"时饭店关门,会多另一层回调;如果饭店开门,吃完后付款失败。。。导致无限级别的回调。My God!
- 不易阅读,调试不方便。
2 Promise
Promise解决以上的嵌套调用问题。
2.1 构造函数
Promise对象是一个构造函数,用来生成Promise实例。如下所示。
// 构造函数接收单个参数,类型为Function,规范中称为executor
// function需要传递2个参数:resolve、reject,均为函数类型
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
看到构造函数看着有点复杂,其实不难。说明如下:
-
构造函数接收单个参数,类型为Function,在规范中被称为***executor***,包含了初始化Promise的代码块
-
单个参数的Function又包含2个参数:resolve、reject,同样为Function类型
-
一般来说,在构造函数内执行异步操作(如Ajax),执行完毕后,如果成功则调用resolve函数;如果失败,调用reject函数。
-
resolve和reject函数在executor中调用时,只能传递1个业务参数:代表相应的业务数据,一般为对象类型。
这几点说明有些抽象,如果结合本文中的业务场景会更好理解。第一步,根据上面的构造函数定义,小明向项目经理请假过程可以描述为
const promiseOfProjectMananger = new Promise(function(resolve, reject) {
console.log("executor开始执行...");
// 小明向项目经理请假,审批过程为异步。
setTimeout(function() {
let success = parseInt(Math.random() * 2);
console.log("过了一段时间,审批结果出来啦...")
if (!success) {
// 审批未通过
reject("projectManager审批失败!");
}
// 审批通过
resolve("projectManager审批通过!")
}, 1000);
})
console.log("等待项目经理审批吧...");
注意:new Promise构造函数一开始被调用时,executor立即执行。上面代码在Chrome控制台打印先后顺序如下:

这也意味着:异步操作代码立即执行(尽管执行结果会异步)。
浏览器控制台输入变量promiseOfProjectMananger并回车后,成功或失败情况截图如下。

2.2 生命周期
上述代码被调用后,返回了一个Promise对象实例(通过promiseOfProjectMananger变量描述),此时处于pending状态。
Promise对象在生命周期内,有如下3个状态:
- pending: 初始状态,既不是fulfilled状态,也不是rejected状态。
- fulfilled: 意味着异步操作已经成功完成。(规范上是fulfilled,一些资料和浏览器使用resolved描述,属于一个含义)
- rejected: 意味着异步操作失败
Promise对象不受外界影响,初始状态为pending,可能装换为fulfilled和reject状态,即:
pending -> fulfilled 或 pending -> reject。
上面创建的对象promiseOfProjectMananger,虽然操作结果还不确定,但有预期:不是成功,就是失败。
如何处理成功或失败呢?答案是:使用then或catch方法。
2.3 对象方法
任何一个Promise类型的对象实例,都有then、catch等方法(此外,还有其它的方法,此处略)。
(1) then方法
then方法对异步操作的结果(成功或失败)进行处理。调用示例如下。
// then方法的调用
promise.then(function(value) {
// 成功后调用,也叫fulfillment handler
}, function(error) {
// 失败时调用,也叫failure handler
});
其中,2个handler的中的单个参数(value参数),数据是在构造函数的异步操作完毕后传入过来的。如下。
// ...2.1小节中的构造函数部分代码
if (!success) {
// 这里,传递了失败后的业务数据。相当于then中的第2个函数。
reject("projectManager审批失败!");
}
// 传递了成功后的业务数据。相当于then中的第1个函数。
resolve("projectManager审批通过!")
注意:then方法中的2个Function参数是可选的,见如下的then方法调用方式:
let promise = doSomthingAsync();
// 监听了fulfillment handler和failure handler
promise.then(function(contents) {
// fulfillment
console.log(contents);
}, function(err) {
// rejection
console.error(err.message);
});
// 仅监听了fulfillment handler
promise.then(function(contents) {
// fulfillment
console.log(contents);
});
// 仅监听了failure handler
promise.then(null, function(err) {
// rejection
console.error(err.message);
});
结合本文中的业务场景,对初始化的promiseOfProjectMananger对象,进行then操作
promiseOfProjectMananger.then(function(result) {
// 如果成功,控制台打印:projectManager审批成功!
console.log(result);
}, function(error) {
// 如果失败,控制台打印:projectManager审批失败!
console.log(error);
})
浏览器运行截图如下:

(2) catch方法
catch方法仅监听了rejected状态的Promise对象,进行相应的处理,等效于then方法仅有rejection handler参数的情况。如下。
// catch调用
promise.catch(function(err) {
// rejection
console.error(err.message);
});
// 等同于:
promise.then(null, function(err) {
// rejection
console.error(err.message);
});
2.4 Promise链
Promise的优势在于:将多个Promise进行链式调用,完成复杂的异步操作。
事实上,每次调用then和catch方法时,创建并返回了另外一个Promise对象。查看如下代码
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
p1.then(function(value) {
console.log(value);
}).then(function() {
console.log("Finished");
})
p1.then()的调用后,返回了第二个Promise对象,该对象的then方法继续被调用。
只有在第一个Promise被执行求解后(产生了fullfilled和rejected状态后),第二个then()方法的fulfillment handler才被调用。
上述代码进行分解后,如下。
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = p1.then(function(value) {
console.log(value);
})
p2.then(function() {
console.log("Finished");
});
常见的链式调用方式为
new Promise(function(resolve, reject) {
// 异步操作,调用resolve和reject
})
.then(function(data) {
// 异步方式操作后返回Promise对象实例
})
.then(function(data) {
// 异步方式操作后返回Promise对象实例
})
.then(function(data) {
// 异步方式操作后返回Promise对象实例
});
3 Promise使用示例
有了链式调用为基础,引例中的回调地狱问题可以Promise解决了。直接上代码。
// 为了后面的链式调用更直观,这里提取公用操作为单独函数
function apply(roleName, time) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
// 为了输出方便,直接给了"审批通过"
let success = 1;
if (!success) {
reject(roleName + "审批失败!");
}
resolve(roleName + "审批通过!")
}, time);
})
}
小明开始申请逐级审批,业务代码如下。
// 链式调用
// 同步书写方式,实现异步编程
// 避免了嵌套回调!
apply("projectManager", 1000)
.then(function(result) {
console.log(result);
return apply("HrManager", 2000);
})
.then(function(result) {
console.log(result);
return apply("Boss", 3000);
})
.then(function(result) {
console.log(result)
console.log("请假成功,搓一顿去!")
});
console.log("向项目经理已经提交了申请,搬砖去!等待结果...");
浏览器控制台,输出结果如下。

4 结论
本文篇幅稍长了一点,说明了Promise的一般概念,以及典型应用场景。解决了传统回调地狱问题。
内容仅为说明Promise概念、原理等:一些严谨的术语如job queue 、job scheduling 、callback hell 未提及或详细说明。
实际上,后续的规范ES7、ES8等,已经提及了利用await实现异步编程的规范。相比而言,比Promise更直观了一下。这种语法形如
async function getAysncResult() {
// 通过关键字async、await进行异步编程,更像同步编程的方式
let contents = await doSomethingAsync();
doSomethingWith(contents);
console.log("Done");
};
后续系列会进行探讨。
5 参考文献
- Nicholas C. Zakas的书籍《Understanding ECMAScript》
- 阮一峰,Promise 对象:es6.ruanyifeng.com/#docs/promi…