好久没有更新文章了,最近刚好遇到考试,而且一直在做数据库课设。
本来这篇文章是上个星期想要分享给工作室的师弟师妹们的,结果因为考试就落下了。
其实我并不是很想写Promise
,毕竟现在更好的方式是结合await/async
和Promise
编写异步代码。但是,其实觉得Promise
这个东西对于入门ES6,改善“回调地狱”有很大的帮助,那也算是回过头来复习一下吧。
本文很多地方参考了阮一峰的这一本书,因为学ES6,这本书是最好的,没有之一。当然,整理的文章也有我自己的思路在,还有加上了自己的一些理解,适合入门ES6的小伙伴学习。
如果已经对Promise
有一定的了解,但并没有实际的用过,那么可以看一下在实例中使用和如何更加优雅的使用Promise
一节。
另外,本文中有三个例子涉及“事件循环和任务队列”(均已在代码头部标出),如果暂时不能理解,可以先学完Promise
之后去了解最后一节的知识,然后再回来看,这样小伙伴你应该就豁然开朗了。
引言
回调函数
所谓回调,就是“回来调用”,这里拿知乎上“常溪玲”一个很形象的例子: “ 你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。”
至于回调函数的官方定义是什么,这里就不展开了,毕竟和我们本篇文章关系不大。有兴趣的小伙伴可以去搜一下。
不友好的“回调地狱”
写过node
代码的小伙伴一定会遇到这样的一个调用方式,比如下面mysql
数据库的查询语句:
connection.query(sql1, (err, result) => { //ES6箭头函数 //第一次查询 if(err) { console.err(err); } else { connection.query(sql2, (err, result) => { //第二次查询 if(err) { console.err(err); } else { ... } }; }})
上面的代码大概的意思是,使用mysql
数据库进行查询数据,当执行完sql1
语句之后,再执行sql2
语句。
可见,上面执行sql1
语句和sql2
语句有一个先后的过程。为了实现先去执行sql1
语句再执行sql2
语句,我们只能这样简单粗暴的去嵌套调用。
如果只有两三步操作还好,那么假如是十步操作或者更多,那代码的结构是不是更加的复杂了而且还难以阅读。
所以,Promise
就为了解决这个问题,而出现了。
promise用法
这一部分的内容绝大部分摘抄自《ES6标准入门》一书,如果你已经读过相关Promise
的使用方法,那么你大可以快速浏览或直接跳过。
同时,你更需要留意一下catch
部分和涉及“事件循环”的三个例子。
promise是什么?
promise的定义
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理,让开发者不用再关注于时序和底层的结果。Promise的状态具有不受外界影响和不可逆两个特点,与译后的“承诺”这个词有着相似的特点。
Promise的三个状态
首先,Promise
对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)、rejected
(已失败)。
只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都没有办法改变这个状态。
状态不可逆
其次,状态是不可逆的。也就是说,一旦状态改变,就不会再变成其他的了,往后无论何时,都可以得到这个结果。
对于Promise
的状态的改变,只有两种情况:一是pending
变成fulfilled
,一是pending
变成rejected
。(注:下文用resolved
指代fulfilled
)
只要这两种情况中的一种发生了,那么状态就被固定下来了,不会再发生改变。
同时,如果改变已经发生了,此时再对Promise
对象指定回调函数,那么会立即执行添加的回调函数,返回Promise
的状态。这与事件完全不同。事件的状态是瞬时性的,一旦错过,它的状态将不会被保存。此时再去监听,肯定是得不到结果的。
Promise怎么用?
promise的基本用法
ES6规定,Promise对象是一个构造函数,用来生成Promise实例。
实例对象
这里,我们先来new
一个全新的Promise
实例。
const promise = new Promise(function(resolve, reject) { // ... some code if(/* 异步操作成功*/) { resolve(value); } else { reject(error); }});
可以看到,Promise
构造函数接受一个匿名函数作为参数,在函数中,又分别接受resolve
和reject
两个参数。这两个参数代表着内置的两个函数。
resovle
的作用是,将Promise
对象的状态从“未完成(pending
)”变为“成功(resolved
)”,通常在异步操作成功时调用,并将异步操作的结果,做为它的参数传递出去。
reject
的作用是,将Promise
对象的状态从“未完成(pending
)”变成"失败(rejected
)",通常在异步操作失败时调用,并将异步操作的结果,作为参数传递出去。
接收状态的回调
在Promise
实例生成以后,可以使用then
方法指定resolved
状态和rejected
状态。
//接上“实例对象”的代码promise.then(function(value) { //success},function(error) { //failure});
可见,then
方法可以接受两个回调函数作为参数。第一个回调函数是Promise
对象的状态变为resolved
时调用,第二个回调函数是promise
对象的状态变为rejected
时调用。其中,第二个函数是可选的。并不一定要提供。另外,这两个函数都接受Promise
对象传出的值作为参数。
下面给出了一个简单的例子:
function timeout(ms) { return new Promise((resolve, reject) { setTimeout(resolve, ms, 'done'); });}timeout(100).then(function(value) { console.log(value); //done});
上面的例子,是在100ms之后,把新建的Promise
对象由pending
状态变为resolved
状态,接着触发then
方法绑定的回调函数。
另外,Promise
在新建的时候就会立即执行,因此我们也可以直接改变Promise
的状态。
//涉及“事件循环”例子1let promise = new Promise(function(resolve, reject) { console.log('Promise'); resolve();});promise.then(function() { console.log('resolved.');});console.log('Hi!');// Promise// Hi!// resolved
上面的代码中,新建了一个Promise
实例后立即执行,所以首先输出的是"Promise"
,仅接着resolve
之后,触发then
的回调函数,它将在当前脚本所有同步任务执行完了之后才会执行,所以接下来输出的是"Hi!"
,最后才是"resolved"
。(注:这里涉及到JS的任务执行过程和事件循环,如果还不是很了解这个流程可以全部看完后再回过来理解一下这段代码。)
关于Promise
的基本用法,就先讲解到这里。
接下来我们来看一下Promise
封装的原生方法。
Promise实例上的then
和catch
Promise.prototype.then
Promise
的原型上有then
方法,前面已经提及和体验过,它的作用是为Promise
实例添加状态改变时的回调函数。 then
的方法的第一个参数是resolved
状态的回调函数,第二个参数(可选)是rejected
状态的回调函数。
then
方法返回的是一个新的Promise
实例,因此可以采用链式写法,也就是说在then
后面可以再调用另一个then
方法。
const promise = new Promise(function(resolve, reject) { // ... some code if(/* 异步操作成功*/) { resolve(obj); } else { reject(error); }});promise.then(function(obj) { return obj.a;}).then(function(a) { //...});
上面的代码使用then
方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。
也就是说,在Promise
中传参有两种方式:
一是实例
Promise
的时候把参数通过resovle()
传递出去。二是在
then
方法中通过return
返回给后面的then
。
采用链式的then
,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise
对象(即有异步操作),这时后一个回调函数,就会等待该Promise
对象的状态发生变化,才会被调用。
const promise1 = new Promise(function(resolve, reject) { // ... some code if(/* 异步操作成功*/) { resolve("promise1"); } else { reject(error); }});const promise2 = new Promise(function(resolve, reject) { // ... some code if(/* 异步操作成功*/) { resolve("promise2"); } else { reject(error); }});promise1.then(function() { return promise2;}).then(function funcA(result) { console.log(result); //"promise2"}, function funcB(err){ console.log("rejected: ", err);});
上面代码中,第一个then
方法指定的回调函数,返回的是另一个Promise
对象。这时,第二个then
方法指定的回调函数,就会等待这个新的Promise
对象状态发生变化。如果变为resolved
,就调用funcA
,如果状态变为rejected
,就调用funcB
。
Promise.prototype.catch
Promise.prototype.catch
方法是.then(null, rejection)
的别名,用于指定发生错误时的回调函数。
const promise = new Promise(function(resolve, reject) { // ... some code if(/* 异步操作成功*/) { resolve(value); } else { reject(error); }});promise.then(function(value) { //success},function(error) { //failure});
于是,这段代码等价为:
const promise = new Promise(function(resolve, reject) { // ... some code if(/* 异步操作成功*/) { resolve(value); } else { reject(error); }})promise.then(function() { //success}).catch(function(err) { //failure})
可见,此时“位置1”中的then
里面的两个参数被剥离开来,如果异步操作抛出错误,就会调用catch
方法指定的回调函数,处理这个错误。
值得一提的是,现在我们在给rejected
状态绑定回调的时候,更倾向于catch
的写法,而不使用then
方法的第二个参数。这种写法,不仅让Promise
看起来更加简洁,更加符合语义逻辑,接近try/catch
的写法。更重要的是,Promise
对象的错误具有向后传递的性质(书中说“冒泡”我觉得不是很合适,可能会误解),直到错误被捕获为止。
const promise1 = new Promise(function(resolve, reject) { // ... some code if(/* 异步操作成功*/) { resolve("promise1"); } else { reject(error); }});const promise2 = new Promise(function(resolve, reject) { // ... some code if(/* 异步操作成功*/) { resolve("promise2"); } else { reject(error); }});promise1.then(function() { return promise2;}).then(function funcA(result) { console.log(result); //"promise2"}).catch(function(err) { console.log(err); //处理错误})
上面的代码中一共有三个Promise
,第一个由promise1
产生,另外两个由不同的两个then
产生。无论是其中的任何一个抛出错误,都会被最后一个catch
捕获。
如果还是对Promise
错误向后传递的性质不清楚,那么可以按照下面的代码做一下实验,便可以更加清晰的认知这个特性。
const promise1 = new Promise(function(resolve, reject) { //1. 在这里throw("promise1错误"),catch捕获成功 // ... some code if(true) { resolve("promise1"); } else { reject(error); }});const promise2 = new Promise(function(resolve, reject) { // ... some code //2. 在这里throw("promise2错误"),catch捕获成功 if(true) { resolve("promise2"); } else { reject(error); }});promise1.then(function() { return promise2;}).then(function funcA(result) { console.log(result); //"promise2" //3. 在这里throw("promise3错误"),catch捕获成功}).catch(function(err) { console.log(err); //处理错误})
以上,分别将1、2、3的位置进行解注释,就能够证明我们以上的结论。
关于catch
方法,还有三点需要提及的地方。
-
Promise
中的错误传递是向后传递,并非是嵌套传递,也就是说,嵌套的Promise
,外层的catch
语句是捕获不到错误的。const promise1 = new Promise(function(resolve, reject) { // ... some code if(true) { resolve("promise1"); } else { reject(error); }});const promise2 = new Promise(function(resolve, reject) { // ... some code if(true) { resolve("promise2"); } else { reject(error); }});promise1.then(function() { promise2.then(function() { throw("promise2出错"); })}).catch(function(err) { console.log(err);});//> Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: undefined}//Uncaught (in promise) promise2出错
所以,代码出现了未捕获的错误,这就是为什么我强调说是“向后传递错误而不是冒泡传递错误”。
-
在
Promise
没有使用catch
而抛出未处理的错误。const someAsyncThing = function() { return new Promise(function(resolve, reject) { // 下面一行会报错,因为x没有声明 resolve(x + 2); });};someAsyncThing().then(function() { console.log('everything is great');});setTimeout(() => { console.log(123) }, 2000);// Uncaught (in promise) ReferenceError: x is not defined// 123
上面代码中,
解决的方法就是在someAsyncThing
函数产生的Promise
对象,内部有语法错误。浏览器运行到这一行,会打印出错误提示ReferenceError: x is not defined
,但是不会退出进程、终止脚本执行,2秒之后还是会输出123
。这就是说,Promise
内部的错误不会影响到Promise
外部的代码,通俗的说法就是“Promise
会吃掉错误”。then
后面接一个catch
方法。 -
涉及到
Promise
中的异步任务抛出错误的时候。//涉及“事件循环”例子2const promise = new Promise(function (resolve, reject){ resolve('ok'); setTimeout(function () { throw new Error('test') }, 0);});promise.then(function (value) { console.log(value);}).catch(function(err) { console.log(err);});// ok// Uncaught Error: test
可以看到,这里的错误并不会
原因有二:catch
捕获,结果就成了一个未捕获的错误。其一,由于在
setTimeout
之前已经resolve
过了,由于这个时候的Promise
状态就变成了resolved
,所以它走的应该是then
而不是catch
,就算后面再抛出错误,由于其状态不可逆的原因,依旧不会抛出错误。也就是下面这种情况:const promise = new Promise(function (resolve, reject) { resolve('ok'); throw new Error('test'); //依然不会抛出错误 });//...省略
其二,
解决的方法就是直接在setTimeout
是一个异步任务,它是在下一个“事件循环”才执行的。当到了下一个事件循环,此时Promise
早已经执行完毕了,此时这个错误并不是在Promise
内部抛出了,而是在全局作用域中,于是成了未捕获的错误。(注:这里涉及到JS的任务执行过程和事件循环,如果还不是很了解这个流程可以全部看完后再回过来理解一下这段代码。)setTimeout
的回调函数中去try/catch
。
更多的方法
Promise.resolve
这个方法可以把现有的对象转换成一个Promise
对象,如下:
const jsPromise = Promise.resolve($.ajax('/whatever.json'));
上面代码把jQuery中生成的deferred
对象转换成了一个新的Promise
对象。
Promise
的参数大致分下面四种:
如果参数是
Promise
实例,那么Promise.resolve
将不做任何修改、原封不动地返回这个实例。-
参数是一个
thenable
对象。thenable
对象指的是具有then
方法的对象,比如下面这个对象。let thenable = { then: function(resolve, reject) { resolve(42); }};
Promise.resolve
方法会将这个对象转为Promise
对象,然后就立即执行thenable
对象的then
方法,如下:let thenable = { then: function(resolve, reject) { resolve(42); }};let p1 = Promise.resolve(thenable);p1.then(function(value) { console.log(value); // 42});
-
参数不是具有then方法的对象,或根本就不是对象。
如果参数是一个原始值,或者是一个不具有
then
方法的对象,则Promise.resolve
方法返回一个新的Promise
对象,状态为resolved
。 -
不带有任何参数。
Promise.resolve
方法允许调用时不带参数,直接返回一个resolved
状态的Promise
对象。//涉及“事件循环”例子3setTimeout(function () { console.log('three');}, 0);Promise.resolve().then(function () { console.log('two');});console.log('one');// one// two// three
上面这个例子,由于
Promise
算是一个微任务,当第一次事件循环执行完了之后(console.log('one')
),会取出任务队列中的所有微任务执行完(Promise.resovle().then
),再进行下一次事件循环,也就是之后再执行setTimeout
。所以输出的顺序就是one
、two
、three
。(注:这里涉及到JS的任务执行过程和事件循环,如果还不是很了解这个流程可以全部看完后再回过来理解一下这段代码。)
Promise.reject
Promise.reject(reason)
方法也会返回一个新的Promise
实例,该实例的状态为rejected
,并立即执行其回调函数。
注意,Promise.reject()
方法的参数,会原封不动地作为reject
的理由,变成后续方法的参数。这一点与Promise.resolve
方法不一致。
const thenable = { then(resolve, reject) { reject('出错了'); }};Promise.reject(thenable) .catch(e => { console.log(e === thenable) });// true
上面代码中,Promise.reject
方法的参数是一个thenable
对象,执行以后,后面catch
方法的参数不是reject
抛出的“出错了”这个字符串,而是thenable
对象。
其他
下面的方法只做简单的介绍,如果需要更详细的了解它,请到处查询相关资料。
Promise.all
Promise.all
方法用于将多个Promise
实例,包装成一个新的Promise
实例。
const p = Promise.all([p1, p2, p3]);
上面代码中,Promise.all
方法接受一个数组作为参数,p1
、p2
、p3
都是Promise
实例,如果不是,就会先调用上面讲到的Promise.resolve
方法,将参数转为Promise
实例,再进一步处理。
p
的状态由p1
、p2
、p3
决定,分成两种情况。
(1)只有p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数。
(2)只要p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p
的回调函数。
Promise.race
const p = Promise.all([p1, p2, p3]);
上面代码中,Promise.race
方法接受一个数组作为参数,p1
、p2
、p3
都是Promise
实例,如果不是,就会先调用上面讲到的Promise.resolve
方法,将参数转为Promise
实例,再进一步处理。
与Promise.all
不同,只要其中有一个实例率先改变状态,p
的状态就跟着改变。那么率先改变的Promise
实例的返回值,就传递给p
的回调函数。
done
Promise
对象的回调链,不管以then
方法或catch
方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到。因此,我们可以提供一个done
方法,总是处于回调链的尾端,保证抛出任何可能出现的错误。
它的实现代码相当简单。
Promise.prototype.done = function (onFulfilled, onRejected) { this.then(onFulfilled, onRejected) .catch(function (reason) { // 抛出一个全局错误 setTimeout( function() { throw reason }, 0); });};
从上面代码可见,done
方法的使用,可以像then
方法那样用,提供fulfilled
和rejected
状态的回调函数,也可以不提供任何参数。但不管怎样,done
都会捕捉到任何可能出现的错误,并向全局抛出。
finally
finally
方法用于指定不管Promise
对象最后状态如何,都会执行的操作。它与done
方法的最大区别,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。
下面是一个例子,服务器使用Promise
处理请求,然后使用finally
方法关掉服务器。
server.listen(0) .then(function () { // run test }); .finally(server.stop);
它的实现也非常的简单。
Promise.prototype.finally = function (callback) { let P = this.constructor; return this.then( function(value) { P.resolve(callback()).then(function() { return value; }); }, function(reason) { reason => P.resolve(callback()).then(function() { throw reason; }); });};
JQuery的Deferred对象
最初,在低版本的JQuery中,对于回调函数,它的功能是非常弱的。无限“嵌套”回调,编程起来十分不友好。为了改变这个问题,JQuery团队就设计了deferred
对象。
它把回调的嵌套调用改写成了链式调用,具体的写法也十分的简单。这里也不详细讲,想了解的小伙伴也可以直接到这个链接去看。
外部修改状态
但是,由于deferred
对象它的状态可以在外部被修改到,这样会导致混乱的出现,于是就有了deferred.promise
。
它是在原来的deferred
对象上返回另外一个deferred
对象,后者只开放与改变执行状态无关的方法,屏蔽与改变执行状态有关的方法。从而来避免上述提到的外部修改状态的情况。
如果有任何疑问,可以回到一看便知。
值得一提的是,JQuery中的Promise
与我们文章讲的Promise
并没有关系,只是名字一样罢了。
虽然两者遵循的规范不相同,但是都致力于一件事情,那就是:基于回调函数更好的编程方式。
promise编程结构
返回新Promise
既然我们学了Promise
,那么就应该在日常开发中去使用它。
然而,对于初学者来说,在使用Promise
的时候,可能会出现嵌套问题。
比如说下面的代码:
var p1 = new Promise(function() { if(...) { reject(...); } else { resolve(...); }});var p2 = new Promise(function() { if(...) { reject(...); } else { resolve(...); }});var p3 = new Promise(function() { if(...) { reject(...); } else { resolve(...); }});p1.then(function(p1_data) { p2.then(function(p2_data) { // do something with p1_data p3.then(fuction(p3_data) { // do something with p2_data // p4... }); });});
假如说现在需要p1
、p2
、p3
按照顺序执行,那么刚入门的小伙伴可能会这样写。
其实也没有错,这里是用了Promise
,但是用得并不彻底,依然存在“回调”地狱,没有深入到Promise
的核心部分。
那么我们应该怎么样更好的去运用它呢?
回顾一下前面Promise
部分,你应该可以得到答案。
下面,看我们修正后的代码。
//同上,省略定义。p1.then(function(p1_data) { return p2; //位置1}).then(function(p2_data){ //位置2 return p3;}).then(function(p3_data){ return p4;}).then(function(p4_data){ //final result}).catch(function(error){ //同一处理错误信息});
可以看到,每次执行完了then
方法之后,我们都return
了一个新的Promise
。那么当新的Promise
中resolve
之后,那么显而易见的,它会执行跟在它后面的then
之中。
也就是说,在p1
的then
方法执行完了之后,现在我们要去执行p2
,那么这个时候我们在“位置1”给它return
了一个新的Promise
,所以此时的代码可以等价为:
p2.then(function(p2_data){ //位置2 return p3;}).then(function(p3_data){ return p4;}).then(function(p4_data){ //final result}).catch(function(error){ //同一处理错误信息});
可见,p2
中resolve
之后,就可以被“位置2”的then
接收到了。
于是,基于这个结构,我们就可以在开发中去封装出一个Promise
供我们来使用。
在实例中使用
刚好最近在做一个mysql
的数据库课设,这里就把我如何封装promise
给贴出来。
下面的例子,可能有些接口刚接触node
的小伙伴会看不懂,那么,我会尽量的做到无死角注释,大家也尽量关注一下封装的过程(注:重点关注标“*”的地方)。
首先是mysql.js
封装文件。
var mysql = require("mysql");//引入mysql库//创建一个连接池,同一个连接池可以同时存在多个连接,连接完成需要释放var pool = mysql.createPool({ ...//省略连接的配置});/** * 把mySQL查询功能封装成一个promise * @param String sql * @returns Promise**/ var QUERY = (sql) => { //注意这里new了一个新的promise(*) var connect = new Promise((resolve, reject) => { //创建连接 pool.getConnection((err, connection) => { //下面是状态执行(*) if (err) { reject(err);//如果创建连接失败的话直接reject(*) } else { //否则可以进行查询了 connection.query(sql, (err, results) => { //执行完查询释放连接 connection.release(); //在查询的时候如果出错直接reject if (err) { reject(err);//(*) } else { //否则成功,把查询的结果resolve出去 //然后给后面then去使用 resolve(results);//(*) } }); } }); }); //最后把promise给return出去(*) return connect; };module.exports = QUERY; //把封装好的库导出
接下来,去使用我们封装好的查询Promise
。
假如我们现在想要使用查询功能获取某个数据表的所有数据:
var QUERY = require("mysql"); //把我们写的库给导入var sql = `SELECT * FROM student`;//sql语句,看不懂直接忽略//执行查询操作QUERY(sql).then((results) => { //(*) //这里就可以使用查询到的results了}).catch((err) => { //使用catch可以捕获到整条链抛出的错误。(*) console.log(err);})
以上,就是一个实例了。所以以后,如果你想要封装一个Promise
来使用,你可以这样来写。
如何更优雅的使用Promise?
那么,现在问题又来了,如果我们现在需要进行很多异步操作(比如Ajax通信),那么如果按照上面的写法,会导致then
链条过长。于是,需要我们不停的去return
一个新的Promise
对象供后面使用。如下:
function getURL(URL) { return new Promise(function (resolve, reject) { var req = new XMLHttpRequest(); req.open('GET', URL, true); req.onload = function () { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function () { reject(new Error(req.statusText)); }; req.send(); });}var request = { comment: function getComment() { return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse); }, people: function getPeople() { return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse); } };function main() { function recordValue(results, value) { results.push(value); return results; } // [] 用来保存初始化的值 相当于声明results = [] var pushValue = recordValue.bind(null, []); return request.comment() //位置1 .then(pushValue) .then(request.people) .then(pushValue); }// 运行示例main().then(function (value) { console.log(value);}).catch(function(error){ console.error(error);});
可以看到,在“位置1”处的代码,return request.comment().then(pushValue).then(request.people).then(pushValue);
使用了三个then
和new
了两个新的Promise
。
因此,如果我们将处理内容统一放到数组里,再配合for
循环进行处理的话,那么处理内容的增加将不会再带来什么问题。首先我们就使用for
循环来完成和前面同样的处理。
function getURL(URL) { return new Promise(function (resolve, reject) { var req = new XMLHttpRequest(); req.open('GET', URL, true); req.onload = function () { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function () { reject(new Error(req.statusText)); }; req.send(); });}var request = { comment: function getComment() { return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse); }, people: function getPeople() { return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse); } };
前面这一部分是不需要改变的。
function main() { function recordValue(results, value) { results.push(value); return results; } // [] 用来保存初始化值 var pushValue = recordValue.bind(null, []); // 返回promise对象的函数的数组 var tasks = [request.comment, request.people]; var promise = Promise.resolve(); // 开始的地方 for (var i = 0; i < tasks.length; i++) { var task = tasks[i]; promise = promise.then(task).then(pushValue); } return promise;}// 运行示例main().then(function (value) { console.log(value);}).catch(function(error){ console.error(error);});
使用for
循环的时候,每次调用then
都会返回一个新创建的Promise
对象 因此类似promise = promise.then(task).then(pushValue);
的代码就是通过不断对promise
进行处理,不断的覆盖 promise
变量的值,以达到对Promise
对象的累积处理效果。 但是这种方法需要promise
这个临时变量,从代码质量上来说显得不那么简洁。 如果将这种循环写法改用Array.prototype.reduce
的话,那么代码就会变得聪明多了。
于是我们再对main
函数进行修改:
function main() { function recordValue(results, value) { results.push(value); return results; } var pushValue = recordValue.bind(null, []); var tasks = [request.comment, request.people]; return tasks.reduce(function (promise, task) { return promise.then(task).then(pushValue); }, Promise.resolve());}
(注:Array.prototype.reduce
第一个参数执行数组每个值的回调函数,第二个参数是初始值。回调函数中,第一个参数是上一次调用回调返回的值或提供的初始值,第二个是数组中正在处理的元素。)
最后,重写完了整个函数就是:
function sequenceTasks(tasks) { function recordValue(results, value) { results.push(value); return results; } var pushValue = recordValue.bind(null, []); return tasks.reduce(function (promise, task) { return promise.then(task).then(pushValue); }, Promise.resolve());}function getURL(URL) { return new Promise(function (resolve, reject) { var req = new XMLHttpRequest(); req.open('GET', URL, true); req.onload = function () { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function () { reject(new Error(req.statusText)); }; req.send(); });}var request = { comment: function getComment() { return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse); }, people: function getPeople() { return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse); } };function main() { return sequenceTasks([request.comment, request.people]);}// 运行示例main().then(function (value) { console.log(value);}).catch(function(error){ console.error(error);});
需要注意的是,在sequenceTasks
中传入的应该是返回Promise
对象的函数的数组,而不是一个Promise
对象,因为一旦返回一个对象的时候,异步任务其实已经是开始执行了。
综上,在写顺序队列的时候,核心思想就是不断的去return
新的Promise
并进行状态判断 。而至于怎么写,要根据实际情况进行编程。
是回调不好还是嵌套不好?
本质上来说,回调本身没有什么不好的,但是因为回调的存在,使得我们无限的嵌套函数构成了“回调地狱”,这对开发者来说无疑是特别不友好的。而虽然Promise
只是回调的语法糖,但是却提供给我们更好的书写方式,解决了回调地狱嵌套的难题。
更多
最后,这里算是一个拓展和学习方向,学习起来有一定的难度。
为什么JavaScript使用异步的方式来处理任务?
由于JavaScript
是一种单线程的语言,所谓的单线程就是按照我们书写的代码一样一行一行的执行下来,于是每次只能做一件事。
如果我们不是用异步的方式而用同步的方式去处理任务,假如现在我们有一个网络请求,请求后面是与其无关的一些操作代码。那么当请求发送出去的时候,由于现在执行代码是按部就班的,于是我们就必须等待网络请求的应答之后,我们才能继续往下执行我们的代码。而这个等待,不仅花费了我们很多时间。同时,也阻塞了我们后面的代码。造成了不必要的资源浪费。
于是,当使用异步的方式来处理任务的时候,每次发送请求,JavaScript
中的执行栈会把异步操作交给浏览器的webCore
内核来处理,然后继续往下执行代码。当主线程的执行栈代码执行完毕之后,就会去检查任务队列中有没有任务需要执行的。
如果有,则取出来到主线程的执行栈中执行,执行完毕后,更新dom
,然后再进行一次同样的循环。
而任务队列中任务的添加,则是靠浏览器内核webCore
。每次异步操作完成之后,webCore
内核就会把相应的回调函数添加到任务队列中。
值得注意的是,任务队列中任务按照任务性质划分为宏任务和微任务。而由于任务类型的不同,可能存在多个类型的任务队列。但是事件循环只能有一个。
所以现在我们把宏任务和微任务考虑进去,第一次执行完脚本的代码(算是一次宏任务),那么就会到任务队列的微任务队列中取出其所有任务放到主线程的执行栈中来执行,执行完毕后,更新dom
。下一次事件循环,再从任务队列中取出一个宏任务,然后执行微任务队列中的所有微任务。再循环...
注:第一次执行代码的时候,就已经开始了第一次的事件循环,此时的script
同步代码是一个宏任务。
整个过程,也就是下面的这一个图:
常见的异步任务有:网络请求、IO操作、计时器和事件绑定等。
以上,如果你能够看懂我在讲什么,那么说明你真正理解了JS中的异步,如果不懂,那么你需要去了解一下“事件循环、任务队列、宏任务与微任务”,下面是两篇不错的博客,值得学习。
事件循环:
对JS异步任务执行的一个总结: