Promise 学习

217 阅读5分钟

我正在参加「掘金·启航计划」 \

1. Promise 简单了解

Promise(中文翻译:承诺)主要用于异步计算

  • 可以将异步操作队列化,按照期望的顺序执行,返回符合预期的结果。
  • 可以在对象之间传递和操作promise,帮助我们处理队列。

异步回调的问题:            

  1. 之前处理异步是通过纯粹的回调函数的形式进行处理,很容易进入到回调地狱中,剥夺了函数 return 的能力。虽然问题可以解决,但是难以读懂,且维护困难,稍有不慎就会踏入:回调地狱 - 嵌套层次深,不好维护。

  2. 而 promise 是一个对象,对象和函数的区别就是对象可以保存状态,函数不可以(闭包除外) 并未剥夺函数 return 的能力,因此无需层层传递 callback,进行回调获取数据。且代码风格容易理解,便于维护多个异步等待合并便于解决 

2. Promise 详解:

/ 创建promise对象
new Promise((resolve,reject)=>{
    // 第一次执行promise
    console.log('第一层promise')
    // 第二次执行promise
    resolve('这是第二层promise')
    // 定义报错信息;报错时输出这一行
    reject('这是一个错误')
  	// 也可以主动抛出异常来控制报错时输出什么
  	throw "抛出一个异常"
// 成功时执行
}).then(res => {
    // 输出执行成功的数据
    console.log(res)
    // 执行 action
    return action("使用函数统一控制嵌套函数",4000)
// 报错时提醒
},err=>{
    console.log(err)
})

2.1. 实例:

执行结果:定时器每隔一秒执行一次,promise 一次性执行完

多个promise嵌套:

结果:

第二种:效果一样

第三种嵌套方式:效果都一样,项目开发时按需使用

2.2. Promise 里嵌套定时器

简单嵌套:

效果:

使用函数的方式使用:

结果:

3. 使用 Promise 加载图片

3.1 传统方案

传统图片加载方式,会导致地狱回调,让代码及其不优雅,且难以维护。

let url1 = "https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3293635140,3955114282&fm=26&gp=0.jpg"
let url2 = "https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=1019333328,1858047370&fm=26&gp=0.jpg"
let url3 = "https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=4226468334,723383513&fm=26&gp=0.jpg"
let oImg1 = new Image()
oImg1.onload = function() {
    console.log('img1加载完毕')
    let oImg2 = new Image()
    oImg2.onload = function() {
        console.log('img2加载完毕')
        let oImg3 = new Image()
        oImg3.onload = function() {
            console.log('img3加载完毕')
            console.log('全部加载完毕')
        }
        oImg3.src = url3
    }
    oImg2.src = url2
}
oImg1.src = url1

3.2 使用 promise 加载图片

function loadImg(url) {
    let img = new Image()
    img.src = url
    return new Promise((resolve, reject) => {
        img.onload = () => {
            console.log(url)
            resolve()
        }
        img.onerror = (e) => {
            reject(e)
        }
    })
}

// 图片
let url1 = 'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3293635140,3955114282&fm=26&gp=0.jpg'
let url2 = "https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=1019333328,1858047370&fm=26&gp=0.jpg"
let url3 = "https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=4226468334,723383513&fm=26&gp=0.jpg"

// 解决回调地狱问题,使代码变得清晰
loadImg(url1).then(() => {
    return loadImg(url2)
}).then(() => {
    return loadImg(url3)
})

4. Promise 方法

4.1 Promise.all()

成功的时候返回的是一个结果数组,而失败的时候则返回最先被 reject 失败状态的值。且一旦有失败,其他请求成功的结果也不会返回。

运行结果:两秒过后同时输出

4.2 Promise.race()

返回第一个执行完毕的 promise。

// promise1
var p1 = new Promise((resolve, reject) => {
    setTimeout(resolve, 500, "one")
})
// promise2
var p2 = new Promise((resolve, reject) => {
    setTimeout(resolve, 100, "two")
})

Promise.race([p1, p2]).then(value => {
  // 输出:two
  // 虽然两个 promise 都执行完成了,但很显然 p2 更快。
  console.log(value)
})

//再来看个案例

// promise3
var p3 = new Promise((resolve, reject) => {
    setTimeout(reject, 100, "three")
})
// promise4
var p4 = new Promise((resolve, reject) => {
    setTimeout(resolve, 500, "four")
})

Promise.race([p4, p3]).then(value => {
  // 输出:three
  // 虽然 p3 是一个错误的 promise,但 race() 方法永远只返回第一个执行完成的 promise。
  console.log(value)
})

4.3 Promise.allSettled()

将传入的 promise 集合执行完成后的状态返回出去。

const promise1 = Promise.resolve(3)
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'foo'))

Promise.allSettled([promise1, promise2]).
	// 输出:fulfilled 3 undefined
	// 			 rejected undefined foo
  then((results) => results.forEach((result) => console.log(result.status, result.value, result.reason)))

4.4 Promise.done()

done() 方法在 promise 里是不存在的,是大佬自己写的在使用 promise 时的一个很实用的方法。

done() 保证捕捉到任何可能出现的错误,并向全局抛出。

Promise.prototype.done = function (onFulfilled, onRejected) {
  this.then(onFulfilled, onRejected).catch(function (reason) {
      // 抛出一个全局错误
      setTimeout(() => {
        throw reason
      }, 0)
    })
}

4.5 Promise.finally()

promise 之前并没有这个方法,是最近两年 ES 更新的新语法,所以在浏览器兼容性上并不是太好。早年是大佬在 promise 的原型上添加了 finally() 这个方法来手动实现的。

在 promise 全部执行完毕后做一些统一处理。无论成功还是失败都会执行的处理。并且因为无法知道 promise 的最终状态,所以 finally() 的回调函数中不接受任何参数。它只能用于最终结果无论如何都必须要执行的情况。

const p = new Promise((resolve, reject) => {
  if(Math.random() > 0.5){
    resolve('resolve')
  }
  reject('reject')
})

p.then((res) => {
  console.log(res);
}).catch((err) => {
  console.error(err);
}).finally(() => {
  // 无论 promise 执行结果如何,都会执行这里的 执行完毕
  console.log('执行完毕')
})

// 假设执行成功,则会输出:resolve 执行完毕
// 执行失败,则会输出:reject 执行完毕

实现 promise.finally() 方法

Promise.prototype.finally = function (callback) {
  let P = this.constructor
  return this.then(
    value => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => throw reason)
  )
}

4.6 Promise.any()

执行传入的所有 promise 并返回第一个执行成功的。只要第一个 promise 已成功返回,那么之后的所有 promise 都不在执行。

const p = [
  Promise.resolve('result A'),
  Promise.resolve('result B'),
  Promise.resolve('result C'),
]

promise.any(p).then((value) => {
  // 输出:result A
  console.log('value: ', value)
})

5. 扩展

1. 实现异步并发数量控制

// 异步控制并发数
// urls:请求列表。
// limit:限制发送数。
function limitRequest(urls = [], limit = 3) {
  return new Promise((resolve, reject) => {
    // 获取请求总数
    const len = urls.length;
    // 当前执行次数
    let count = 1;
    // 同步启动limit个任务
    while (limit > 0) {
      start();
      // 并发数减一
      limit -= 1;
    }
    // 实现方法
    function start() {
      // 从数组中拿取第一个任务
      const url = urls.shift();
      if (url) {
        axios.post(url).finally(() => {
          // 判断当前执行次数是否和请求总数一致
          if (count == len) {
            // 最后一个任务完成时,调用 resolve() 退出 Promise
            resolve();
          } else {
            // 完成之后,启动下一个任务
            count++;
            start();
          }
        });
      }
    }
  });
}