Promise
是我最喜欢的es6语法,也是面试中最容易问到的部分。那么怎么作到在使用中驾轻就熟,在面试中脱颖而出呢?
先来个面试题作作:html
面试官常常会让手写一个Promise封装,写出下面这一版就好了(想了解更多的可自行扩展):java
function ajaxMise(url, method, data, async, timeout) {
var xhr = new XMLHttpRequest()
return new Promise(function (resolve, reject) {
xhr.open(method, url, async);
xhr.timeout = options.timeout;
xhr.onloadend = function () {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304)
resolve(xhr);
else
reject({
errorType: 'status_error',
xhr: xhr
})
}
xhr.send(data);
//错误处理
xhr.onabort = function () {
reject(new Error({
errorType: 'abort_error',
xhr: xhr
}));
}
xhr.ontimeout = function () {
reject({
errorType: 'timeout_error',
xhr: xhr
});
}
xhr.onerror = function () {
reject({
errorType: 'onerror',
xhr: xhr
})
}
})
}
复制代码
Promise
是一个对象,保存着将来将要结束的事件。她有两个特征,引用阮一峰老师的描述就是:react
(1)对象的状态不受外界影响。Promise对象表明一个异步操做,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操做的结果,能够决定当前是哪种状态,任何其余操做都没法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其余手段没法改变。
(2)一旦状态改变,就不会再变,任什么时候候均可以获得这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种状况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。若是改变已经发生了,你再对Promise对象添加回调函数,也会当即获得这个结果。这与事件(Event)彻底不一样,事件的特色是,若是你错过了它,再去监听,是得不到结果的。es6
let promise1 = new Promise(function (resolve, reject){
setTimeout(function (){
resolve('ok') //将这个promise置为成功态(fulfilled),会触发成功的回调
},1000)
})
promise1.then(fucntion success(val) {
console.log(val) //一秒以后会打印'ok'
})
复制代码
class PromiseM {
constructor (process) {
this.status = 'pending'
this.msg = ''
process(this.resolve.bind(this), this.reject.bind(this))
return this
}
resolve (val) {
this.status = 'fulfilled'
this.msg = val
}
reject (err) {
this.status = 'rejected'
this.msg = err
}
then (fufilled, reject) {
if(this.status === 'fulfilled') {
fufilled(this.msg)
}
if(this.status === 'rejected') {
reject(this.msg)
}
}
}
//测试代码
var mm=new PromiseM(function(resolve,reject){
resolve('123');
});
mm.then(function(success){
console.log(success);
},function(){
console.log('fail!');
});
复制代码
上面提到Promise
和事件的不一样,除此以外还有一个重要不一样,就是Promise
建立是micro-task
。再看一道面试题:面试
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function () {
console.log('promise1');
}).then(function () {
console.log('promise2');
});
console.log('script end');
复制代码
正确答案是:'script start'、'script end'、'promise1'、'promise2'、'setTimeout'。缘由就是:ajax
Event Loop
是js的一个重要机制,就是遇到事件或者setTimeout
等就会把对应的回调函数放入一个事件队列(task queue),等到主程序执行完毕就依次把队列里的函数压入栈中执行。能够参考阮一峰老师的JavaScript 运行机制详解:再谈Event Loop,不过貌似老师的网站被攻击尚未恢复。
可是Promise
不是上面的机制,她建立的是一个微任务(micro-task),micro-task
的执行老是在当前执行栈结束和下一个task
执行以前,顺序就是“当前执行栈” -> “micro-task” -> “task queue中取一个回调” -> “micro-task” -> ... (不断消费task queue) -> “micro-task”,总之就是当前执行栈为空时,就到了一个micro-task
的检查点。
下面是micro-task
的定义:数据库
Microtasks are usually scheduled for things that should happen straight after the currently executing script, such as reacting to a batch of actions, or to make something async without taking the penalty of a whole new task.
编程
Promise
注册的是micro-task
,因此上面题目中:主线程中'script start'、'script end'先打印,而后清空微任务队列,'promise1'、'promise2'打印,而后取出task queue
中的回调执行,'setTimeout'打印。后端
Promise
提供了对js异步编程的新的解决方案,由于咱们一直使用的回调函数实际上是存在很大问题,只是限制于js的单线程等缘由不得不大量书写。固然Promise
并非彻底摆脱回调,她只是改变了传递回调的位置。那么传统的回调存在什么问题呢?promise
这里所说的嵌套是指大量的回调函数会使得代码难以读懂和修改,试想一个这个场景:让你把下面的url4的调用提到url2以前。你须要很是当心的剪切代码,而且笨拙的粘贴,result4这个参数你还不敢修改,由于这要额外花费不少功夫而且存在风险。
$.ajax('url1',function success(result1){
$.ajax('url2',function success(result2){
$.ajax('url3',function success(result3){
$.ajax('url4',function success(result4){
//……
})
})
})
})
复制代码
固然,上面的问题有点戏剧成分,现实中极少出现这种难搞的状况。与此相比,回调函数带来的思惟上的难以理解是更致命的,由于咱们的大脑更喜欢同步的逻辑,这也是为何await
关键字那么受欢迎的缘由。
我记得有一次我给后端的同窗作JS新特性分享的时候,说到await
关键字,有我的惊呼:“哇!这个不错啊,这就能够像写java同样写代码了”。
除去书写的不优雅和维护的困难之外,回调函数其实还存在信任问题。
事实上回调函数不必定会像你指望的那样被调用。由于控制权不在你的手上。这种问题被称做“控制反转”。例以下面的例子:
$.ajax('xxxxxx',function success(result1){
//好比成功以后我会操做数据库记录结算金额
})
复制代码
上面是jQuery
中的ajax
调用,咱们指望在某些事件结束后,让第三方(jQ)帮咱们执行个人程序(回调)。
那么,咱们和第三方之间并无一个契约或者规范能够遵循,除非你把你想使用的第三方库通读一遍,保证它作了你想作的事,但事实上你很难肯定。即便在本身的代码中,或者本身编写的工具,咱们都很难作到百分之百信任。
Promise
是一个规范,尝试以一种更加友好的方式书写代码。Promise
对象接受一个函数做为参数,函数提供两个参数:
promise
从未完成切换到成功状态,也就是上面提到的从pending
切换到fufilled
,resolve
能够传递参数,下一级promise
中的成功函数会接收到它promise
从未完成切换到失败状态,即从pending
切换到rejected
let promise1 = new Promise(function(reslove, reject){
//reslove或者reject或者出错
})
promise1.then(fufilled, rejected).then().then() //这是伪代码
promise1.then(fufilled, rejected)//能够then屡次
function fufilled(data) {
console.log(data)
}
function rejected(e){
console.log(e)
}
复制代码
正如上面提到的两个特征,一旦状态改变,这个Promise
就已经完成决议(不会再更改),而且返回一个新的Promise
,能够链式调用。而且能够注册多个then
方法,他们同时决议而且互不影响。这种设计明显比回调函数要优雅的多,也更易于理解和维护。那么在信任问题上她又有哪些改善呢?
Promise
经过通知的机制将“控制反转”的关系又“反转”回来。回调是我传递给第三方一个函数,指望它在事件发生时帮我执行,而Promise
是在你们都遵循规范的前提下,我会在事件发生时获得通知,这时我决定作一些事(执行一些函数)。看到了吧,这是有本质差别的。
此外,回调函数还有如下信任问题,Promise
也都作了相关约束:
一个Promise
回调必定会在当前栈执行完毕和下一个异步时机点上调用,即便像下面这样的同步resolve
代码也会异步执行,而你传给工具库的回调函数却可能被同步执行(调用过早)或者被忘记执行(或者过晚)。
new Promise(function (resolve) {
resolve(111111);
})
复制代码
Promise
只能被决议一次,若是你屡次决议,她只会执行第一次决议,例如:
new Promise(function (reslove, reject) {
resolve()
setTimeout(function () {
resolve(2)
},1000)
resolve(3)
}).then(function (val) {
console.log(val) //undefined
})
复制代码
成功回调的参数是经过resolve
传递的,例如像上面的代码同样,没有传递参数,那么val收到的会是undefined
,因此,不管如何都会收到参数。注意:resolve
只接收一个参数,以后的参数会被忽略。
Promise
的错误处理机制是这样的:若是显示的调用reject
并传递错误理由,这个消息会传递给拒绝回调。
此外,若是任意过程当中出现错误(例如TypeError或者ReferenceError),这个错误会被捕捉,而且使这个Promise
拒绝,也就是说这个错误消息也会传递给拒绝回掉,这与传统的回调是不一样的,传统的回调一旦出错会引发同步相应,而不出错则是异步。
all
和race
两个函数都是并发执行promise
的方法,他们的返回值也是promise
,all
会等全部的promise
都决议以后决议,而race
是只要有一个决议就会决议。
Promise.all([promise1, promise2, promise3]).then(function(values) {
console.log(values);
});
复制代码
注意:若是参数为空,
all
方法会马上决议,而race
方法会挂住。
Promise.all = function(ary) {
let num = 0
let result = []
return new Promise(function(reslove, reject){
ary.forEach(promise => {
promise.then(function(val){
if(num >= ary.length){
reslove(result)
}else{
result.push(val)
num++
}
},function(e){
reject(e)
})
})
})
}
复制代码
你肯能会想到 instanceof Promise
,但遗憾的是不能够。缘由是每种环境都封装了本身的Promise
,而不是使用原生的ES6 Promise
。
因此目前判断Promise
的一种方法就是判断它是否是thenable
对象(若是它是一个对象或者函数,而且它具备then
方法)。
这是一种js常见的类型检测方法——鸭子类型检测:
鸭子类型检测:若是它看起来像鸭子,叫起来也像鸭子,那么它就是鸭子
resolve
返回一个当即成功的Promise
,reject
返回一个当即失败的Promise
,他们是new Promise
的语法糖,因此下面两个写法是等价的:
let p1 = new Promise(function(resolve, reject){
reslove(11111)
})
let p2 = Promise.resolve(11111) //这和上面的写法结果同样
复制代码
此外,若是传入reslove
方法的参数不是promise
而是一个thenable
值,那么reslove
会将它展开。最终的决议值由then
方法来决定。
上面提到,Promise
是异步处理错误,也就是说个人错误要在下一个Promise
才能捕获到,大多状况这是好的,可是存在一个问题:若是捕获错误的代码再出现错误呢?
个人作法一般是在代码的最后加catch
:
let p1 = new Promise(function(reslove, reject){
ajax('xxxxx')
})
p1
.then(fullfilled, rejected)
.then(fullfilled, rejected)
.catch(function(e){
//处理错误
})
复制代码
文章到这里就结束了,若是你看完了而且所以思考了一些东西,我很高兴。
接下来会继续更新Promise
+generator
、异步函数等Promise
相关知识,愿共同进步。