在阅读本文以前,你应该已经了解JavaScript异步实现的几种方式:回调函数,发布订阅模式,Promise,生成器(Generator),其实还有async/await方式,这个后续有机会会介绍。本篇将介绍Promise,读完你应该了解什么是Promise,为何使用Promise,而不是回调函数,Promise怎么使用,使用Promise须要注意什么,以及Promise的简单实现。javascript
若是你已经对JavaScript异步有必定了解,或者已经阅读过本系列的其余两篇文章,那请继续阅读下一小节,若你还有疑惑或者想了解JavaScript异步机制与编程,能够阅读一遍这两篇文章:前端
回调函数,做为JavaScript异步编程的基本单元,很是常见,你确定对下面这类代码一点都不陌生:java
component.do('purchase', funcA);
function funcA(args, callback) {
//...
setTimeout(function() {
$.ajax(url, function(res) {
if (res) {
callback(res)
} else {//...}
});
}, 300);
funcB();
setTimeout(function() {
$.ajax(arg, function(res) {
if (res) {
callback(res);
}
});
}, 400);
}复制代码
上面这些代码,一层一层,嵌套在一块儿,这种代码一般称为回调地狱,不管是可读性,仍是代码顺序,或者回调是否可信任,亦或是异常处理角度看,都是不尽人意的,下面作简单阐述。面试
上文例子中代码funcB
函数,还有两个定时器回调函数,回调内各自又有一个ajax异步请求而后在请求回调里面执行最外层传入的回调函数,对于这类代码,你是否能明确指出个回调的执行顺序呢?若是funcB
函数内还有异步任务呢?,状况又如何?ajax
假如某一天,好比几个月后,线上出了问题,咱们须要跟踪异步流,找出问题所在,而跟踪这类异步流,不只须要理清个异步任务执行顺序,还须要在众多回调函数中不断地跳跃,调试(或许你还能记得诸如funcB
这些函数的做用和实现),不管是出于效率,可读性,仍是出于人性化,都不但愿开开发者们再经历这种痛苦。编程
如上,咱们调用了一个第三方支付组件的支付API,进行购买支付,正常状况发现一切运行良好,可是假如某一天,第三方组件出问题了,可能屡次调用传入的回调,也可能传回错误的数据。说到底,这样的回调嵌套,控制权在第三方,对于回调函数的调用方式、时间、次数、顺序,回调函数参数,还有下一节将要介绍的异常和错误都是不可控的,由于不管如何,并不总能保证第三方是可信任的。数组
关于JavaScript错误异常,初中级开发接触的可能并很少,可是其实仍是有不少能够学习实践的地方,如前端异常监控系统的设计,开发和部署,并非三言两语能阐述的,以后会继续推出相关文章。promise
咱们知道当JavaScript抛出错误或异常时,对于未捕获异常,浏览器会默认在控制台输出错误堆栈信息,以下,当test
未定义时:浏览器
function init(name) {
test(name)
}
init('jh');复制代码
输出如图:异步
如图中自顶向下输出红色异常堆栈信息,Uncaught
表示该异常未捕获,ReferenceError
代表该异常类型为引用异常,冒号后是异常的详细信息:test is not defined
,test
未定义;后面以at
起始的行就是该异常发生处的调用堆栈。第一行说明异常发生在init
函数,第二行说明init
函数的调用环境,此处在控制台直接调用,即至关于在匿名函数环境内调用。
上面例子是同步代码执行的异常,当异常发生在异步任务内时,又会如何呢?,假如把上例中代码放在一个setTimeout
定时器内执行:
function init(name) {
test(name)
}
setTimeout(function A() {
setTimeout(function() {
init();
}, 0);
}, 0);复制代码
如图:
能够看到,异步任务中的未捕获异常,也会在控制台输出,可是setTimeout
异步任务回调函数没有出如今异常堆栈,为何呢?这是由于当init
函数执行时,setTimeout
的异步回调函数不在执行栈内,而是经过事件队列调用。
JavaScript的异常捕获,主要有两种方式:
try{}catch(e){}主动捕获异常;
如上,对于同步执行大代码出现异常,try{}catch(e){}
是能够捕获的,那么异步错误呢?
如上图,咱们发现,异步回调中的异常没法被主动捕获,由浏览器默认处理,输出错误信息。
window.onerror事件处理器,全部未捕获异常都会自动进入此事件回调
如上图,输出了script error
错误信息,同时,你也许注意到了,控制台依然打印出了错误堆栈信 息,或许你不但愿用户看到这么醒目的错误提醒,那么可使window.onerror
的回调返回true便可阻止浏览器的默认错误处理行为:
固然,通常不随意设置window.onerror回调,由于程序一般可能须要部署前端异常监控系统,而一般就是使用window.onerror处理器实现全局异常监控,而该事件处理器只能注册一个回调。
以上咱们谈到的诸多关于回调的不足,都很常见,因此必须是须要解决的,而Promise正是一种很好的解决这些问题的方式,固然,如今已经提出了比Promise更先进的异步任务处理方式,可是目前更大范围使用,兼容性更好的方式仍是Promise,也是本篇要介绍的,以后会继续介绍其余处理方式。
分析了一大波问题后,咱们知道Promise的目标是异步管理,那么Promise究竟是什么呢?
因此,Promise是一种封装将来值的易于复用的异步任务管理机制。
为了更好的理解Promise,咱们介绍一下Promises/A+,一个公开的可操做的Promises实现标准。先介绍标准规范,再去分析具体实现,更有益于理解。
Promise表明一个异步计算的最终结果。使用promise最基础的方式是使用它的then
方法,该方法会注册两个回调函数,一个接收promise完成的最终值,一个接收promise被拒绝的缘由。
你可能还会想问Promises/A是什么,和Promises/A+有什么区别。Promises/A+在Promises/A议案的基础上,更清晰阐述了一些准则,拓展覆盖了一些事实上的行为规范,同时删除了一些不足或者有问题的部分。
Promises/A+规范目前只关注如何提供一个可操做的then
方法,而关于如何建立,决议promises是往后的工做。
then
方法的对象;then
方法的对象;undefined
,thenable
对象,promise
对象;一个promise只可能处于三种状态之一:
这三个状态变动关系需知足如下三个条件:
一个promise必须提供一个then
方法,以供访问其当前状态,或最终值或拒绝缘由。
参数
该方法接收两个参数,如promise.then(onFulfilled, onRejected)
:
返回值
该方法必须返回一个promise:
var promise2 = promise1.then(onFulfilled, onRejected);
// promise2依然是一个promise对象复制代码
决议是一个抽象操做过程,该操做接受两个输入:一个promise和一个值,能够记为;[[resolve]](promise, x)
,若是x是一个thenable
对象,则尝试让promise
参数使用x
的状态值;不然,将使用x
值完成传入的promise
,决议过程规则以下:
promise
和x
引用自同一对象,则使用一个TypeError
缘由拒绝此promise
;x
为Promise,则promise
直接使用x
的状态;x
为对象或函数:
x.then
的引用;x.then
时抛出异常e
,使用该e
做为缘由拒绝promise
;then
;then
是一个函数,就调用该函数,其做用域为x
,并传递两个回调函数参数,第一个是resolvePromise
,第二个是rejectPromise
:
resolvePromise(y)
,则执行resolve(promise, y)
;rejectPrtomise(r)
,则使用缘由r
拒绝promise
;then
抛出异常e
,则:
promise
已决议,即调用了resolvePromise
或rejectPrtomise
,则忽略此异常;e
拒绝promise
;then
不是函数,则使用x
值完成promise
;x
不是对象或函数,则使用x
完成promise
。天然,以上规则可能存在递归循环调用的状况,如一个promsie
被一个循环的thenable
对象链决议,此时天然是不行的,因此规范建议进行检测,是否存在递归调用,若存在,则以缘由TypeError
拒绝promise
。
在ES6中,JavaScript已支持Promise,一些主流浏览器也已支持该Promise功能,如Chrome,先来看一个Promsie使用实例:
var promise = new Promise((resolve, reject) => {
setTimeout(function() {
resolve('完成');
}, 10);
});
promise.then((msg) => {
console.log('first messaeg: ' + msg);
})
promise.then((msg) => {
console.log('second messaeg: ' + msg);
});复制代码
输出以下:
建立promise语法以下:
new Promise(function(resolve, reject) {});复制代码
参数
一个函数,该函数接受两个参数:resolve函数和reject函数;当实例化Promise构造函数时,将当即调用该函数,随后返回一个Promise对象。一般,实例化时,会初始一个异步任务,在异步任务完成或失败时,调用resolve或reject函数来完成或拒绝返回的Promise对象。另外须要注意的是,若传入的函数执行抛出异常,那么这个promsie将被拒绝。
all方法接受一个或多个promsie(以数组方式传递),返回一个新promise,该promise状态取决于传入的参数中的全部promsie的状态:
var p1 = new Promise((resolve, reject) => {
setTimeout(function(){
console.log('p1决议');
resolve('p1');
}, 10);
});
var p2 = new Promise((resolve, reject) => {
setTimeout(function(){
console.log('p2决议');
resolve('p2');
}, 10);
});
Promise.all( [p1, p2] )
.then((msgs) => {
// p1和p2完成并传入最终值
console.log(JSON.stringify(msgs));
})
.then((msg) => {
console.log( msg );
});复制代码
输出以下:
race方法返回一个promise,只要传入的诸多promise中的某一个完成或被拒绝,则该promise一样完成或被拒绝,最终值或拒绝缘由也与之相同。
resolve方法返回一个已决议的Promsie对象:
x
是一个promise或thenable
对象,则返回的promise对象状态同x
;x
不是对象或函数,则返回的promise对象以该值为完成最终值;该方法遵循Promise/A+决议规范。
返回一个使用传入的缘由拒绝的Promise对象。
该方法为promsie添加完成或拒绝处理器,将返回一个新的promise,该新promise接受传入的处理器调用后的返回值进行决议;若promise未被处理,如传入的处理器不是函数,则新promise维持原来promise的状态。
咱们经过两个例子介绍then
方法,首先看第一个实例:
var promise = new Promise((resolve, reject) => {
setTimeout(function() {
resolve('完成');
}, 10);
});
promise.then((msg) => {
console.log('first messaeg: ' + msg);
}).then((msg) => {
console.log('second messaeg: ' + msg);
});复制代码
输出以下:
输出两行信息:咱们发现第二个then
方法接收到的最终值是undefined
,为何呢?看看第一个then
方法调用后返回的promise
状态以下:
如上图,发现调用第一个then
方法后,返回promise最终值为undefined
,传递给第二个then
的回调,若是把上面的例子稍加改动:
var promise = new Promise((resolve, reject) => {
setTimeout(function() {
resolve('完成');
}, 10);
});
promise.then((msg) => {
console.log('first messaeg: ' + msg);
return msg + '第二次';
}).then((msg) => {
console.log('second messaeg: ' + msg);
});复制代码
输出以下:
此次两个then
方法的回调都接收到了最终值,正如咱们前文所说,'then'方法返回一个新promise,而且该新promise根据其传入的回调执行的返回值,进行决议,而函数未明确return
返回值时,默认返回的是undefined
,这也是上面实例第二个then
方法的回调接收undefined
参数的缘由。
这里使用了链式调用,咱们须要明确:共产生三个promise,初始promise,两个then方法分别返回一个promise;而第一个then
方法返回的新promise是第二个then
方法的主体,而不是初始promise。
该方法为promise添加拒绝回调函数,将返回一个新promise,该新promise根据回调函数执行的返回值进行决议;若promise决议为完成状态,则新promise根据其最终值进行决议。
var promise = new Promise((resolve, reject) => {
setTimeout(() => {
reject('failed');
}, 0);
});
var promise2 = promise.catch((reason) => {
console.log(reason);
return 'successed';
});
var promise3 = promise.catch((reason) => {
console.log(reason);
});
var promise4 = promise.catch((reason) => {
console.log(reason);
throw 'failed 2';
});复制代码
输出以下图:
如图中所输出内容,咱们须要明白如下几点:
catch
会为promise
注册拒绝回调函数,一旦异步操做结束,调用了reject
回调函数,则依次执行注册的拒绝回调;then
方法类似,catch
方法返回的新promise将使用其回调函数执行的返回值进行决议,如promise2,promise3状态均为完成(resolved),可是promise3最终值为undefined
,而promise2
最终值为successed
,这是由于在调用promise.catch
方法时,传入的回调没有显式的设置返回值;catch
方法时,回调中throw
抛出异常,因此promise4状态为拒绝(rejected),拒绝缘由为抛出的异常;then
方法,仍是catch
方法,都会返回一个新promise,此新promise与初始promise相互独立。catch
方法和then
方法的第二个参数同样,都是为promise注册拒绝回调。
和jQuery的链式调用同样,Promise设计也支持链式调用,上一步的返回值做为下一步方法调用的主体:
new Promise((resolve, reject) => {
setTimeout(()=>{
resolve('success');
},0);
}).then((msg) => {
return 'second success';
}).then((msg) => {
console.log(msg);
});复制代码
最后输出:second success
,初始化promise做为主体调用第一个then
方法,返回完成状态的新promise其最终值为second success
,而后该新promise做为主体调用第二个then
方法,该方法返回第三个promise,并且该promise最终值为undefined
,若不清楚为何,请回到关于Promise.prototype.then
和Promise.prototype.catch
的介绍。
咱们前文提到了JavaScript异步回调中的异常是难以处理的,而Promise对异步异常和错误的处理是比较方便的:
var promise = new Promise((resolve, reject) => {
test(); // 抛出异常
resolve('success'); // 被忽略
});
console.log(promise);
promise.catch((reason) => {
console.log(reason);
});复制代码
输出如图,执行test
抛出异常,致使promise被拒绝,拒绝缘由即抛出的异常,而后执行catch
方法注册的拒绝回调:
目前为止,关于Promise是什么,咱们应该有了必定的认识,这里,须要再次说明的是Promise的三个重要概念及其关系:决议(resolve),完成(fulfill),拒绝(reject)。
Promise.resolve
描述的就是一个决议过程,而Promise构造函数,传入的回调函数的两个参数:resolve和reject,一个是完成函数,一个是拒绝函数,这里使人疑惑的是为何这里依然使用resolve而不是fulfill,咱们经过一个例子解释这个问题:var promise = new Promise((resolve, reject) => {
resolve(Promise.reject('failed'));
});
promise.then((msg) => {
console.log('完成:' + msg);
}, (reason) => {
console.log('拒绝:' + reason);
});复制代码
输出如图:
上例中,在建立一个Promise时,给resolve
函数传递的是一个拒绝Promise,此时咱们发现promise状态是rejected
,因此这里第一个参数函数执行,完成的是一个更接近决议的过程(能够参考前文讲述的决议过程),因此命名为resolve
是更合理的;而第二个参数函数,则只是拒绝该promise:
var promise = new Promise((resolve, reject) => {
reject(Promise.resolve('success'));
});
promise.then((msg) => {
console.log('完成:' + msg);
}, (reason) => {
console.log('拒绝:' + reason);
});复制代码
reject
函数并不会处理参数,而只是直接将其当作拒绝缘由拒绝promise。
Promise是什么,怎么样使用就介绍到此,另一个问题是面试过程当中常常也会被说起的:如何实现一个Promise,固然,限于篇幅,咱们这里只讲思路,不会长篇大论。
首先建立一个构造函数,供实例化建立promise,该构造函数接受一个函数参数,实例化时,会当即调用该函数,而后返回一个Promise对象:
var MyPromise = (() => {
var value = undefined; // 当前Promise
var tasks = []; // 完成回调队列
var rejectTasks = []; // 拒绝回调队列
var state = 'pending'; // Promise初始为等待态
// 辅助函数,使异步回调下一轮事件循环执行
var nextTick = (callback) => {
setTimeout(callback, 0);
};
// 辅助函数,传递Promsie的状态值
var ref = (value) => {
if (value && typeof value.then === 'function') {
// 若状态值为thenable对象或Promise,直接返回
return value;
}
// 不然,将最终值传递给下一个then方法注册的回调函数
return {
then: function(callback) {
return ref(callback(value));
}
}
};
var resolve = (val) => {};
var reject = (reason) => {};
function MyPromise(func) {
func(resolve.bind(this), reject.bind(this));
}
return MyPromise;
});复制代码
在实例化建立Promise时,咱们会将构造函数的两个静态方法:resolve
和reject
传入初始函数,接下来须要实现这两个函数:
var resolve = (val) => {
if (tasks) {
value = ref(val);
state = 'resolved'; // 将状态标记为已完成
// 依次执行任务回调
tasks.forEach((task) => {
value = nextTick((val) => {task[0](self.value);});
});
tasks = undefined; // 决议后状态不可变
return this;
}
};
var reject = (reason) => {
if (tasks) {
value = ref(reason);
state = 'rejected'; // 将状态标记为已完成
// 依次执行任务回调
tasks.forEach((task) => {
nextTick((reason) => {task[1](value);});
});
tasks = undefined; // 决议后状态不可变
return this;
}
};复制代码
还有另外两个静态方法,原理仍是同样,就不细说了。
目前构造函数,和静态方法完成和拒绝Promise都已经实现,接下来须要考虑的是Promise的实例方法和链式调用:
MyPromise.prototype.then = (onFulfilled, onRejected) => {
onFulfilled = onFulfilled || function(value) {
// 默认的完成回调
return value;
};
onRejected = onRejected || function(reason) {
// 默认的拒绝回调
return reject(reason);
};
if (tasks) {
// 未决议时加入队列
tasks.push(onFulfilled);
rejectTasks.push(onRejected);
} else {
// 已决议,直接加入事件循环执行
nextTick(() => {
if (state === 'resolved') {
value.then(onFulfilled);
} else if (state === 'rejected') {
value.then(onRejected);
}
});
}
return this;
};复制代码
以上能够简单实现Promise部分异步管理功能:
var promise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('完成');
}, 0);
});
promise.then((msg) => {console.log(msg);});复制代码
本篇由回调函数起,介绍了回调处理异步任务的常见问题,而后介绍Promises/A+规范及Promise使用,最后就Promise实现作了简单阐述(以后有机会会详细实现一个Promise),花费一周终于把基本知识点介绍完,下一篇将介绍JavaScript异步与生成器实现。