zoukankan      html  css  js  c++  java
  • 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

      详见:

  • 相关阅读:
    Swift:属性观察器
    swift:谈谈swift几种常见属性的区别
    iOS:崩溃统计工具Crashlytics的使用
    【互动出版网】2013双11全场科技图书六折包邮
    【互动出版网】11.11购物狂欢节重磅大促,免费领万千优惠券
    C#编程兵书
    C++编程兵书
    HTML+CSS网站开发兵书
    Java编程兵书
    网络运维与管理2013超值精华本
  • 原文地址:https://www.cnblogs.com/xiugeng/p/12775175.html
Copyright © 2011-2022 走看看