在 JavaScript Promise 快速入门 这一篇对Promise 及其基本用法做了简单的介绍。本篇介绍Promise 的完整功能以及对Promise 的来源以及实现进行解密。
Promise 是什么?
该怎么解释Promise 呢? 试着从以下几种角度来理解:
- 从代码的角度: Promise 是ECMAScript 6中提供了一个类 ,Promise 对象可以让异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数的方式。
- 从Promise工作机制的角度:Promise 表示一个异步操作的最终结果,与之进行交互的方式主要是
then
方法,该方法注册了两个回调函数,用于接收 promise 的终值或本 promise 不能执行的原因。
如果到这里还不理解Promise 是什么?那就看看Promise 是怎么来的。
Promise 是怎么来的?
JavaScript是单线程语言,也就是没有像Java语言一样有Thread这种线程类(可以单独的启动一个线程运行任务)。 换言之, JavaScript是一种同步的编程语言, 但是往往JavaScript也有很多异步编程的需要,最典型的就是AJAX,如何实现AJAX呼叫后端服务之后的回调呢? 这里为了让演示更简单和直观,使用setTimeout延迟调用代替AJAX。
传统的回调方式
传统的回调将回调函数作为参数传递给异步函数,在异步函数执行完成之后,调用回调函数。以以下代码为例:
var successCallBack = function(result){
console.log("成功执行的结果是="+result)
}
var failCallBack = function(result){
console.log("失败执行的结果是="+result)
}
function asyncFunc(successCallBackFunc,failCallBackFunc){
var isSuccess = true;
//使用setTimeout模拟异步行为
setTimeout(function(){
//根据返回结果设置isSuccess的值
if(isSuccess){
successCallBackFunc("返回值1");
}else{
failCallBackFunc("返回值2");
}
},1000);
}
上面的这种写法对于单层的回调来说看起来也能接受,但是如果是多层回调的话,代码就很复制了,也就是经典的回调地狱。
Promise 的出现
早期的JavaScript一般也是使用上面的方式来处理回调的,是否有避免回调地狱的写法呢?
于是有一些JavaScript组织探求比较好的写法, 这其中最知名的就是CommonJS 。
CommonJS 是一个由服务的JavaScript运行库作者组成的小组,也就是CommonJS 是JavaScript的一个社区, 或者是一个民间组织,不是官方的标准。
CommonJS 提出了Promise 的方式,JavaScript也很认同这种方式,于是在JavaScript发布的新版本ECMAScript 6中,就将Promise 作为JavaScript的一个原生类了。
ECMAScript 6 是2015年发布的JavaScript 语言的标准。Promise 是ECMAScript 6 中的一个类。
Promise的基本用法
对于上面的立子, 使用Promise,aysnFunc返回一个Promise 对象,则简写为:
let promise = aysnFunc();
promise.then(successCallBackFunc,failCallBackFunc);
再简化一点:
aysnFunc().then(successCallBackFunc,failCallBackFunc);
为什么叫Promise ?
Promise ,翻译是承诺, 也就是保证某人会做某事或是某件事情会发生。结合then() 方法等,形象的理解一下,虽然我是异步执行的,但是我承诺在干完该干的事情之后,我会把承诺的事情做完。
浏览器对Promise 的支持
因为Promise 是在ECMAScript 6才开始出现的,所以对于一些早期的浏览器版本是不支持的,对于
Safari 10,Windows 的 Edge 14以上的浏览器支持。完整的支持参考下表:
ECMAScript 6的Promise
- Promise 是ECMAScript 6的一个类
- Promise 对象表示异步操作的最终完成(或失败)及其结果值。
- Promise 对象具有静态方法和原型方法,静态方法可以独立应用,原型方法需要应用于Promise对象的示例。
当创建一个Promise 时,它处于pending状态,Promise 根据是否fulfilled(已成功)或rejected(已失败),将运行以下三种方法中的一种或多种: Promise.prototype.catch(onRejected)
Promise.prototype.then(onFulfilled, onRejected)
Promise.prototype.finally(onFinally)
下图显示了 then 和 .catch 方法的流程。由于它们返回一个 Promise ,它们可以再次被链式调用。不管 promise 最后的状态,在执行完then 或 catch 指定的回调函数以后,都会执行finally方法指定的回调函数。
Promise 的状态
Promise 有3种状态:
- Pending, 进行中
- Resolved(Fullfilled),已完成
- Rejected,已失败
在早期的Promise,完成的状态是Fullfilled,后来习惯就称为Resolved。
Promise 状态的改变:
- Pending -> Resolved
- Pending -> Rejected
链式调用
通过多次调用then() 可以添加多个回调函数,它们按照插入顺序执行。
Promise链可以实现在上一个操作执行成功之后,开始下一个的操作,并带着上一步操作所返回的结果。
在没有Promise之前,多重异步操作,会导致经典的回调地狱:
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
使用Promise , 形成一个Promise 链
doSomething().then(function(result) {
return doSomethingElse(result);
})
.then(function(newResult) {
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);
catch(failureCallback) 是 then(null, failureCallback) 的缩略形式。
注意:一定要有返回值,否则,callback 将无法获取上一个 Promise 的结果。
多说一点, 上面的写法也可以使用箭头函数进一步简化:
doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);
() => x 比 () => { return x; } 更简洁一些,但后一种保留 return 的写法才支持使用多个语句。
Promise.resolve() 和 Promise.reject()
两者都是new promise() 的快捷方式。
Promise.resolve(value)
等同于
new Promise(function (resolve) {
resolve(value)
})
Promise.reject(value)
等同于
new Promise(function(resolve,reject){
reject(new Error("xx"));
});
Promise.all() 和Promise.race()
Promise.all()
接收一个Promise对象的数组作为参数, 当数组里的所有Promise对象全部变为resolve时, 该方法才resolve;如果其中一个Promise对象reject, 则该方法reject。
Promise.all([
new Promise(function(resolve){
setTimeout(function(){
resolve(100);
},100);
}),
new Promise(function(resolve){
setTimeout(function(){
resolve(200);
},200);
}),
new Promise(function(resolve){
setTimeout(function(){
resolve(300);
},300);
})
]).then(function(value){
console.log(value); //[100,200,300]
});
- 接收一个Promise对象的数组作为参数, 只要一个Promise对象变为Resolved或者Rejected状态,该方法返回,进行后面的处理。
Promise.race([
new Promise(function(resolve){
setTimeout(function(){
resolve(100);
},100);
}),
new Promise(function(resolve){
setTimeout(function(){
resolve(200);
},200);
}),
new Promise(function(resolve){
setTimeout(function(){
resolve(300);
},300);
})
]).then(function(value){
console.log(value); //100
});
使用Promise 封装AJAX
上面一直说Promise 可以让异步的回调使用同步的方式书写,典型的就是在AJAX中使用, 那么AJAX是如何结合Promise 使用的呢? 看如下代码:
function ajaxGet(url) {
// 返回一个Promise对象
return new Promise(function (resolve, reject) {
// 创建一个XHR对象
var req = new XMLHttpRequest() || new ActiveXObject('Microsoft.XMLHTTP')
req.open('GET', url, true)
// 注意:使用req.onload监听req的状态
req.onload = function () {
if (req.readyState == 4 && req.status == 200) {
resolve(req.response)
} else {
reject(Error(req.statusText))
}
}
// 网络错误
req.onerror = function () {
reject(Error("Network Error"));
};
// 发送请求
req.send();
});
}
调用AJX的方式可以简化为:
ajaxGet("http://xxx").then(function (response) {
console.log(response);
}).catch(function (error) {
console.log(error);
})
注意:
- 在AJAX中使用的事件是onreadystatechange, 这里使用的是onload。为什么不使用
onreadystatechange呢?因为Promise 的状态改变时单向的,一次性的, 一旦改变,状态就会凝固,也即是req.onreadystatechange只会被调用一次。如果用onreadystatechange
Promise 的典型使用场景
Promise 应用在很多功能场景中, 包括:
- 文件操作
- API调用
- DB调用
- IO调用
- Node.js 基于Promise
Promise 实现原理解密
说了这么多, 感觉Promise 很强大,也很神秘, 其实Promise 也不是那么神秘,简单的模拟一下Promise的实现:
function Promise(fn) {
var state = 'pending',
value = null,
deferreds = [];
this.then = function (onFulfilled) {
if (state === 'pending') {
deferreds.push(onFulfilled);
return this;
}
onFulfilled(value);
return this;
};
function resolve(newValue) {
value = newValue;
state = 'fulfilled';
setTimeout(function () {
deferreds.forEach(function (deferred) {
deferred(value);
});
}, 0);
}
fn(resolve);
}
- Promise 有三个变量: 状态 state ,值value和延迟队列deferreds 。
- 初始的状态是pending ,value是空,延迟队列也是空
- 传入Promise 的函数fn立即执行,fn执行完成调用resolve
- resolve方法采用异步方式执行延迟队列里的方法
- promise对象上添加then方法,当前promise对象状态为pending时,将通过then方法注册的新方法,添加到延迟队列;当前promise对象状态为完成时,执行注册的方法