# Promise

# 基本用法

Promise 的简单封装与使用

// 封装
function 摇色子() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(Math.floor(Math.random() * 6) + 1)
    }, 3000)
  })
}
// 使用
摇色子().then(success1, failed1).then(success2, failed2)

# Ma Mi 任务模型

  • Ma 指 MacroTask (宏任务),Mi 指 MicroTask (微任务)
  • 先 Ma 再 Mi,即先执行宏任务再执行微任务
  • JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务
  • 其实最初 JS 只存在一个任务队列,为了让 Promise 回调更早执行,强行又插入了一个异步的任务队列,用来存放 Mi 任务
  • 宏任务:setTimeout ()、setInterval ()、 setImmediate ()、 I/O、UI 渲染(常见的定时器,用户交互事件等等)
  • 微任务:Promise、process.nextTick、Object.observe、MutationObserver

# Promise 的其他 API

# Promise.resolve(result) : 制造一个成功(或失败)

制造成功

function 摇色子() {
  return Promise.resolve(4)
}
// 等价于
function 摇色子() {
  return new Promise((resolve, reject) => {
    resolve(4)
  })
}
摇色子().then(n => console.log(n)) // 4

制造失败

function 摇色子() {
  // 此处 Promise.resolve 接收的是一个失败的 Promise 实例(状态为 reject)
  return Promise.resolve(new Promise((resolve, reject) => reject()))
}
摇色子().then(n => console.log(n)) // 1 Uncaught (in promise) undefined

关于 Promise.resolve 接收参数的问题,ECMAScript 6 入门里其实说得很清楚

如果参数是 Promise 实例,那么 Promise.resolve 将不做任何修改、原封不动地返回这个实例;如果参数是一个原始值,或者没有参数, Promise.resolve 都会直接返回一个 resolved 状态的 Promise 对象。

# Promise.reject(reason) : 制造一个失败

Promise.reject('我错了')
// 等价于
new Promise((resolve, reject) => reject('我错了'))
Promise.reject('我错了').then(null, reason => console.log(reason)) // 我错了

# Promise.all(数组) : 等待全部成功,或者有一个失败

全部成功,将所有成功 promise 结果组成的数组返回

Promise.all([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)])
  .then(values => console.log(values)) // [1, 2, 3]

只要有一个失败,就结束,返回最先被 reject 失败状态的值

Promise.all([Promise.reject(1), Promise.resolve(2), Promise.resolve(3)])
  .then(values => console.log(values)) // Uncaught (in promise) 1

Promse.all 在需要对多个异步进行处理时往往非常有用;

不过在某些特殊情况下,直接使用 Promse.all 就显得不那么方便了

举个例子,比如现在有 3 个请求,request1, request2 和 request3,我们需要对这 3 个请求进行统一处理,并且不管请求成功还是失败,都需要拿到所有的响应结果,如果这时候使用 Promise.all([request1, request2, request3]) 的话,request1 请求失败了,后面的两个请求 request2, request3 就都不会执行了(这里实际上是 request1 在 rejected 之后,被 Promise.all([]).catch 给捕获了
)。

如何解决 Promise.all() 在第一个 Promise 失败就会中断的问题?

利用 .then() 后会返回一个状态为 resolved 的 Promise(即会自动包装成一个已 resolved 的 promise),从而避免被 Promise.all([]).catch 捕获

// 3 个请求
const request1 = () => new Promise((resolve, reject) => {
 setTimeout(() => {
   reject('第 1 个请求失败')
 }, 1000)
})
const request2 = () => new Promise((resolve, reject) => {
 setTimeout(() => {
   reject('第 2 个请求失败')
 }, 2000)
})
const request3 = () => new Promise((resolve, reject) => {
 setTimeout(() => {
   resolve('第 3 个请求成功')
 }, 3000)
})
Promise.all([
  request1().then(value => ({ status: 'ok', value }), reason => ({ status: 'not ok', reason })),
  request2().then(value => ({ status: 'ok', value }), reason => ({ status: 'not ok', reason })),
  request3().then(value => ({ status: 'ok', value }), reason => ({ status: 'not ok', reason }))
]).then(result => console.log(result))

可以把对每个请求的 .then 操作封装一下

const x = promiseList => promiseList.map(promise => promise.then(value => ({
  status: 'ok',
  value
}), reason => ({
  status: 'not ok',
  reason
})))
const xxx = promiseList => Promise.all(x(promiseList))
xxx([request1(), request2(), request3()])
  .then(result => console.log(result))

打印结果如下:

image.png

# Promise.allSettled(数组) : 等待全部状态改变

Promise.allSettled([Promise.reject(1), Promise.resolve(2), Promise.resolve(3)])
  .then(result => console.log(result))

打印结果如下:

image.png

可以看出 Promise.allSettled 的作用其实和上面我们实现的 xxx 函数的作用是一致的,因此针对上文提到场景,可以直接使用 Promise.allSettled ,更加简洁。

# Promise.race(数组) : 等待第一个状态改变

Promise.race([request1(), request2(), request3()]).then((result) => {
  console.log(result)
}).catch((error) => {
  console.log(error) // 第 1 个请求失败
})

Promise.race([request1, request2, request3]) 里面哪个请求最先响应,就返回其对应的结果,不管结果本身是成功状态还是失败状态(这里最先响应的请求是 request1)。

一般情况下用不到 Promise.race 这个 api,不过在某些场景下还是有用的。例如在多台服务器部署了同样的服务端代码,要从一个商品列表的接口拿数据,这时候就可以在 race 中写上所有服务器中的查询商品列表的接口地址,哪个服务器响应快,就优先从哪个服务器拿数据。

# Promise 的应用场景

# 多次处理一个结果

摇色子().then(v => v1).then(v1 => v2)

第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。

# 串行

  • 这里有一个悖论:一旦 promise 出现,那么任务就已经执行了
  • 所以不是 promise 串行,而是任务串行
  • 解法:把任务放进队列,完成一个再做下一个(用 Reduce 实现 Promise 串行执行

# 并行

Promise.allPromise.allSettledPromise.race 都可以看作是并行地在处理任务

这里可能你会产生疑问,JS 不是单线程吗,怎么做到并行执行任务?

这里指的是并行地做网络请求的任务,而网络请求实际上是由浏览器来做的,并非是 JS 做的,就像 setTimeout 是浏览器的功能而不是 JS 的,setTimeout 只是浏览器提供给 JS 的一个接口。

# Promise 的错误处理

# 自身的错误处理

promise 自身的错误处理其实挺好用的,直接在 .then 的第二个回调参数中进行错误处理即可

promise.then(s1, f1)

或者使用 .catch 语法糖

// 上面写法的语法糖
promise.then(s1).catch(f1)

建议总是使用 catch() 方法,而不使用 then() 方法的第二个参数,原因是第二种写法可以捕获前面 then 方法执行中的错误,也更接近同步的写法( try/catch

# 全局错误处理

以 axios 为例,Axios 作弊表

# 错误处理之后

  • 如果你没有继续抛错,那么错误就不再出现
  • 如果你继续抛错,那么后续回调就要继续处理错误

# 前端似乎对 Promise 不满

Async/Await 替代 Promise 的 6 个理由,主要是以下 6 个方面:

  • 简洁
  • 错误处理
  • 条件语句
  • 中间值
  • 错误栈
  • 调试(在 .then 代码块中设置断点,使用 Step Over 快捷键,调试器不会跳到下一个 .then ,因为它只会跳过异步代码)

# async / await

# async /await 基本用法

最常见的用法

const fn = async() => {
  const temp = await makePromise()
  return temp + 1
}

优点:完全没有缩进,就像是在写同步代码

# 封装一个 async 函数

async 的封装和使用

function 摇色子() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(Math.floor(Math.random() * 6) + 1)
    }, 3000)
  })
}
async function fn() {
  const result = await 摇色子()
  console.log(result)
}
fn()

try...catch 进行错误处理

async function 摇色子() {
  throw new Error('色子坏了')
}
async function fn() {
  try {
    const result = await 摇色子()
    console.log(result)
  } catch (error) {
    console.log(error)
  }
}
fn()

# 为什么需要 async

在函数前面加一个 async ,这看起来非常多余, await 所在的函数就是 async ,不是吗?

理由之一:

在 ES 标准的 async/await 出来之前,有些人自己用函数实现了 await,为了兼容旧代码里普通函数的 await (xxx)(为了将旧代码里面的 await 和新的 ES 标准里的 async/await 区分开来),其实 async 本身并没有什么意义。

你可能会说, async 函数会隐式地返回一个 Promise 对象呀,但这并不能成为必须要在函数前加 async 的理由,有兴趣的可以去看看知乎上关于 async 的讨论。

  • 为什么 js 里使用了 await 的方法必须定义成 async 的?
  • C# 中,async 关键字到底起什么作用?

# await 错误处理

用 try/catch 来同时处理同步和异步错误是很常见的做法

let response
try {
  response = await axios.get('/xxx')
} catch (e) {
  if (e.response) {
    console.log(e.response.status)
    throw e
  }
}
console.log(response)

但其实还有更好的写法,就像下面这样

const errorHandler = error => {
  console.log(error)
  // 注意这里要抛出一个错误
  throw error
  // 或者 return Promise.reject (error),注意:一定要 return
}
// 只用一句代码就可以处理成功和失败
const response = await axios.get('/xxx').then(null, errorHandler)
// 或者使用 catch 语法糖
const response = await axios.get('/xxx').catch(errorHandler)

需要注意的是, errorHandler 函数中不要直接 return 一个值,一定要抛出一个错误(打断程序的运行)。因为在请求调用失败的情况下,会把 errorHandlerreturn 的值直接赋值给 response(通俗的说法就是 “Promise 会吃掉错误”),在 errorHandler 中抛出一个错误能够保证在请求成功的情况下才会有 response,请求失败的情况下一定是会进入 errorHandler 函数中的

下面是一个实际的例子

const ajax = function() {
  return new Promise((resolve, reject) => {
    reject('这是失败后的提示')
    //resolve (' 这是成功后的结果 ')
  })
}
const error = (error) => {
  console.log('error:', error)
  return Promise.reject(error)
}
async function fn() {
  const response = await ajax().then(null, error)
  console.log('response:', response)
}
fn()

可以看到,我们仅仅只用了一句代码就可以同时处理 Promise 成功和失败的情况了,绝大多数的 ajax 调用都是可以用这样的方式来处理的。

所以,对于 async/await ,并不是一定需要使用 try/catch 来做错误处理的。

之前我常常陷入一个误区:就是认为 await.then 是对立的,始终觉得用了 await 后就不应该再出现 .then

但其实并非如此,说到底 async/await 也只不过是 .then 的语法糖而已。就像上面的例子一样, .thenawait 完全是可以结合在一起使用的,在 .then 中进行错误处理,而 await 左边只接受成功结果。

另外,我们还可以把 4xx/5xx 等常见错误用拦截器全局处理, errorHandler 也可以放在拦截器里。

# await 的传染性

代码:

console.log(1)
await console.log(2)
console.log(3) //await 会使这句代码变成异步的,如果想要让他立即执行,放到 await 前面即可

分析:

  • await 会使得所有它左边的和下面的代码变成异步代码
  • console.log(3) 变成异步任务了
  • Promise 同样有传染性(同步变异步),放到 .then 回调函数中的代码会变成异步的,不过相比于 await.then 下面的代码并不会变成异步的
  • 回调没有传染性

# await 的应用场景

# 多次处理一个结果

const r1 = await makePromise()
const r2 = handleR1(r1)
const r3 = handleR2(r2)

# 串行

天生串行(多个 await 并排时,从上到下依次执行,后面的会等前面执行完了再执行)

await promise1
await promise2
await promise3
...

# 并行

同 Promise, await Promise.all([p1, p2, p3])await Promise.allSettled([p1, p2, p3])await Promise.race([p1, p2, p3]) 都是并行的

# 循环的时候存在 bug

正常情况下,即便在循环中, await 也应当是串行执行的。

例如 for 循环中的 await 是串行的(后面等前面)

async function runPromiseByQueue(myPromises) {
  for (let i = 0; i < myPromises.length; i++) {
    await myPromises[i]();
  }
}
const createPromise = (time, id) => () =>
  new Promise((resolve) =>
    setTimeout(() => {
      console.log("promise", id);
      resolve();
    }, time)
  );
runPromiseByQueue([
  createPromise(3000, 4),
  createPromise(2000, 2),
  createPromise(1000, 1)
]);
// 4 2 1

但是在某些循环中,如 forEach 和 map 中,await 会并行执行(后面不等前面)

async function runPromiseByQueue(myPromises) {
  myPromises.forEach(async (task) => {
    await task();
  });
}
const createPromise = (time, id) => () =>
  new Promise((resolve) =>
    setTimeout(() => {
      console.log("promise", id);
      resolve();
    }, time)
  );
runPromiseByQueue([
  createPromise(3000, 4),
  createPromise(2000, 2),
  createPromise(1000, 1)
]);
// 1 2 4

后面 JS 又出了一个新的东西 for await...of 来弥补这个 bug

async function runPromiseByQueue(myPromises) {
  // 异步迭代
  for await (let item of myPromises) {
    console.log('promise', item);
  }
}
const createPromise = (time, id) =>
  new Promise((resolve) =>
    setTimeout(() => {
      resolve(id);
    }, time)
  );
runPromiseByQueue([
  createPromise(3000, 4),
  createPromise(2000, 2),
  createPromise(1000, 1)
]);
// 4 2 1

# Reference

  • 异步操作概述
  • 事件循环:微任务和宏任务
  • Event Loop、计时器、nextTick
  • 理解和使用 Promise.all 和 Promise.race
  • For await of
更新于