JS异步编程方案(promise)

  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() 
// 执行结果和上面完全一样

  与监听例子的区别:

  1. 创建了一个Message全局对象,并且listeners移到该对象
  2. on方法改名为subscribe方法,并且移到Message对象上
  3. 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方法包含两个参数:onfulfilledonrejected,它们都是 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

  详见:

posted @ 2020-04-26 21:05  休耕  阅读(891)  评论(0编辑  收藏  举报