Promise、async和await在业务代码中的实战和踩坑

25 阅读5分钟

一、前置知识

语法和基础用法:深入解析Promise与async/await的核心区别

二、在项目中使用

  • 封装请求时:
// @/utils/request.js 
import axios from "axios";
export function requestFn(options) {
  return axios({
    method: options.method || "get",
    url: options.url,
    baseURL: import.env.BASE_API,
    headers: options.headers,
    timeout: 200000,
    data: options.data || "",
    params: options.params || "",
    responseType: options.responseType || "",
  })
    .then((response) => {
      const res = response.data;
      if (res.status !== 200) {
        return Promise.reject(res.message);
        // 或者 throw new Error(res.message);
      }
      // 在then里return普通值,会自动包装成 Promise.resolve,所以没必要手写Promise.resolve(res)
      return res;
    })
    .catch((error) => {
      throw error; 
    });
}
  • 封装业务函数:
import { requestFn } from "@/utils/request";
// 调用:requestFn 得到一个Promise实例并返回
export const getAllSwiper = () => requestFn({ url: API.getAllSwiper });
  • 在组件.vue 中调用业务函数:
// async/await 调用(推荐)
async yeWuFn1 () {
  try {
    const res = await getAllSwiper();
    this.imageUrl = res.data;
  } catch (err) {
  }
};

// 也可用Promise的链式调用(.then/.catch),但多层then嵌套易形成回调地狱
yeWuFn1 () {
  getAllSwiper()
    .then((res) => {
      this.imageUrl = res.data;
    })
    .catch((err) => {});
};
// 回调地狱
a().then(res => {
  b().then(res => {
    c().then(...)
  })
})
// 正确链式调用
a()
  .then(() => b())
  .then(() => c())

三、async函数和.then()的返回值

已知async/await 是基于 Promise 的语法糖

1、async函数返回promise对象

即使

async foo() {}

也等价于:

function foo() {
  return Promise.resolve(undefined)
}

❌ 错误写法1:

async getsfxFn() {
  getsfxDetail().then(res => {
    this.sfxList = res.data
  })
}
export function getsfxDetail() {
  return requestFn({
    url: "xxx",
  }).then((res) => {
    return res;
  });
}

这里getsfxFn虽然是async函数,但其中没有显式return一个promise,所以等价于:

async getsfxFn() {
  getsfxDetail().then(res => {
    this.sfxList = res.data
  })
  return Promise.resolve(undefined)
}
// 此时
console.log(await getsfxFn()) 为 undefined

❌ 错误写法2:

getsfxFn() {
  getsfxDetail().then(res => {
    this.sfxList = res.data
  })
}
普通函数默认返回 undefined,使用 await getsfxFn()时,await完全无效

✅ 正确写法

// 写法1:这里getsfxFn是普通函数,返回必须是promise对象才能使用await
getsfxFn() {
  return getsfxDetail().then(res => {
    this.sfxList = res.data
  })
}
// 或者写法2(推荐)
async getsfxFn() {
  const res = await getsfxDetail()
  this.sfxList = res.data
  // 这里async函数会自动 return Promise.resolve(undefined)
}
// 调用:
await getsfxFn()

2、p.then()返回一个新的promise

为了支持promise的链式调用,必须返回新promise,promiseObj.then() !== promiseObj

所以就算.then() 里显式return普通值,也会自动进行包装成promise

上面举例这两种正确写法都:

  • 返回一个 Promise
  • 等待 getsfxDetail 完成
  • 外部可以 await 它
  • 执行顺序一致

为什么等价?

因为正确写法1中:getsfxFn() 返回的是 then 返回的 newPromise 实际相当于:

const promiseObj = getsfxDetail()

const newPromiseObj = promiseObj.then(res => {
  this.sfxList = res.data
})

// 这个newPromiseObj是一个先 pending 后 fulfilled(undefined) 的 Promise,最终resolve(undefined)
return newPromiseObj 

四、❗new Promise 常见 3 大坑

  • 踩坑1:多此一举 + 风险翻倍的反模式:Promise 包 Promise
  • 踩坑2:忘记 resolve/reject
  • 踩坑3:executor 里同步抛错会自动 reject,但executor的异步回调里的抛错,如果不处理,会导致 pending:

executor(执行器函数):

new Promise(executor)

executor = function(resolve, reject) {}

executor 的特点:

① 创建 Promise 时立即执行(同步执行)

console.log("A")
new Promise(() => {
  console.log("B")
})
console.log("C")
依次输出:A->B->C

② 只能自动捕获同步异常

executor 里同步抛错会自动 reject:

new Promise(() => {
  throw Error("boom")
})
等价于:
new Promise((resolve, reject) => {
  reject(Error("boom"))
})

③ 不能自动捕获executor 内的异步异常

executor的异步回调里的抛错,需要手动调用 resolve/reject,否则会导致 pending:

new Promise((resolve, reject) => {
  setTimeout(() => {
    throw Error("boom")
  })
})
// 因为没有手动调用 resolve/reject,所以同样会 pending —— 即使没抛错
new Promise((resolve, reject) => {
  setTimeout(() => {
     console.log(1)
   })
})
//根本原因是异步回调setTimeout 已经脱离 Promise executor 同步作用域

一般是这么几种:

  1. 当前Promise实例接着进行 then/catch 回调里的异常,then 回调里的异常属于then 返回的新 Promise,不是原 Promise

  2. 当前Promise的executor内的异步异常,比如定时器或者调用接口

  3. 事件回调里的异常

举例:

// 能自动捕获:
new Promise(() => {
  throw Error()
})
// 不能自动捕获:这个异常属于 then 链上的 Promise,不是外层 Promise
new Promise(resolve => {
  resolve()
}).then(() => {
  throw Error()
})

实战举例说明:

在项目里封装请求方法时:

因为axios 本身就已经返回 Promise,默认有 timeout/错误处理

  • 不推荐
new Promise((resolve, reject) => {
  somePromise.then(resolve).catch(reject);
});

如果非要这么写,那么要确保 requestFn()得到的Promise 不会出现永远 pending的情况

❗ 注意: new Promise 写法 = 依赖你手动调用 resolve/reject

所以要求:

✔ 有 timeout

✔ 有 .catch

✔ 所有if else 分支都有 resolve/reject

进行如下封装:

export function requestFn(options) {
  return new Promise((resolve, reject) => {
    axios({
      method: options.method || "get",
      url: options.url,
      baseURL: store.state.baseUrl,
      headers: options.headers,
      timeout: 200000, // 设置请求超时时间
      data: options.data || "",
      params: options.params || "",
      responseType: options.responseType || "",
    })
      .then((response) => {
        if (response.status === 200 || response.status === 204) {
          const res = response.data;
          if (res.code === 200) {
            resolve(res);
          } else {
            reject(res);
          }
        } else {
          reject(response.data);
        }
      })
      .catch((error) => {
        reject(error);
      });
  });
}

❗千万注意在使用的时候,不要在业务层再 new Promise了!!

比如:

getList() {
  return new Promise((resolve, reject) => {
    requestFn({
      url: "xxx",
      params: {},
    }).then((res) => {
      const data = res?.data || {}
      this.bookdirObj = this.changeBookdir(data.bookdir)
      resolve(data)
    }).catch((err) => {
      resolve({})
    });
  })
}
  • 一旦const bookdirObj = this.changeBookdir(bookdir)这里报错:
  • → resolve/reject 都没执行,导致Promise 的 pending 状态没有变为 fulfilled/rejected 状态
  • 那么getList()得到一个永远pending的Promise

这个异常是requestFn 返回的 Promise 链.then 回调里的同步异常,发生在 new Promise 的 executor 触发的异步链中

不属于new Promise的executor 里的同步抛错,所以 new Promise 没有捕获

✔ 改成:

getList() {
    return requestFn({
      url: "xxx",
      params: {},
    }).then((res) => {
      const data = res?.data || {}
      this.bookdirObj = this.changeBookdir(data.bookdir)
      return data
    }).catch((err) => ({}));
}

这种写法不会 pending 的原因:这里没有new Promise

requestFn()返回的Promise实例,接着进行then 回调,那么Promise 链的规则:

  • then回调里抛错:
  • → Promise 自动 rejected
  • → 进入 catch
  • → 返回 {}:普通值{},会自动包装成 Promise resolve({})返回
  • getList()得到resolve({})

✔ 更推荐用async/await:

async getList() {
  try {
    const res = await requestFn({...})
    const data = res?.data || {}
    try {
      this.bookdirObj = this.changeBookdir(data.bookdir)
    } catch(e) {
      this.bookdirObj = null
    }
    return data
  } catch(e) {
    return {}
  }
}

pending 那咋了?

❗危害1:用Promise.all的时候必须注意:只要有某个 Promise 永远 pending,会导致Promise.all 卡死而永远等待,影响后续代码执行

❗危害2:如果 pending Promise 被长期引用,可能导致相关闭包变量无法释放,从而产生内存占用增长

❗危害3:loading 状态永远不结束

loading = true
await somePendingPromise
loading = false // 永远执行不到

五、resolve(res)和promise.resolve(res)的区别

  • resolve(res)只能在 executor 里写:用于把当前这个 Promise变成 fulfilled
new Promise((resolve) => {
  resolve(res)
})
  • Promise.resolve(res) 创建一个新的已完成 Promise
new Promise((resolve) => {
  resolve(123)
}) 
// 非严格意义上可以看做等价于:
Promise.resolve(123)

============分割线============

写到最后几乎不认识Promise了,感觉我仍然处于表面理解的状态,后续碰到了其它情况再来补充