Javascript语言的执行环境是“单线程”——一次只能完成一件任务,若有多个任务则必须排队,前面的任务完成,再执行后面的一个任务。
一、同步和异步
这种模式实现简单,执行环境也相对单纯,但如果某个任务耗时很长,后面的任务必须排队等候,会拖累整个程序运行。
为解决这个问题,javascript语言将任务的执行模式分为两种:同步、异步。
1、同步
同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务,打开网站的渲染过程,其实就是一个同步任务。
读取文件的同步任务执行如下:
2、异步
可以把异步理解为一个任务分成两段。先执行第一段,再转而执行其他任务,等准备好后,再回头来执行第二段。
因此排在异步任务后面的代码,不用等待异步任务结束会马上运行,即:异步任务不具有“堵塞”效应。打开网站时,图片加载、音乐加载都是一个异步任务。
读取文件的异步任务执行如下:
3、异步模式意义
在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应(例如Ajax操作)。
在服务器端,“异步模式”几乎是唯一的模式,如果执行环境是单线程的,若允许同步执行所有http请求,服务器性能将急剧下降,很快失去响应。
二、传统异步方案
1、回调函数(Callback)
回调函数是异步的最基本实现方式。
思路:将回调函数作为参数传入主函数,执行完主函数内容后,执行回调函数。
(1)回调示例
假定有两个函数f1()和f2(),f2需要等待f1的执行结果。
如果f1是一个很耗时的任务,则可以改写f1,将f2作为f1的回调函数。
function f1(callback){ setTimeout(function () { // f1的任务代码 callback(); }, 1000); }
执行代码就变为了f1(f2);
采用这种方法,就将同步操作变为了异步操作,f1不会堵塞程序运行,相当于先执行主要逻辑,将耗时的操作推迟执行。
(2)回调的优缺点
优点:简单、容易理解和部署。
缺点:1)代码耦合度太高,不利于代码的阅读和维护;
2)有多层回调的情况下,容易引起回调地狱;
3)每个任务只能指定一个回调函数,例如fs.readFile
等函数,只提供传入一个回调函数,如果想触发2个回调函数,就只能再用一个函数把这两个函数包起来
// 例子1:回调地狱,依次执行f1,f2,f3... const f1 = (callback) => setTimeout(()=>{ console.log('f1') callback() },1000) const f2 = (callback) =>setTimeout(()=>{ console.log('f2') callback() },1000) ... // 假设还有f3,f4...fn都是类似的函数,那么就要不断的把每个函数写成类似的形式,然后使用下面的形式调用: f1(f2(f3(f4))) // 例子2:如果想给`fs.readFile`执行2个回调函数callback1,callback2 // 必须先包起来 const callback3 = ()=>{ callback1 callback2 } fs.readFile(filename,[encoding],callback3)
2、事件监听(Listener)
事件监听的含义:采用事件驱动模式,让任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
(1)监听示例
仍假定有两个函数f1()和f2(),f2需要等待f1的执行结果。首先为f1绑定一个事件(这里为jQuery写法):
f1.on('done', f2);
上面代码含义:当f1发生done事件,就执行f2。对f1改写如下:
const f1 = () => setTimeout(()=>{ console.log('f1') // 函数体 f1.trigger('done') // 执行完函数体部分 触发done事件 },1000) f1.on('done',f2) // 绑定done事件回调函数 f1() // 一秒后输出 f1,再过一秒后输出f2
f1.trigger('done')表示:执行完成后,立即触发done事件,从而开始执行f2。
(2)监听原理
手动实现上面的例子,体会下面方案的原理:
const f1 = () => setTimeout(()=>{ console.log('f1') // 函数体 f1.trigger('done') // 执行完函数体部分 触发done事件 },1000) /*----------------核心代码start--------------------------------*/ // listeners 用于存储f1函数各种各样的事件类型和对应的处理函数 f1.listeners = {} // on方法用于绑定监听函数,type表示监听的事件类型,callback表示对应的处理函数 f1.on = function (type,callback){ if(!this.listeners[type]){ this.listeners[type] = [] } this.listeners[type].push(callback) //用数组存放 因为一个事件可能绑定多个监听函数 } // trigger方法用于触发监听函数 type表示监听的事件类型 f1.trigger = function (type){ if(this.listeners&&this.listeners[type]){ // 依次执行绑定的函数 for(let i = 0;i < this.listeners[type].length;i++){ const fn = this.listeners[type][i] fn() } } } /*----------------核心代码end--------------------------------*/ const f2 = () =>setTimeout(()=>{ console.log('f2') },1000) const f3 = () =>{ console.log('f3') } f1.on('done',f2) // 绑定done事件回调函数 f1.on('done',f3) // 多个回调 f1() // 一秒后输出 f1, f3,再一秒后输出f2
核心原理:
- 用listeners对象储存要监听的事件类型和对应的函数;
- 调用on方法时,往listeners中对应的事件类型添加回调函数;
- 调用trigger方法时,检查listeners中对应的事件,如果存在回调函数,则依次执行。
和回调相比,代码的区别只是把原先执行callback的地方,换成执行对应监听事件的回调函数,但从模式上看,转换为事件驱动模型。
(3)监听优缺点
- 优点:避免了直接使用回调的高耦合问题,可以绑定多个回调函数
- 缺点:整个程序变为事件驱动型,不容易看出执行的主流程
3、发布/订阅模式(Publish/Subscribe)
上面的事件,完全可以理解为“信号”。
若假定一个“信号中心”,某个任务执行完,就向信号中心“发布”(publish)一个信号,其他任务则可以向信号中心“订阅”(subscribe)这个信号,从而自己执行任务。
这就是“发布/订阅模式”(publish-subscribe pattern),又称为“观察者模式”(observer pattern)。
(1)发布/订阅示例
采用jQuery的插件——Ben Alman的Tiny Pub/Sub来实现这种模式。
首先,f2向“信号中心”jQuery订阅“done”信号:
jQuery.subscribe("done", f2);
然后,改写f1:
function f1(){ setTimeout(function () { // f1的任务代码 jQuery.publish("done"); }, 1000); }
jQuery.publish("done")的意思是,f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引发f2的执行。
f2完成执行后,也可以取消订阅(unsubscribe):
jQuery.unsubscribe("done", f2);
(2)发布/订阅原理
将事件监听中f1的监听函数和触发事件功能,赋给一个新建的全局对象,就转为了发布订阅模式:
// 消息中心对象 const Message = { listeners:{} } // subscribe方法用于添加订阅者 类似事件监听中的on方法 里面的代码完全一致 Message.subscribe = function (type,callback){ if(!this.listeners[type]){ this.listeners[type] = [] } this.listeners[type].push(callback) //用数组存放 因为一个事件可能绑定多个监听函数 } // publish方法用于通知消息中心发布特定的消息 类似事件监听中的trigger 里面的代码完全一致 Message.publish = function (type){ if(this.listeners&&this.listeners[type]){ // 依次执行绑定的函数 for(let i = 0;i < this.listeners[type].length;i++){ const fn = this.listeners[type][i] fn() } } } const f2 = () =>setTimeout(()=>{ console.log('f2') },1000) const f3 = () => console.log('f3') Message.subscribe('done',f2) // f2函数 订阅了done信号 Message.subscribe('done',f3) // f3函数 订阅了done信号 const f1 = () => setTimeout(()=>{ console.log('f1') Message.publish('done') // 消息中心发出done信号 },1000) f1() // 执行结果和上面完全一样
与监听例子的区别:
- 创建了一个Message全局对象,并且listeners移到该对象
on
方法改名为subscribe
方法,并且移到Message对象上trigger
方法改名为publish
,并且移到Message对象上
- 在事件监听模式中,消息传递路线:被监听函数f1与监听函数f2直接交流
- 在发布/订阅模式中,是发布者f1和消息中心交流,订阅者f2也和消息中心交流
(3)优缺点
这种方法的性质与"事件监听"类似,但是明显优于后者。
可以通过查看"消息中心",了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
三、ES6异步方案——Promise
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。ES6 将其写进了语言标准,统一了用法,原生提供了Promise
对象。
Promise
对象是一个代理对象(代理一个值),被代理的值在 Promise 对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的 promise 对象。
1、Promise的三种状态
- pending:初始状态,既不是成功,也不是失败状态;
- fulfilled:意味着操作成功完成;
- rejected:意味着操作失败。
(1)状态变化
Promise 对象只有两种状态变化可能:pending变为fulfilled状态、pending变为rejected状态。
当任一种情况出现时,状态即凝固不再改变,而且Promise 对象的 then
方法绑定的处理方法(handlers )就会被调用(then方法包含两个参数:onfulfilled 和 onrejected,它们都是 Function 类型。当Promise状态为fulfilled时,调用 then 的 onfulfilled 方法,当Promise状态为rejected时,调用 then 的 onrejected 方法, 所以在异步操作的完成和绑定处理方法之间不存在竞争)。
(2)Promise优缺点
优点:有了Promise
对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise
对象提供统一的接口,使得控制异步操作更加容易。
缺点:
1)无法取消Promise
,一旦新建它就会立即执行,无法中途取消。
2)如果不设置回调函数,Promise
内部抛出的错误,不会反应到外部。
3)当处于pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
2、Promise实例
ES6规定,Promise对象是一个构造函数,用于生成 Promise 实例。基本语法如下:
new Promise( function(resolve, reject) {...} /* executor */ );
(1)参数
定义的Promise有两个参数,resolve和reject。
- resolve:将异步的执行从 pending(请求)变成了resolve(成功返回),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。
- reject:将
Promise
对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
(2)简单示例
需要注意:Promise新建后就会立即执行。
let promise = new Promise(function(resolve, reject) { console.log('Promise'); resolve(); }); promise.then(function() { console.log('resolved.'); }); console.log('Hi!'); // Promise // Hi! // resolved
Promise新建后立即执行,因此首先输出“Promise”。由于执行then方法指定的回调函数,将在当前脚本所有同步任务执行完才执行,因此“resolved”最后输出。
3、Promise的then和catch
(1)Promise.prototype.then(onFulfilled, onRejected)
then方法:定义在原型对象 Promise.prototype 上的。作用——为Promise实例添加状态改变时的回调函数。then方法两个参数:
- resolved状态的回调函数
- rejected状态的回调函数(可选)
const setDelay = (millisecond) => { return new Promise((resolve, reject)=>{ if (typeof millisecond != 'number') reject(new Error('参数必须是number类型')); setTimeout(()=> { resolve(`我延迟了${millisecond}毫秒后输出的`) }, millisecond) }) } setDelay(3000) .then((result)=>{ console.log(result) // 输出“我延迟了2000毫秒后输出的” })
添加解决(fulfillment)和拒绝(rejection)回调到当前 promise, 返回一个新的 promise, 将以回调的返回值来resolve.
(2)Promise.prototype.catch(onRejected)
catch方法:.then(null, rejection) 或 .then(undefined, rejection)的别名,用于指定发生错误时的回调函数。
const setDelay = (millisecond) => { return new Promise((resolve, reject)=>{ if (typeof millisecond != 'number') reject(new Error('参数必须是number类型')); setTimeout(()=> { resolve(`我延迟了${millisecond}毫秒后输出的`) }, millisecond) }) } setDelay('我是字符串') .then((result)=>{ console.log(result) // 不进去了 }) .catch((err)=>{ console.log(err) // 输出错误:“参数必须是number类型” })
添加一个拒绝(rejection) 回调到当前 promise, 返回一个新的promise。当这个回调函数被调用,新 promise 将以它的返回值来resolve,否则如果当前promise 进入fulfilled状态,则以当前promise的完成结果作为新promise的完成结果.
promise抛出错误,由catch方法指定的回调函数获取。三种写法示例:
// 写法一 const promise = new Promise(function(resolve, reject) { throw new Error('test'); }); promise.catch(function(error) { console.log(error); }); // 写法二 const promise = new Promise(function(resolve, reject) { try { throw new Error('test'); } catch(e) { reject(e); } }); promise.catch(function(error) { console.log(error); }); // 写法三 const promise = new Promise(function(resolve, reject) { reject(new Error('test')); }); promise.catch(function(error) { console.log(error); });
比较上面的写法,可以发现reject()
方法的作用,等同于抛出错误。
4、Promise相互依赖
测试一个Promise里的resolve去返回另一个Promise:
const setDelay = (millisecond) => { return new Promise((resolve, reject)=>{ if (typeof millisecond != 'number') reject(new Error('参数必须是number类型')); setTimeout(()=> { resolve(`我延迟了${millisecond}毫秒后输出的`) }, millisecond) }) } const setDelaySecond = (seconds) => { return new Promise((resolve, reject)=>{ if (typeof seconds != 'number' || seconds > 10) reject(new Error('参数必须是number类型,并且小于等于10')); setTimeout(()=> { console.log(`先是setDelaySeconds函数输出,延迟了${seconds}秒,一共需要延迟${seconds+2}秒`) resolve(setDelay(2000)) // 这里依赖上一个Promise }, seconds * 1000) }) } setDelaySecond(3).then((result)=>{ console.log(result) }).catch((err)=>{ console.log(err); }) /* 依次输出: * 先是setDelaySeconds函数输出,延迟了3秒,一共需要延迟5秒 * 我延迟了2000毫秒后输出的 * */
上面虽然做到了依次执行的目的,但耦合性太高,可以使用 Promise 的链式写法改进。
5、Promise链式写法
then
方法返回的是一个新的Promise
实例(注意,不是原来那个Promise
实例)。因此可以采用链式写法,即then
方法后面再调用另一个then
方法。
采用链式的then
,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise
对象(即有异步操作),这时后一个回调函数,就会等待该Promise
对象的状态发生变化,才会被调用。
const setDelay = (millisecond) => { return new Promise((resolve, reject)=>{ if (typeof millisecond != 'number') reject(new Error('参数必须是number类型')); setTimeout(()=> { resolve(`我延迟了${millisecond}毫秒后输出的`) }, millisecond) }) } const setDelaySecond = (seconds) => { return new Promise((resolve, reject)=>{ if (typeof seconds != 'number' || seconds > 10) reject(new Error('参数必须是number类型,并且小于等于10')); setTimeout(()=> { resolve(`我延迟了${seconds}秒后输出的,是第二个函数`) }, seconds * 1000) }) } setDelay(2000) .then((result)=>{ console.log(result) console.log('我进行到第一步的'); return setDelaySecond(3) }) .then((result)=>{ console.log('我进行到第二步的'); console.log(result); }).catch((err)=>{ console.log(err); })
如上所示,第一个then
方法指定的回调函数,返回的是另一个Promise
对象。这时,第二个then
方法指定的回调函数,就会等待这个新的Promise
对象状态发生变化。如果变为resolved
,就调用第一个回调函数,如果状态变为rejected
,就调用第二个回调函数。
先执行setDelay再执行setDelaySecond,只需要在第一个then的结果中返回下一个Promise就可以一直链式写下去,相当于依次执行。
可以看到then的链式写法非常优美,这样就可以脱离异步嵌套苦海。
6、链式写法注意要点
then
链式写法的本质其实是一直往下传递返回一个新的Promise,也就是说then在下一步接收的是上一步返回的Promise。
setDelay(2000) .then((result)=>{ console.log(result) console.log('我进行到第一步的'); return setDelaySecond(20) }) .then((result)=>{ console.log('我进行到第二步的'); console.log(result); }, (_err)=> { console.log('我出错啦,进到这里捕获错误,但是不经过catch了'); }) .then((result)=>{ console.log('我还是继续执行的!!!!') }) .catch((err)=>{ console.log(err); })
改写代码后,输出结果进到了then的第二个参数(reject)中,不再经过catch了。
如果将catch移到then错误处理前:
setDelay(2000) .then((result)=>{ console.log(result) console.log('我进行到第一步的'); return setDelaySecond(20) }) .catch((err)=>{ // 挪上去了 console.log(err); // 这里catch到上一个返回Promise的错误 }) .then((result)=>{ console.log('我进行到第二步的'); console.log(result); }, (_err)=> { console.log('我出错啦,但是由于catch在我前面,所以错误早就被捕获了,我这没有错误了'); }) .then((result)=>{ console.log('我还是继续执行的!!!!') })
此时的情况是,先经过catch捕获,后面不再出现错误。
注意要点:
catch
写法是针对于整个链式写法的错误捕获的,而then
第二个参数是针对于上一个返回Promise
的。- 两者的优先级:就是看谁在链式写法的前面,在前面的先捕获到错误,后面就没有错误可以捕获了,链式前面的优先级大,而且两者都不是
break
, 可以继续执行后续操作不受影响。
7、链式写法的错误处理
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch
语句捕获。
因此,即使有很多Promise也只用写一个catch。链式中任何一个环节出问题,都会被catch到,同时在某个环节后面的代码就不再执行了。
const setDelay = (millisecond) => { return new Promise((resolve, reject)=>{ if (typeof millisecond != 'number') reject(new Error('参数必须是number类型')); setTimeout(()=> { resolve(`我延迟了${millisecond}毫秒后输出的`) }, millisecond) }) } const setDelaySecond = (seconds) => { return new Promise((resolve, reject)=>{ if (typeof seconds != 'number' || seconds > 10) reject(new Error('参数必须是number类型,并且小于等于10')); setTimeout(()=> { console.log(`先是setDelaySeconds函数输出,延迟了${seconds}秒,一共需要延迟${seconds+2}秒`) resolve(setDelay(2000)) // 这里依赖上一个Promise }, seconds * 1000) }) } setDelay('2000') .then((result)=>{ console.log('第一步完成了'); console.log(result) return setDelaySecond(3) }) .catch((err)=>{ // 这里移到第一个链式去,发现上面的不执行了,下面的继续执行 console.log(err); }) .then((result)=>{ console.log('第二步完成了'); console.log(result); })
执行效果如下所示:
Error: 参数必须是number类型 at /Users/hqs/WebstormProjects/hsedu_mgr/hsedu_mgr/test.js:3:48 at new Promise (<anonymous>) at setDelay (/Users/hqs/WebstormProjects/hsedu_mgr/hsedu_mgr/test.js:2:10) at Object.<anonymous> (/Users/hqs/WebstormProjects/hsedu_mgr/hsedu_mgr/test.js:20:1) at Module._compile (internal/modules/cjs/loader.js:774:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:785:10) at Module.load (internal/modules/cjs/loader.js:641:32) at Function.Module._load (internal/modules/cjs/loader.js:556:12) at Function.Module.runMain (internal/modules/cjs/loader.js:837:10) at internal/main/run_main_module.js:17:11 第二步完成了 undefined
可以看到虽然出现了错误,但是链式走完了。输出undefined是由于上一个then没有返回Promise。
由此可见:链式中的catch
并不是终点,catch完如果还有then还会继续往下走。
catch
只是捕获错误的一个链式表达,并不是break!
因此catch放的位置也很有讲究,一般放在一些重要的、必须catch的程序的最后。这些重要的程序中间一旦出现错误,会马上跳过其他后续程序的操作直接执行到最近的catch代码块,但不影响catch后续的操作!
到这就不得不体一个ES2018标准新引入的Promise的finally
,表示在catch后必须肯定会默认执行的的操作。细节可以参考:Promise的finally
8、Promise链式中间返回自定义值
Promise.resolve():返回一个状态由给定value决定的Promise对象。如果该值是thenable(即,带有then方法的对象),返回的Promise对象的最终状态由then方法执行决定;否则的话(该value为空,基本类型或者不带then方法的对象),返回的Promise对象状态为fulfilled,并且将该value传递给对应的then方法。
通常而言,如果不知道一个值是否是Promise对象,使用Promise.resolve(value) 来返回一个Promise对象,这样就能将该value以Promise对象形式使用。
(1)示例
const setDelay = (millisecond) => { return new Promise((resolve, reject)=>{ if (typeof millisecond != 'number') reject(new Error('参数必须是number类型')); setTimeout(()=> { resolve(`我延迟了${millisecond}毫秒后输出的`) }, millisecond) }) } const setDelaySecond = (seconds) => { return new Promise((resolve, reject)=>{ if (typeof seconds != 'number' || seconds > 10) reject(new Error('参数必须是number类型,并且小于等于10')); setTimeout(()=> { console.log(`先是setDelaySeconds函数输出,延迟了${seconds}秒,一共需要延迟${seconds+2}秒`) resolve(setDelay(2000)) // 这里依赖上一个Promise }, seconds * 1000) }) } setDelay(2000).then((result)=>{ console.log('第一步完成了'); console.log(result); let message = '这是我自己想处理的值'; return Promise.resolve(message) // 这里返回我想在下一阶段处理的值 }) .then((result)=>{ console.log('第二步完成了'); console.log(result); // 这里拿到上一阶段的返回值 //return Promise.resolve('这里可以继续返回') }) .catch((err)=>{ console.log(err); }) /* 输出结果: * 第一步完成了 * 我延迟了2000毫秒后输出的 * 第二步完成了 * 这是我自己想处理的值 * */
(2)Promise.resolve方法参数
- 参数是一个Promise实例
- 参数是一个thenable对象
- 参数不是具有then方法的对象,或根本不是对象
- 不带有任何参数
9、跳出或停止Promise链式
不同于通过break跳出或停止循环和switch。
在使用Promise链式时,如果使用这样的操作:func().then().then().then().catch()
的方式,想在第一个then
就跳出链式,后面的不执行,就需要去中断后继调用链。
(1)方法一:通过抛出一个异常终止
如果catch在中间(不再末尾),同时也不想执行catch后面的代码,即实现链式的绝对中止。
要实现绝对终止,需要理解Promise的三种状态:pending,resolve,rejected。pending状态就是请求中的状态,成功请求就是resolve,失败就是reject,因此pending就是个中间过渡状态。
then
的下一层级其实得到的是上一层级返回的Promise对象,也就是说原Promise对象与新对象状态保持一致。
因此如果想让链式在某一层终止,可以让它永远都pending下去,后续的操作就不存在了。
setDelay(2000) .then((result)=>{ console.log(result) console.log('我进行到第一步的'); return setDelaySecond(1) }) .then((result)=>{ console.log(result); console.log('我主动跳出循环了'); // return Promise.reject('跳出循环的信息') // 重点在这 return new Promise(()=>{console.log('后续的不会执行')}) // 这里返回的一个新的Promise,没有resolve和reject,那么会一直处于pending状态,因为没返回啊,那么这种状态就一直保持着,中断了这个Promise }) .then((result)=>{ console.log('我不执行'); }) .catch((mes)=>{ console.dir(mes) console.log('我跳出了'); }) .then((res)=>{ console.log('我也不会执行') })
执行结果:
我延迟了2000毫秒后输出的
我进行到第一步的
先是setDelaySeconds函数输出,延迟了1秒,一共需要延迟3秒
我延迟了2000毫秒后输出的
我主动跳出循环了
后续的不会执行
这样就实现了错误跳出且完全终止Promise链。但是这种方法会导致潜在的内存泄露问题。
因为这个一直处于pending状态下的Promise会一直处于被挂起的状态,而且浏览器的机制细节也不清楚,一般的网页没有关系,但大量的复杂的这种pending状态势必会导致内存泄漏,具体的没有测试,这篇文章可以参考查阅:从如何停掉 Promise 链说起。
但是一般情况下是不会存在泄漏,只是有这种风险。取消问题一直是Promise的痛点。
(2)方法二:通过reject来中断
依据链式的思想,拒绝掉某一链,相当于直接跳到了catch模块。
setDelay(2000) .then((result)=>{ console.log(result) console.log('我进行到第一步的'); return setDelaySecond(1) }) .then((result)=>{ console.log('我进行到第二步的'); console.log(result); console.log('我主动跳出循环了'); return Promise.reject('跳出循环的信息') // 这里返回一个reject,主动跳出循环了 }) .then((result)=>{ console.log('我不执行'); }) .catch((mes)=>{ console.dir(mes) console.log('我跳出了'); })
执行输出:
我延迟了2000毫秒后输出的 我进行到第一步的 先是setDelaySeconds函数输出,延迟了1秒,一共需要延迟3秒 我进行到第二步的 我延迟了2000毫秒后输出的 我主动跳出循环了 '跳出循环的信息' 我跳出了
查看执行输出,可以发现’我不执行‘这一条没有输出,跳过了这一链,直接跳到了catch模块。
很容易看到缺点:有时不确定是因为错误跳出还是主动跳出。加入标识位优化:
return Promise.reject({ isNotErrorExpection: true // 返回的地方加一个标志位,判断是否是错误类型,如果不是,那么说明可以是主动跳出循环的 })
或者根据上述的代码判断catch的地方输出的类型是不是属于错误对象的,是的话说明是错误,不是的话说明是主动跳出的,可以自己选择(这就是为什么要统一错误reject的时候输出new Error('错误信息')的原因,规范!)
当然也可以直接抛出一个错误跳出:
throw new Error('错误信息') // 直接跳出,那就不能用判断是否为错误对象的方法进行判断了
10、将多个Promise打包成一个
Promise.all()方法用于将多个Promise实例,包装成一个新的Promise实例。
Promise.all(iterable); /* 参数 * iterable:一个可迭代对象,Arry或String等 */
(1)返回值
成功和失败的返回值是不同的,成功的时候返回的是一个结果数组。
let p1 = new Promise((resolve, reject) => { resolve('成功了') }) let p2 = new Promise((resolve, reject) => { resolve('success') }) let p3 = Promse.reject('失败') Promise.all([p1, p2]).then((result) => { console.log(result) //['成功了', 'success'] }).catch((error) => { console.log(error) })
失败的时候则返回最先被reject失败状态的值。
let p1 = new Promise((resolve, reject) => { resolve('成功了') }) let p2 = new Promise((resolve, reject) => { resolve('success') }) let p3 = Promse.reject('失败') Promise.all([p1,p3,p2]).then((result) => { console.log(result) }).catch((error) => { console.log(error) // 失败了,打出 '失败' })
(2)应用场景
Promse.all在处理多个异步处理时非常有用,比如说一个页面上需要等两个或多个ajax的数据回来以后才正常显示,在此之前只显示loading图标。
let wake = (time) => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(`${time / 1000}秒后醒来`) }, time) }) } let p1 = wake(3000) let p2 = wake(2000) Promise.all([p1, p2]).then((result) => { console.log(result) // [ '3秒后醒来', '2秒后醒来' ] }).catch((error) => { console.log(error) })
需要特别注意的是:Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,即p1的结果在前,即便p1的结果获取的比p2要晚。这带来了一个绝大的好处:在前端开发请求数据的过程中,偶尔会遇到发送多个请求并根据请求顺序获取和使用数据的场景,使用Promise.all毫无疑问可以解决这个问题。
四、ES6异步方案——生成器Generators/yield
详见:ES6异步方案——生成器Generators/yield
五、ES7异步方案——async/await
详见: