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);
})
其实处理和封装异步关键就在于两个问题
- 定义异步执行的内容。比如发送一个网络请求,设置一个定时器,读取一个文件的内容等等
- 捕获到异步内容执行结束的时期。比如上述的请求返回数据的时期,定时器结束的时期,文件读取完毕的时期,综上所述的时期其实就是触发回调的时机
当通过 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 很多人都会用,但要注意几个非常重要的点:
- await 同一行后面的内容对应 Promise 主体内容,即同步执行的
- await 下一行的内容对应 then()里面的内容,是异步执行的
- await 同一行后面应该跟着一个 Promise 对象,如果不是,需要转换(如果是常量会自动转换)。
- 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 秒到了')
}