Promise详解

前言:


日常开发中,异步操作几乎每天都能见到。传统的意不解决方案是通过回调函数,随着程序逻辑越来越复杂,回调函数的方式变得越来越繁琐,很容易出现回调地狱,于是一种更合理更强大的代替方案出现–Promise,接下来就深入学习Promise是如何解决异步操作的。

一.基础

定义: Promise 对象用于表示一个异步操作的最终完成 (或失败), 及其结果值.

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('foo');
  }, 300);
});

promise1.then((value) => {
  console.log(value);
  // expected output: "foo"
});

console.log(promise1);
// expected output: [object Promise]



运行上面这段代码,先是打印 [object Promise] ,300ms后打印foo

语法

new Promise( function(resolve, reject) {...} /* executor */  )

 
 

    参数 executor executor是带有 resolve 和 reject 两个参数的函数 。Promise构造函数执行时立即调用executor 函数, resolve 和 reject 两个函数作为参数传递给executor(executor 函数在Promise构造函数返回promise实例对象前被调用)。resolve 和 reject 函数被调用时,分别将promise的状态改为fulfilled(完成)或rejected(失败)。executor 内部通常会执行一些异步操作,一旦异步操作执行完毕(可能成功/失败),要么调用resolve函数来将promise状态改成fulfilled,要么调用reject 函数将promise的状态改为rejected。如果在executor函数中抛出一个错误,那么该promise 状态为rejected。executor函数的返回值被忽略。

    这段描述分解下就是:

    1. 实例化Promise对象时需要传入一个executor函数,所有业务代码都需要写在这个函数中;
    2. executor函数在构造函数执行时就会调用,此时实例化对象还并没有被创建, resolve 和 reject 两个函数作为参数传递给executor,resolve 和 reject 函数被调用时,分别将promise的状态改为fulfilled(完成)或rejected(失败)。一旦状态改变,就不会再变,任何时候都可以得到这个结果。
    3. 如果executor中代码抛出了错误,promise 状态为rejected;
    4. executor函数的返回值被忽略。

    状态 一个 Promise有以下几种状态:

    • pending: 初始状态,既不是成功,也不是失败状态。
    • fulfilled: 意味着操作成功完成。
    • rejected: 意味着操作失败。

    pending 状态的 Promise 对象可能会变为fulfilled 状态并传递一个值给相应的状态处理方法,也可能变为失败状态(rejected)并传递失败信息。当其中任一种情况出现时,Promise 对象的 then 方法绑定的处理方法(handlers )就会被调用(then方法包含两个参数:onfulfilled 和 onrejected,它们都是 Function 类型。当Promise状态为fulfilled时,调用 then 的 onfulfilled 方法,当Promise状态为rejected时,调用 then 的 onrejected 方法, 所以在异步操作的完成和绑定处理方法之间不存在竞争)。

    const promise1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('fulfilled');
      }, 1000);
    });
    
    const promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
    reject(‘rejected’);
    }, 3000);
    });
    
    promise1.then((fulfilled) => {
    console.log(fulfilled);
    },(rejected)=>{
    console.log(rejected);
    });
    promise2.then((fulfilled) => {
    console.log(fulfilled);
    },(rejected)=>{
    console.log(rejected);
    })
    

    运行上面这段代码,1s后打印fulfilled,3s后打印rejected rejected状态的 Promise也可以通过.catch进行捕获,因为 Promise.prototype.then 和 Promise.prototype.catch 方法返回promise 对象, 所以它们可以被链式调用。所以上述代码可以改为:

    promise1.then((fulfilled) => {
      console.log(fulfilled);
    }).catch((rejected)=>{
    	console.log(rejected);
    });
    promise2.then((fulfilled) => {
      console.log(fulfilled);
    }).catch((rejected)=>{
    	console.log(rejected);
    });
    

    二.深入理解

    1.Promise 是用来管理异步编程的,它本身不是异步的,new Promise的时候会立即把executor函数执行,只不过我们一般会在executor函数中处理一个异步操作。例如下面一段代码:

    let firstPromise = new Promise(()=>{
        setTimeout(()=>{
          console.log(1)
        },1000)
        console.log(2)
      })
    console.log(3) // 2 3 1
    

    2.Promise 采用了回调函数延迟绑定技术,在执行 resolve 函数的时候,回调函数还没有绑定,那么只能推迟回调函数的执行。这具体是啥意思呢?我们先来看下面的例子:

    let p1 = new Promise((resolve,reject)=>{
      console.log(1);
      resolve('浪里行舟')
      console.log(2)
    })
    // then:设置成功或者失败后处理的方法
    p1.then(result=>{
     //p1延迟绑定回调函数
      console.log('成功 '+result)
    },reason=>{
      console.log('失败 '+reason)
    })
    console.log(3)
    // 1
    // 2
    // 3
    // 成功 浪里行舟
    

    new Promise的时候先执行executor函数,打印出 1、2,Promise在执行resolve时,触发微任务,还是继续往下执行同步任务, 执行p1.then时,存储起来两个函数(此时这两个函数还没有执行),然后打印出3,此时同步任务执行完成,最后执行刚刚那个微任务,从而执行.then中成功的方法。

    3.错误处理,多个Promise链式操作的错误捕获可以通过一个catch处理;例如下面一段代码:

    let executor = function(resolve,reject){
    	let random = Math.random()
    	if(random>0.5){
    		resolve()
    	}else{
    		reject()
    	}
    }
    let p1 = new Promise(executor)
    p1.then(resualt=>{
    	console.log(1)
    	return new Promise(executor)
    }).then(resualt=>{
    	console.log(2)
    	return new Promise(executor)
    }).then(resualt=>{
    	console.log(3)
    	return new Promise(executor)
    }).catch((error) => {
      console.log('error', error)
    })
    

    这段代码有四个 Promise 对象,无论哪个对象里面抛出异常,都可以通过最后一个.catch 来捕获异常,通过这种方式可以将所有 Promise 对象的错误合并到一个函数来处理,这样就解决了每个任务都需要单独处理异常的问题。

    4.常用方法

    • Promise.resolve() Promise.resolve(value)方法返回一个以给定值解析后的Promise 对象。 Promise.resolve()等价于下面的写法:
    Promise.resolve('foo')
    // 等价于
    new Promise(resolve => resolve('foo'))
    

    Promise.resolve方法的参数分成四种情况。

    (1)参数是一个 Promise 实例

    const p1 = new Promise(function (resolve, reject) {
      setTimeout(() => reject(new Error('fail')), 3000)
    })
    const p2 = new Promise(function (resolve, reject) {
      setTimeout(() => resolve(p1), 1000)
    })
    p2
      .then(result => console.log(result))
      .catch(error => console.log(error))
    // Error: fail
    

    上面代码中,p1是一个 Promise,3 秒之后变为rejected。p2的状态在 1 秒之后改变,resolve方法返回的是p1。由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以,后面的then语句都变成针对后者(p1)。又过了 2 秒,p1变为rejected,导致触发catch方法指定的回调函数。

    (2)参数不是具有then方法的对象,或根本就不是对象

    Promise.resolve("Success").then(function(value) {
     // Promise.resolve方法的参数,会同时传给回调函数。
      console.log(value); // "Success"
    }, function(value) {
      // 不会被调用
    });
    

    (3)不带有任何参数 Promise.resolve()方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。如果希望得到一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve()方法。

    Promise.resolve().then(function () {
      console.log('two');
    });
    console.log('one');
    // one two
    

    (4)参数是一个thenable对象 thenable对象指的是具有then方法的对象,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
    });
    
    • Promise.reject() Promise.reject()方法返回一个带有拒绝原因的Promise对象。
    new Promise((resolve,reject) => {
        reject(new Error("出错了"));
    });
    // 等价于
     Promise.reject(new Error("出错了"));  
    
    // 使用方法
    Promise.reject(new Error(“BOOM!”)).catch(error => {
    console.error(error);
    });
    
    

    值得注意的是,调用resolve或reject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,而不应该直接写在resolve或reject的后面。所以,最好在它们前面加上return语句,这样就不会有意外。

    new Promise((resolve, reject) => {
      return reject(1);
      // 后面的语句不会执行
      console.log(2);
    })
    
    • Promise.all()
    var p1 = Promise.resolve(1)
    var p2 = Promise.resolve({a:2})
    var p3 = new Promise(function(resolve,reject){
    	setTimeout(function(){
    		resolve(3)
    	},3000)
    })
    Promise.all([p1,p2,p3]).then(result=>{
    	// 返回的结果是按照Array中编写实例的顺序来
    	console.log(result)
    })
    

    Promise.all 生成并返回一个新的 Promise 对象,所以它可以使用 Promise 实例的所有方法。参数传递promise数组中所有的 Promise 对象都变为resolve的时候,该方法才会返回, 新创建的 Promise 则会使用这些 promise 的值。 如果参数中的任何一个promise为reject的话,则整个Promise.all调用会立即终止,并返回一个reject的新的 Promise 对象。

    • Promise.allSettled()

    有时候,我们不关心异步操作的结果,只关心这些操作有没有结束。这时,ES2020 引入Promise.allSettled()方法就很有用。如果没有这个方法,想要确保所有操作都结束,就很麻烦。Promise.all()方法无法做到这一点。

    假如有这样的场景:一个页面有三个区域,分别对应三个独立的接口数据,使用 Promise.all 来并发请求三个接口,如果其中任意一个接口出现异常,状态是reject,这会导致页面中该三个区域数据全都无法出来,显然这种状况我们是无法接受,Promise.allSettled的出现就可以解决这个痛点:

    Promise.allSettled([
      Promise.reject({ code: 500, msg: '服务异常' }),
      Promise.resolve({ code: 200, list: [] }),
      Promise.resolve({ code: 200, list: [] })
    ]).then(res => {
      console.log(res)
      /*
        0: {status: "rejected", reason: {…}}
        1: {status: "fulfilled", value: {…}}
        2: {status: "fulfilled", value: {…}}
      */
      // 过滤掉 rejected 状态,尽可能多的保证页面区域数据渲染
      RenderContent(
        res.filter(el => {
          return el.status !== 'rejected'
        })
      )
    })
    

    Promise.allSettled跟Promise.all类似, 其参数接受一个Promise的数组, 返回一个新的Promise, 唯一的不同在于, 它不会进行短路, 也就是说当Promise全部处理完成后,我们可以拿到每个Promise的状态, 而不管是否处理成功。

    • Promise.race()

    Promise.all()方法的效果是"谁跑的慢,以谁为准执行回调",那么相对的就有另一个方法"谁跑的快,以谁为准执行回调",这就是Promise.race()方法,这个词本来就是赛跑的意思。race的用法与all一样,接收一个promise对象数组为参数。

    Promise.all在接收到的所有的对象promise都变为FulFilled或者Rejected状态之后才会继续进行后面的处理,与之相对的是Promise.race只要有一个promise对象进入FulFilled或者Rejected状态的话,就会继续进行后面的处理。

    // `delay`毫秒后执行resolve
    function timerPromisefy(delay) {
        return new Promise(resolve => {
            setTimeout(() => {
                resolve(delay);
            }, delay);
        });
    }
    // 任何一个promise变为resolve或reject的话程序就停止运行
    Promise.race([
        timerPromisefy(1),
        timerPromisefy(32),
        timerPromisefy(64)
    ]).then(function (value) {
        console.log(value);    // => 1
    });
    

    上面的代码创建了3个promise对象,这些promise对象会分别在1ms、32ms 和 64ms后变为确定状态,即FulFilled,并且在第一个变为确定状态的1ms后,.then注册的回调函数就会被调用。

    • Promise.prototype.finally()

    ES9 新增 finally() 方法返回一个Promise。在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。这为在Promise是否成功完成后都需要执行的代码提供了一种方式。这避免了同样的语句需要在then()和catch()中各写一次的情况。 比如我们发送请求之前会出现一个loading,当我们请求发送完成之后,不管请求有没有出错,我们都希望关掉这个loading。

    this.loading = true
    request()
      .then((res) => {
        // do something
      })
      .catch(() => {
        // log err
      })
      .finally(() => {
        this.loading = false
      })
    

    finally方法的回调函数不接受任何参数,这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

    三.实际应用

    假设有这样一个需求:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯不断交替重复亮灯? 三个亮灯函数已经存在:

    function red() {
        console.log('red');
    }
    function green() {
        console.log('green');
    }
    function yellow() {
        console.log('yellow');
    }
    

    这道题复杂的地方在于需要“交替重复”亮灯,而不是亮完一遍就结束,我们可以通过递归来实现:

    // 用 promise 实现
    let task = (timer, light) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          if (light === 'red') {
            red()
          }
          if (light === 'green') {
            green()
          }
          if (light === 'yellow') {
            yellow()
          }
          resolve()
        }, timer);
      })
    }
    let step = () => {
      task(3000, 'red')
        .then(() => task(1000, 'green'))
        .then(() => task(2000, 'yellow'))
        .then(step)
    }
    step()
    

    同样也可以通过async/await 的实现:

    //  async/await 实现
    let step = async () => {
      await task(3000, 'red')
      await task(1000, 'green')
      await task(2000, 'yellow')
      step()
    }
    step()
    

    参考资料 你真的懂Promise吗

    • 0
      点赞
    • 0
      收藏
      觉得还不错? 一键收藏
    • 0
      评论

    “相关推荐”对你有帮助么?

    • 非常没帮助
    • 没帮助
    • 一般
    • 有帮助
    • 非常有帮助
    提交
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值