赞
踩
一个Promise对象可以理解为这样一个状态机,它(通常)接收一个异步任务作为输入,然后去执行这个异步任务,根据异步任务的执行结果来改变自身的状态,并保留这个执行结果 。
这个状态机总共有三种状态:pending(异步任务执行中)、fullfilled(执行成功)和rejected(执行失败)。在异步任务执行完之前,状态机处于pending状态;一旦任务执行成功,状态就会转为fullfilled;如果任务执行失败,则状态转为rejected。一旦发生了状态转变,状态机就会冻结在该状态,在任何情况下都不会再次改变状态。此时的状态机处于resolved(已完成)状态,它表示状态机已经执行完了异步任务。
我们可以通过Promise对象上的原型方法then为状态机注册回调函数,注册进来的回调函数会在状态发生相应变化时被调用。它接收两个函数作为参数,第一个是状态为fullfilled(成功)时要调用的函数,第二个是状态为rejected(失败)时需要执行的函数。我们可以多次调用then方法为promise注册多个回调函数,他们以数组的形式保存在promise内,并在状态机状态变化时依次被调用。举个例子:
//这里传入的function就是我们封装的一个异步任务 var promise = new Promise(function(resolve, reject){ $.ajax({ url: url, method: "GET", success: function(data){ resolve(data); //将状态机的状态变为fullfilled(成功) }, error: function(err){ reject(err); //将状态机的状态变为rejected(失败) } }) }); promise.then(function(){...}, function(){...}); //两者分别在异步任务执行成功和失败时被调用 //我们又为promise注册了第二个fullfilled回调函数,它也会在状态转为fullfilled时执行 promise.then(function(){...})
在使用new Promise生成一个promise对象时,我们传入了一个函数作为参数,它封装了一个ajax异步任务。js引擎在调用这个函数时会为我们提供两个定义在Promise内部的方法resolve和reject(由于只是形参,你可以用任何合法的变量名来接收这两个参数),前者用于通知状态机将状态由pending变为fullfilled(成功),后者用于通知状态机将状态由pending变为rejected(失败)。我们在调用这两个方法时传入的值,将作为状态机执行异步任务的结果保存在状态机内,并在执行回调函数时作为参数传入回调函数。
前面说到,状态机的状态一旦发生了变化,就会被冻结。因此如果你调用了resolve将状态机的状态变为成功,那么就不能再将其变为失败,反之亦然。由于状态机被冻结,js引擎将不会执行resolve或reject语句后面的代码(因为它们已经无法再影响到状态机)。
说到这里,我们就可以理解一下Promise为什么被称为Promise。Promise中文释义为“承诺”,那么promise对象向我们“承诺”了什么呢?
它承诺的是它的状态永远只取决于异步任务的执行结果,你无法在状态机的外部通过任何方式改变状态机的状态。而且promise会为你保留这个异步任务的执行结果,你可以在任何时候向它注册回调函数,它都会根据当前状态为你执行它们,哪怕这个异步任务早已经执行完毕。如:
var promise = new Promise(function(resolve, reject){
setTimeout(function(){
resolve(1)
}, 100); //100毫秒后将状态变为fullfilled
}).then(function(data){ //这里是在异步任务还未执行完时就注册到promise对象上的回调
console.log(data);
})
//我们在5秒后又为promise注册了一个回调,显然此时异步任务已经执行完了,
//但我们注册的回调函数依然会被执行
setTimeout(function(){
promise.then(function(){...}) //这里的回调也会被执行
}, 5000)
注意:传入Promise构造函数的函数(也就是我们封装的异步任务)会立即被调用,因此是同步执行的。而我们为该对象注册的回调函数将被存储在微任务队列,他们会在同步任务执行完毕后被执行,但是执行的优先级高于宏任务(如setTimeout任务),这点在Promise的实现中可以找到答案。
要回答这个问题,我们可以先思考一下以前我们是如何按顺序执行多个异步任务的(当上个异步任务的结果会对下一个异步任务产生影响时,你必须保证它们按顺序执行,否则后面的异步任务将无法启动)。
不知道你有没有写过下面这种令人头皮发麻的代码:
$.ajax({ url: url, method: "GET", success: function(data){ //在第一个ajax请求成功后,继续发送第二个ajax请求 $.ajax({ url: url, method: "GET", data: data, success: function(data){ //第二个ajax请求成功后,继续发送第三个ajax请求 $.ajax({ url: url, method: "GET", data: data, success: function(data){ ...... //这里可能还有更多的ajax请求需要发送 }, error: function(err){ console.log(err); } }) }, error: function(err){ console.log(err); } }) }, error: function(err){ console.log(err); } })
类似的回调函数嵌套在前端称为“回调地狱”。过深的嵌套会导致代码非常难以阅读,难以阅读的代码从另一个角度来说就是难以维护,而难以维护的代码就是垃圾代码。如果写成下面的形式,代码看上去就会清爽很多:
var promise = new Promise(function(resolve, reject){ $.ajax({ url: url, method: "GET", success: function(data){ resolve(data); //将状态机的状态变为fullfilled(成功) }, error: function(err){ reject(err); //将状态机的状态变为rejected(失败) } }) }).catch(function(err){ console.log(err) }).then(function(data){ return new Promise(function(resolve, reject){ $.ajax({ url: url, method: "GET", data: data, success: function(data){ resolve(data); //将状态机的状态变为fullfilled(成功) }, error: function(err){ reject(err); //将状态机的状态变为rejected(失败) } }) }) }).catch(function(err){ console.log(err) }).then(function(data){ return new Promise(function(resolve, reject){ $.ajax({ url: url, method: "GET", data: data, success: function(data){ resolve(data); //将状态机的状态变为fullfilled(成功) }, error: function(err){ reject(err); //将状态机的状态变为rejected(失败) } }) }) }).catch(function(err){ console.log(err); })
我们可以很容易理解上面的代码在做什么:先发送一个ajax请求,成功后执行then内的回调函数,并把上个ajax的返回值传进去,依次类推。由于这里的每个then方法都返回了新的promise对象,因此你需要在每个then后面都附带一个catch,依次处理每个promise抛出的异常。该代码同样实现了三个ajax任务的顺序执行,但由于采用了链式结构,代码变得非常易读。
如果你不太理解为什么Promise可以使用链式语法,没关系,我们会在后面手动实现Promise时详细介绍这一点。
除此之外,如果不使用Promise,你必须在创建一个异步任务时就指定回调函数,已经执行或执行完毕的异步任务不会接收新的回调函数。而Promise为异步任务提供了更好的封装,它可以保留异步任务的结果状态,以便你可以在任何时候为异步任务注册回调。
Promise因对异步任务的良好封装,而被纳入了ES6语言规范。
上面已经介绍了Promise最简单的用法,就是创建promise对象时传入一个异步任务,然后注册成功和失败回调。当异步任务执行完毕,状态机会根据自身状态调用相应的回调函数。下面我们来看一下Promise的更多常见用法。
我们上面说到,then方法可以接收两个函数,分别是成功和失败时需要调用的回调函数,但是在实际使用Promise时我们却很少这样写。我们一般只会为then传入一个成功的函数,然后使用链式语法继续添加一个catch方法来处理失败的情况。举个例子:
//我们一般不会这样写
var promise = new Promise(function(resolve, reject){
... //执行一个异步任务
}).then(function(data){/*成功回调*/}, function(err){/*失败回调*/})
//我们更推荐这样写
var promise = new Promise(function(resolve, reject){
... //执行一个异步任务
}).then(function(data){/*成功回调*/})
.catch(function(err){/*失败回调*/})
显然使用catch更加如何符合链式语法。而catch本质上只是then方法的一个变形,它等同于下面的实现:
Promise.prototype.catch = function(onRejected){
return this.then(null, onRejected)
}
也就是说,当你向then方法的第一个参数传入null或undefined,它就是catch方法,因为此时的then只能处理失败的情况。这样做的一个很大的优势在于,当链式调用很长,而你并不在乎出错是哪个调用导致的,你就可以只在最后面写一个catch处理即可。如:
new Promise(function(resolve, reject){
...
}).then(function(){
...
}).then(function(){
...
}).then(function(){
...
}).catch(function(err){
... //在这里捕获错误
})
网上有人把这种情况归结为Promise的数据传递具有“穿透”性,错误对象会在链式语法中进行传递。不过我本人更倾向于另一种解释:这里的三个then和一个catch都是注册在同一个Promise对象上的(假设注册的then方法没有返回值,如果有,它可能会影响原Promise对象的data),它们会共享同一个promise状态机的状态,也就是说每个then其实都可以拿到相同的参数,所以这种现象是理所当然的。
从内部结构来看,前三个传入then的函数被添加到了promise对象的成功回调函数队列,第四个catch中的函数被添加到了失败回调函数队列,如下:
promise = {
resolvedFunc: [func1, func2, func3], //注册进来的成功回调函数
rejectedFunc: [func] //注册进来的失败回调函数
... //其他属性
}
假如promise的状态变为成功,则resolvedFunc队列的函数依次被执行,并传入调用resolve方法时传入的异步任务的执行结果。假如promise的状态变为失败,则rejectedFunc队列的函数依次被执行,以reject方法抛出的错误为参数。
也就是说,虽然我们通过链式语法连续注册了四个回调函数,但他们最终会进入两个队列中,在实际调用时没有任何关系。每个回调函数在执行时所接收的数据对象都仅仅来自于promise实例,而不是由上一个函数“传递”下来的。
不过当你在then方法中重新创建并返回了一个新的Promise对象后,你后续注册的回调函数都会注册到这个新的promise对象上,你可以手动将上个promise抛出的异常传递给下一个promise。如:
new Promise(function(resolve, reject){
...
}).then(function(data){
return new Promise(function(resolve, reject){
...
//你可以在这里继续传递上一个promise的任务结果
})
}).then(function(data){
//现在你是在为上一个then中返回的新的promise对象注册回调函数,而不是最初的那个promise
})
假设你现在是在访问文件系统进行文件读取,那么无论读取成功或失败,你都应该关闭文件模块,这时候你就可以使用finally方法,它规定无论promise的状态无论变成成功还是失败,都要执行该回调函数。如:
new Promise(function(resolve, reject){
fs.open();
fs.readFile(filename, function(...){
...
})
})
.then(function(data){...}).catch(function(err){...})
.finally(function(){
fs.close(); //无论文件读取成功或失败,都关闭文件模块
})
Promise.all接收若干个promise对象构成的数组作为参数,返回一个新的promise对象。如:
var p = Promise.all([p1, p2, p3]);
p.then(function(){...}).catch(function(){...})
这里的p1,p2,p3都是promise对象,p的状态符合以下情况:
简单来说,当三者没有全部完成时,p就未完成,当三者全部成功,p就为成功,只要有一个失败,p的状态就是失败。
当你需要在多个异步任务全部成功后执行某些操作,你就可以使用Promise.all()。假如现在要读取若干个json文件,然后在全部读取成功后提示用户,就可以这么写:
// 生成一个Promise对象的数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON('/post/' + id + ".json");
});
Promise.all(promises).then(function (posts) {
//文件读取成功
...
}).catch(function(reason){
//文件没有全部读取到
...
});
race:中文释义为赛跑,在这里的含义为哪个执行得快,它就可以决定最终promise的状态。
与上面的Promise.all类似,race方法也会返回一个Promise对象,但它的状态取决于最先完成的那个Promise的状态。比如我们现在要取一个文件,但是规定了超时时间为5秒(超过5秒就判定为超时),我们可以使用下面的语法:
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p.then(console.log)
.catch(console.error);
我们为Promise.race传入了两个promise,前者用于获取一个资源,后者会在5秒后将状态变为失败。假如fetch先执行完(由于第二个promise会在5秒后改变状态,因此fetch先执行完的含义就是,该http请求在5秒内得到响应),它就可以率先改变最终的promise的状态,使其与自身状态保持一致,否则setTimeout定时任务会把状态变为失败。
这里指Promise.allSettled()、Promise.any()、Promise.resolve()、Promise.reject()。由于篇幅有限,并且这些方法较新或者用的不多,我们只做简单的介绍,详细请参考阮一峰 Promise入门。
Promise.allSettled返回一个新的Promise对象,记为p,也接收一个promise对象数组作为参数。当所有的promise状态都变为resolved(已完成,不管成功或失败)时,p的状态变为fullfilled。否则p的状态将一直是pending。注意,Promise.allSettled返回的promise没有失败状态,它永远只会从未完成转变为成功。
Promise.any同上,返回一个新的Promise对象,记为p,也接收一个promise对象数组作为参数。只要有一个promise的状态变为fullfilled(成功),p的状态就是fullfilled。只有所有的promise都是rejected(失败)时,p的状态才会变成rejected。这一点类似于数组方法中的some。
Promise.resolve可以将一个普通对象(或基本数据类型)转化为一个promise对象,如果该对象具有then方法,那么它将作为异步任务传递给新创建的promise对象,否则,js引擎会直接创建一个promise对象,状态为resolved,并且值为传入的那个参数。这里不再详述。
Promise.reject可以创建一个状态为rejected的promise对象。同样的,如果传入的对象具有then方法,它将作为异步任务用于promise的创建,即:
const thenable = { //这是一个具有then方法的对象 then(resolve, reject) { reject('出错了'); } }; Promise.reject(thenable) .catch(e => { console.log(e === thenable) }) //上面等同于该写法,resolve方法也是同样的原理 new Promise(thennable.then) .catch(e => { console.log(e === thenable) })
看了上面的介绍,你可能还是不了解Promise的内部原理究竟是怎么样的。没关系,我们可以一步步手动实现一个Promise类(本代码参考自github项目xieranmaya/Promise3,感兴趣的可以阅读原作者的介绍)。
首先,根据我们调用Promise的方式可以知道,Promise应该是一个构造函数,并且接受一个函数作为参数。
//这里的exector就是我们传入new Promise的函数,它封装了我们的异步任务
function Promise(executor){
...
}
根据前文介绍,我们知道一个promise内至少应该维护四个变量:状态、异步任务的结果、成功的回调队列、失败的回调队列。
function Promise(executor){
var self = this
self.status = 'pending' // Promise当前的状态
self.data = undefined // Promise的值
self.onResolvedCallback = [] //成功时的回调
self.onRejectedCallback = [] //失败时的回调
...
}
接下来我们来定义resolve和reject这两个内部函数,我们需要把它们作为参数传给传入的executor函数。它们的任务有三个:
function Promise(executor) { var self = this self.status = 'pending' // Promise当前的状态 self.data = undefined // Promise的值 self.onResolvedCallback = [] //成功时的回调 self.onRejectedCallback = [] 失败时的回调 function resolve(value) { if (self.status === 'pending') { self.status = 'fullfilled'; // 在这里将promise的状态变为fullfilled self.data = value; // 接收调用resolve时传入的异步任务的执行结果,保存在data中 //依次调用成功回调队列的回调函数 for(var i = 0; i < self.onResolvedCallback.length; i++) { self.onResolvedCallback[i](value) } } function reject(reason) { if (self.status === 'pending') { self.status = 'rejected'; // 在这里将promise的状态变为rejected self.data = reason; // 接收调用reject时传入的失败原因,保存在data中 //依次调用失败回调队列的回调函数 for(var i = 0; i < self.onRejectedCallback.length; i++) { self.onRejectedCallback[i](reason) } } } //考虑到执行executor的过程中有可能出错,所以我们用try/catch块给包起来, //并且在出错后以catch到的值reject掉这个Promise try { executor(resolve, reject) // 执行executor } catch(e) { reject(e) } }
现在Promise的构造函数已经封装完毕了。它已经可以根据传入的异步任务的执行结果改变自身的状态,并一直维护这个状态了。从这里可以看到,我们在调用Promise的构造函数时立即执行了传入的executor,因此它是同步执行的。
但是到现在,我们还没有提供向成功和失败的回调函数队列添加回调函数的方法,如果不能为开发者执行回调函数,那么这个状态机将没有任何意义。所以我们现在要实现promise的then方法了。
在之前的讲解中我们看到,then方法是在promise实例对象上调用的,那么很显然,它应该是Promise原型上的方法。所以我们现在为Promise的原型对象定义一个then方法:
//then方法接受一个成功的回调和一个失败的回调 Promise.prototype.then = function(onResolved, onRejected){ var self = this var promise2 // 根据标准,如果then的参数不是function,则我们需要忽略它,此处以如下方式处理 onResolved = typeof onResolved === 'function' ? onResolved : function(v) {} onRejected = typeof onRejected === 'function' ? onRejected : function(r) {} if (self.status === 'fullfilled') { return promise2 = new Promise(function(resolve, reject) { try { //维持新promise与之前promise对象的状态同步,这样在进行 //链式调用时,就如同在访问同一个promise对象 var x = onResolved(self.data) if (x instanceof Promise) { // 如果onResolved的返回值是一个Promise对象,直接取它的结果做为promise2的结果 x.then(resolve, reject) } else { resolve(x) // 否则,以它的返回值做为promise2的结果 } } catch (e) { reject(e) // 如果出错,以捕获到的错误做为promise2的结果 } }) } if (self.status === 'rejected') { return promise2 = new Promise(function(resolve, reject) { try { //同上 var x = onRejected(self.data) if (x instanceof Promise) { x.then(resolve, reject) } else { reject(x) // 否则,以它的返回值做为promise2的结果 } } catch (e) { reject(e) } }) } //如果当前promise还未执行完毕,则需要把注册的回调函数添加到Promise中对应的队列里 //并像上面一样创建一个promise,以支持链式调用 if (self.status === 'pending') { return promise2 = new Promise(function(resolve, reject) { self.onResolvedCallback.push(function(value) { try { var x = onResolved(self.data) if (x instanceof Promise) { x.then(resolve, reject) } else { resolve(x) // 否则,以它的返回值做为promise2的结果 } } catch (e) { reject(e) } }) self.onRejectedCallback.push(function(reason) { try { var x = onRejected(self.data) if (x instanceof Promise) { x.then(resolve, reject) } else { reject(x) // 否则,以它的返回值做为promise2的结果 } } catch (e) { reject(e) } }) }) } }
我们看到,then方法的返回值也是一个Promise对象,这是Promise支持链式语法的关键。因为当你调用了then方法后得到的又是一个promise对象,那么你就可以在这个对象上继续调用then方法,也就形成了链式结构。
这里then方法根据当前状态的不同走了三个分支。如果当前状态为fullfilled,那么应该立即执行刚刚注册的成功的回调函数,传入异步任务的结果data。然后按照当前promise的状态创建一个新的promise对象并返回,以此支持链式调用。如果当前状态时rejected,过程类似,只是需要执行失败的回调函数。而如果当前状态为pending,说明异步任务还没有执行完,因此不能执行回调函数。这时需要把传入的成功和失败的回调函数添加到对应的回调队列,等待任务执行完毕时执行。
同时我们还可以实现catch方法,它是then方法的变形:
Promise.prototype.catch = function(onRejected) {
return this.then(null, onRejected)
}
至此,我们就实现了一个很简单的Promise,它具有我们上面提到的大部分功能,并且很好地揭示了Promise的工作原理。完整的实现代码如下:
function Promise(executor) { var self = this self.status = 'pending' // Promise当前的状态 self.data = undefined // Promise的值 self.onResolvedCallback = [] //成功时的回调 self.onRejectedCallback = [] 失败时的回调 function resolve(value) { if (self.status === 'pending') { self.status = 'fullfilled'; // 在这里将promise的状态变为fullfilled self.data = value; // 接收调用resolve时传入的异步任务的执行结果,保存在data中 //依次调用成功回调队列的回调函数 for(var i = 0; i < self.onResolvedCallback.length; i++) { self.onResolvedCallback[i](value) } } function reject(reason) { if (self.status === 'pending') { self.status = 'rejected'; // 在这里将promise的状态变为rejected self.data = reason; // 接收调用reject时传入的失败原因,保存在data中 //依次调用失败回调队列的回调函数 for(var i = 0; i < self.onRejectedCallback.length; i++) { self.onRejectedCallback[i](reason) } } } //考虑到执行executor的过程中有可能出错,所以我们用try/catch块给包起来, //并且在出错后以catch到的值reject掉这个Promise try { executor(resolve, reject) // 执行executor } catch(e) { reject(e) } } Promise.prototype.then = function(onResolved, onRejected){ var self = this var promise2 // 根据标准,如果then的参数不是function,则我们需要忽略它,此处以如下方式处理 onResolved = typeof onResolved === 'function' ? onResolved : function(v) {} onRejected = typeof onRejected === 'function' ? onRejected : function(r) {} if (self.status === 'fullfilled') { return promise2 = new Promise(function(resolve, reject) { try { //维持新promise与之前promise对象的状态同步,这样在进行 //链式调用时,就如同在访问同一个promise对象 var x = onResolved(self.data) if (x instanceof Promise) { // 如果onResolved的返回值是一个Promise对象,直接取它的结果做为promise2的结果 x.then(resolve, reject) } resolve(x) // 否则,以它的返回值做为promise2的结果 } catch (e) { reject(e) // 如果出错,以捕获到的错误做为promise2的结果 } }) } if (self.status === 'rejected') { return promise2 = new Promise(function(resolve, reject) { try { //同上 var x = onRejected(self.data) if (x instanceof Promise) { x.then(resolve, reject) } } catch (e) { reject(e) } }) } //如果当前promise还未执行完毕,则需要把注册的回调函数添加到Promise中对应的队列里 //并像上面一样创建一个promise,以支持链式调用 if (self.status === 'pending') { return promise2 = new Promise(function(resolve, reject) { self.onResolvedCallback.push(function(value) { try { var x = onResolved(self.data) if (x instanceof Promise) { x.then(resolve, reject) } } catch (e) { reject(e) } }) self.onRejectedCallback.push(function(reason) { try { var x = onRejected(self.data) if (x instanceof Promise) { x.then(resolve, reject) } } catch (e) { reject(e) } }) }) } } Promise.prototype.catch = function(onRejected) { return this.then(null, onRejected) }
本文探讨了Promise的基本用法及一个简单版本的promise的实现,promise的实现代码参考了xieranmaya关于promise3的实现,如果感兴趣,请点击查看github链接。
es的异步解决方案不止promise这一个,其他的还有Generator函数、async函数等。从某种程度上来说,它们可能比promise更加优雅,只是由于浏览器的支持性问题还未得到大量普及。希望大家尽快学习和使用。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。