【JavaScript】ES6之Promise用法详解及其应用(超时、控制并发、重复请求问题等)

601 阅读5分钟

概念

Promise 是异步编程的一种解决方案。

Promise对象是一个构造函数,用来生成Promise实例

  1. 当使用promise时,会传入一个执行器,执行器传入两个函数(resolve, reject),此执行器是立即执行的;
  2. 有三种状态:成功(fulfilled),失败(rejected),等待(pending);
  3. 执行器调用resolve走成功态,如果调用reject或发生异常,走失败态;如果执行器抛异常,走失败态;
  4. promise状态一旦改变,以后不能更改。
new Promise((resolve, reject) => {
  resolve('ok')
})

手写Promise,实现Promises/A+规范,实现all、race、finally、allSettlte、any等方法的实现 - 掘金 (juejin.cn)

原型方法

Promise.prototype.then

then() 方法返回一个新的Promise实例。它最多需要有两个参数:Promise 的成功和失败情况的回调函数。

// 成功的情况
new Promise((resolve, reject) => {
  resolve('ok')
}).then((value) => {
  console.log(value) // ok
})
// 失败的情况
new Promise((resolve, reject) => {
  reject('error')
}).then(null, (resson) => {
    console.log(resson) // error
})

链式调用:

  • 情况1:then后面直接.then
new Promise((resolve, reject) => {
  resolve('ok')
}).then().then().then((value) => {
  console.log(value) // ok
})
  • 情况2:then返回一个常量
new Promise((resolve, reject) => {
  resolve('ok')
})
  .then(value => {
    return 'okok'
  })
  .then(value => {
    console.log(value) // 'okok'
  })
  • 情况3:then返回一个Promise
new Promise((resolve, reject) => {
  resolve('ok')
})
  .then(value => {
    return new Promise((resolve, reject) => {
      resolve('okok')
    })
  })
  .then(value => {
    console.log(value) // 'okok'
  })

Promise.prototype.catch

catch() 用于注册一个在 promise 被拒绝时调用的函数,返回一个新的Promise实例。此方法是Promise.prototype.then(undefined, onRejected)的一种简写形式。

new Promise((resolve, reject) => {
  reject('error')
}).catch(reason => {
  console.log(reason) // error
})

同样支持链式写法

new Promise((resolve, reject) => {
  reject('error')
}).catch().catch().catch(reason => {
  console.log(reason) // error
})

catch与then情况一样:

  1. 不返回值或返回常值,promise状态为成功
  2. 产生异常(比如手动throw),promise状态为失败
  3. 返回一个promise,状态就是返回的promise的状态

Promise.prototype.finally

finally()  方法返回一个 Promise。在 promise 结束时,无论结果是 fulfilled 或者是 rejected,都会执行指定的回调函数。
这为在 Promise 是否成功完成后都需要执行的代码提供了一种方式,这避免了同样的语句需要在 then() 和 catch() 中各写一次的情况。

静态方法

Promise.resolve()

将现有对象转为 Promise 对象。方法也会返回一个新的 Promise 实例。

const p1 = Promise.resolve('p1-resolve')
p1.then((val) => {
  console.log(val)
})
Promise.resolve().then(() => {
  return Promise.resolve('123')
}).then((val) => {
  console.log(val) // 123
})
  • 参数是一个 Promise 实例 如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。
const p1 = new Promise((resolve, reject) => {
  resolve('abc')
})
const p2 = Promise.resolve(p1)
console.log(p1 === p2) // true
p2.then((value) => {
  console.log(value)
})
  • 参数是一个thenable对象 thenable对象指的是具有then方法的对象,比如下面这个对象。
const thenable = {
  then: function(resolve, reject) {
    resolve('abc')
  }
};

Promise.resolve()方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then()方法。

const thenable = {
  then: function(resolve, reject) {
    resolve('abc')
  }
}
const p1 = Promise.resolve(thenable).then((val) => {
  console.log(val)
})

Promise.reject()

Promise.reject()  方法返回一个带有拒绝原因的 Promise 对象。

Promise.reject(new Error('err')).catch(e => {
  console.log(e.message) // err
})

Promise.race()

Promise.race(iterable) 该方法返回一个 Promise,它将与第一个传递的 promise 相同的完成方式被完成。它可以是完成或失败,这要取决于第一个完成的方式是两个中的哪个。

const delay = (ms, msg) => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(msg)
    }, ms)
  })
}
const p1 = delay(1500, 'p1')
const p2 = delay(2000, 'p2')
const p3 = delay(1000, 'p3')
Promise.race([p1, p2, p3]).then(value => {
  console.log(value) // p3
})

Promise.all()

Promise.all()将多个Promise放在一个数组中,当整个数组的全部promise成功时才会返回成功,当数组中的promise有一个出现失败时就返回失败 (失败的原因是第一个失败promise的结果)。

const delay = (ms, msg) => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(msg)
    }, ms)
  })
}
const p1 = delay(1500, 'p1')
const p2 = delay(2000, 'p2')
const p3 = delay(1000, 'p3')
Promise.all([p1, p2, p3]).then(value => {
  console.log(value) // 2s后打印 [ 'p1', 'p2', 'p3' ]
})

Promise.allSettled

返回一个新的 Promise 对象。只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled还是rejected),返回的 Promise 对象才会发生状态变更。

const delay = (ms, msg, isReject = false) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      isReject ? reject(msg) : resolve(msg)
    }, ms)
  })
}
const p1 = delay(1500, 'p1')
const p2 = delay(2000, 'p2')
const p3 = delay(1000, 'p3', true)
Promise.allSettled([p1, p2, p3]).then(value => {
  console.log(value) // 2s后打印
  /**
   [
    { status: 'fulfilled', value: 'p1' },
    { status: 'fulfilled', value: 'p2' },
    { status: 'rejected', reason: 'p3' }
  ]
   */
})

Promis.any()

ES2021 引入了Promise.any()方法。该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。
只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。\

Promise.any()Promise.race()方法很像,只有一点不同,就是Promise.any()不会因为某个 Promise 变成rejected状态而结束,必须等到所有参数 Promise 变成rejected状态才会结束。

const delay = (ms, msg, isReject = false) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      isReject ? reject(msg) : resolve(msg)
    }, ms)
  })
}
const p1 = delay(1500, 'p1')
const p2 = delay(2000, 'p2')
const p3 = delay(1000, 'p3', true)
Promise.any([p1, p2, p3]).then(value => {
  console.log(value) // 2s后打印: p1
})

应用

接口请求超时

使用Promise.race来实现

// 模拟请求
function request(params) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(params)
    }, 3000)
  })
}
/**
 * 超时包装函数
 * @param {() => Promise<any>} p 请求函数
 * @param {number} timeout 超时时间
 * @returns
 */
function timeoutWrap(p, timeout) {
  const delay = new Promise((_, reject) => {
    setTimeout(() => {
      reject('超时了')
    }, timeout)
  })
  return (...args) => {
    const p1 = p(...args)
    const res = Promise.race([p1, delay])
    return res
  }
}
const r1 = timeoutWrap(request, 1000)({ id: 1 })
r1.then(response => {
  console.log(response)
}).catch(e => {
  console.log('e', e)
})

控制并发

之前在开发大文件上传的时候,文件很大的时候,会分成较多的切片,如果此时将全部切片一次性上传,并发就会很大。那我们能不能控制一下,比如一次只发6个请求,某一个请求完了,就让第7个补上,又请求完了,让第8个补上,以此类推,让最高并发量变成可控的。
我们先来模拟一下并发很多会怎么样,如下面的例子:

<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
function request(i = 0) {
    const r = Date.now() + Math.floor(Math.random() * 1000)
    return axios({
        method: 'get',
        url: `http://127.0.0.1:8088/abc?s=${r}`
    })
}
// 100个并发
function run() {
    const count = 100
    for (let i = 0; i < count; i++) {
        request(i)
    }
}
run()
</script>

情况如下图:此时会有大量的请求挂起 图片.png 解决的思路,是使用并发池,并发池设置一个最大值,超过最大值就等待:

function run() {
    const maxPoolsSize = 6 // 最大并发数
    const count = 100
    let pools = [] // 并发池
    for (let i = 1; i <= count; i++) {
        const task = request(i)
        task.then(response => {
            // 执行完成,从池中移除
            const idx = pools.findIndex(x => x === task)
            pools.splice(idx, 1)
        })
        pools.push(task)
        if (pools.length >= maxPoolsSize) {
            // 等待并发池执行完一个任务后
            await Promise.race(pools)
        }
    }
}

进一步封装:

class Scheduler {
  constructor(limit = 6) {
    this.limit = limit
    this.tasks = []
  }
  addTask(task, ...args) {
    this.tasks.push({
      task,
      ...args
    })
  }
  async startTask() {
    const pools = []
    for (let i = 0; i < this.tasks.length; i++) {
      let { task, ...args } = this.tasks[i]
      task = task(args)
      task.then(response => {
        // 执行完成,从池中移除
        const idx = pools.findIndex(x => x === task)
        pools.splice(idx, 1)
      })
      pools.push(task)
      if (pools.length >= this.limit) {
        // 等待并发池执行完一个任务后
        await Promise.race(pools)
      }
    }
  }
}
// 使用
const scheduler = new Scheduler()
for (let i = 0; i < 100; i++) {
  scheduler.addTask(request, i)
}
scheduler.startTask()

重复请求问题

比如:tab切换,接口返回时长不确定数据紊乱。
这个问题有两个解决方案,一个是接口数据舍弃,另一个是取消请求。
我们来看重复取消的问题:

<script setup lang="ts">
import { ref } from 'vue'
import axios from 'axios'
function defRequest(params = {}) {
  const r = Date.now() + Math.floor(Math.random() * 1000)
  return axios({
    method: 'get',
    url: `http://127.0.0.1:8088/abc?r=${r}`,
    params
  })
}
const showData = ref('')
const onClick = (id: number) => {
  defRequest({ id }).then(() => {
    showData.value = id + ''
  })
}
</script>
<template>
  <div>
    <button @click="onClick(1)">类型1</button>
    <button @click="onClick(2)">类型2</button>
    <button @click="onClick(3)">类型3</button>
    <p>数据:{{ showData }}</p>
  </div>
</template>
<style lang="scss" scoped></style>

动画.gif

我们借助Promise.race来实现

class CancelPromise {
  pendingPromise: Promise<any> | null = null
  reject: any
  request(requestFn: Promise<any>) {
    if (this.pendingPromise) {
      this.cancel('取消了请求')
    }
    const cancelPromise = new Promise((_, reject) => (this.reject = reject))
    this.pendingPromise = Promise.race([cancelPromise, requestFn])
    return this.pendingPromise
  }
  cancel(reason: string) {
    this.reject(reason)
    this.pendingPromise = null
  }
}
function defRequest(params = {}) {
  const r = Date.now() + Math.floor(Math.random() * 1000)
  return axios({
    method: 'get',
    url: `http://127.0.0.1:8088/abc?r=${r}`,
    params
  })
}
const cancelPromise = new CancelPromise()
const showData = ref('')
const onClick = (id: number) => {
  cancelPromise.request(defRequest({ id })).then(() => {
    showData.value = id + ''
  })
}

动画.gif

async/await

async/await 是Promise和生成器的语法糖。具体参考:【JavaScript】ES6之迭代器(Iterator)和生成器(Generttor) - 掘金 (juejin.cn)