ES6 Promise 最佳实践介绍

对于一些复杂的代码模块,不够熟悉的话就会感觉比较绕。比如以下这些实际应用中的经验。

异步Promise的关键两点

在业务实际使用的过程中,我们会尽量的把所有的异步操作都封装成Promise,方便其他地方直接调用,摈弃了过往的陈旧callback写法,比如我们封装了一个类 classA,里面需要有一些准备工作才能被外界使用,以前我们可能会提供 ready(callback) 方法,那么现在就可以这样 ready().then()。

另外,在实例开发中,我们应当尽量把new Promise封装到内部底层,而不是在业务层面再实例化进行操作,异步工具类和axios请求之类的都应该在底层做好开发

const func = () => {
    const promise = new Promise((resovle, reject) => {
        //  还没有导入axios包,用异步的计时器替代也是一样的
        setTimeout(() => {
            console.log('模拟axios请求');
            resovle('succuess');
        }, 1000)
    })
    return promise;
}

// 模拟业务层使用
func().then((res) => {
    console.log(res);
}).catch((err) => {
    console.log(err);
})

其实处理和封装异步关键就在于两个问题

  1. 定义异步执行的内容。比如发送一个网络请求,设置一个定时器,读取一个文件的内容等等
  2. 捕获到异步内容执行结束的时期。比如上述的请求返回数据的时期,定时器结束的时期,文件读取完毕的时期,综上所述的时期其实就是触发回调的时机

当通过 new Promise 初始化实例的时候,就定义了异步任务的执行内容,即 Promise 主体。然后 Promise 给我们两个函数 resolve 和 reject 来让我们明确指出任务结束的时机,也就是告诉 Promise 执行的内容和结束的时机就行了,不用像 callback 那样,需要把处理过程也嵌套写在里面,而是在原来 callback 的地方调用一下 resolve(成功)或 reject(失败)来标识任务结束了。

在实际开发中,不管业务模块或者老代码多么复杂,只需要抓住上述两点去进行改造,就能正确地将所有异步代码进行 Promise 化。所有异步甚至同步逻辑都可以 Promise 化,只要抓住 任务内容任务结束时机这两点就可很清晰的来完成封装。

如何避免冗余封装?

现在有很多的库已经开始支持Promise实例,并且提供了一定的TS类型支持,在使用前请务必查清楚文档再做封装,有的库既支持 callback 形式,也支持 Promise 形式。
冗余封装的经典例子就比如下面的axios:

const getData = () => {
  return new Promise((resolve) => {
    axios.get(url).then((data) => {
      resolve(data)
    })
  })
}

另一个案例就是,有时我们会需要构建微任务或者将同步执行的结果数据,以 Promise 的形式返回给业务,会容易写成下面的冗余写法:

const getData = () => {
  return new Promise((resolve) => {
    const a = 1;
    const b = 2;
    const c = a + b;
    resolve(c);
  })
}

实际上我们完全可以用Promise.resolve快速构建一个Promise 对象来优化这个冗余的封装:

const getData = () => {
  const a = 1;
  const b = 2;
  const c = a + b;
  return Promise.resolve(c);
}

异常处理

在上一篇文章Promise基础里,我介绍了尽量通过 catch() 去捕获 Promise 异常,需要说明的是,一旦被 catch 捕获过的异常,将不会再往外部传递,除非在 catch 中又触发了新的异常。

有个很经典的案例,如下面代码,第一个异常被捕获后,就返回了一个新的 Promise,这个 Promise 对象没有异常,将会进入后面的 then() 逻辑:

const p = new Promise((resolve, reject) => {
  reject('异常啦'); // 或者通过 throw new Error() 跑出异常
}).catch((err) => {
  console.log('捕获异常啦'); // 进入
}).catch(() => {
  console.log('还有异常吗'); // 不进入
}).then(() => {
  console.log('成功'); // 进入
})

如果 catch 里面在处理异常时,又发生了新的异常,将会继续往外冒,这个时候我们不可能无止尽的在后面添加 catch 来捕获,所以 Promise 有一个小的缺点就是最后一个 catch 的异常没办法捕获(当然实际出现异常的可能性很低,基本不造成什么影响)。

async await

我们可以通过这组async await在实例使用中派上大用场,配合Promise的使用,使代码的可读性大大的提高,”回调“这个东西也就再也没有了他的踪影

const getDataOne = async () => {
    const data = await axios.get(url);
    return data;
  }
  // 等效于
  const getDataTwo = () => {
    return axios.get(url).then((data) => {
      return data
    });
  }

对 async await 很多人都会用,但要注意几个非常重要的点:

  1. await 同一行后面的内容对应 Promise 主体内容,即同步执行的
  2. await 下一行的内容对应 then()里面的内容,是异步执行的
  3. await 同一行后面应该跟着一个 Promise 对象,如果不是,需要转换(如果是常量会自动转换)。
  4. async 函数的返回值还是一个 Promise 对象

我们再举一个例子:

async function getData() {
  // await 不认识后面的 setTimeout,不知道何时返回
  const data = await setTimeout(() => {
    return;
  }, 3000)
  console.log('3 秒到了')
}

正确写法:

async function getData() {
  const data = await new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, 3000)
  })
  console.log('3 秒到了')
}
最后修改:2023 年 02 月 22 日
收款不要了,给孩子补充点点赞数吧