持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第11天,点击查看活动详情
导读
本文旨在通过几种实例分析,探索 异步函数 中 值得提点的问题 和 异步函数 程序设计技巧
1. 异步等待
什么是 异步等待 思想?
在解释这个问题之前,我们必须得详细了解 异步的回调函数 究竟解决了什么问题
首先看这么一个例子:
function delayConsole(prevCb: Function, nextCb: Function) {
prevCb();
nextCb();
}
delayConsole(
() => console.log(1),
() => {
delayConsole(
() => console.log(2),
() => {
delayConsole(
() => console.log(3),
() => {}
);
}
);
}
);
// 1 2 3
我们定义一个函数 delayConsole,这个函数的功能只是确保了两个 回调函数 执行的 先后顺序
这真的能确保回调函数的先后执行顺序吗?
如果我们在这个例子上简单的修改一下,在 console.log(2) 外侧包裹一个定时器
delayConsole(
() =>
setTimeout(() => { // 修改的代码
console.log(2);
}),
() => {
// ...
}
);
// 1 3 2
为什么会出现这种 乱序 的这种情况,不妨把代码拆解的简单一点
console.log(1);
setTimeout(() => {
console.log(2);
});
console.log(3);
// 1 3 2
这样是否能明白呢?由于 js 存在 异步机制,导致代码很难按照书写的顺序执行,但是在很多场景下,必须要确定一个先后执行顺序,那我们怎么才能确保执行顺序呢?
function delayConsole(prevCb: Function, nextCb: Function) {
setTimeout(() => {
prevCb();
nextCb();
});
}
// 1 2 3
只要给 delayConsole 添加一个 异步执行 的过程,就可以确保代码的执行顺序,为什么这样可以确保执行顺序呢?
代码简化之后结果
setTimeout(() => { // delayConsole
console.log(1);
setTimeout(() => { // delayConsole
setTimeout(() => {
console.log(2);
});
setTimeout(() => { // delayConsole
console.log(3);
(() => {})();
});
});
});
// 1 2 3
那么最内层的两个定时器中的回调函数就可以当做在一个 执行域 执行,这样就能确保代码执行顺序
那我们再给 console.log(2) 包裹一层定时器
setTimeout(() => {
setTimeout(() => { // 修改的代码
console.log(2);
});
});
// 1 3 2
输出结果又会乱序了,那我们又怎么解决呢?
我们依然再给 delayConsole 添加一个 异步执行 的过程,就可以解决了,这又是为什么呢
function delayConsole(prevCb: Function, nextCb: Function) {
setTimeout(() => {
setTimeout(() => {
prevCb();
nextCb();
});
});
}
代码简化过后的结果:
setTimeout(() => { // delayConsole
setTimeout(() => { // delayConsole
console.log(1);
setTimeout(() => { // delayConsole
setTimeout(() => { // delayConsole
setTimeout(() => { // ----------- 合并
setTimeout(() => {
console.log(2);
});
});
setTimeout(() => { // delayConsole // ----------- 合并
setTimeout(() => { // delayConsole
console.log(3);
(() => {})();
});
});
});
});
});
});
这时候,最内层的两个定时器包裹定时器的可以合并成一个 执行域
这时候,你就会发现一个很有意思的现象,随着异步任务嵌套的增多,我们 确保异步任务执行顺序 的方法是让后面的任务也嵌套异步任务,达到 相对同步 的效果
这种程序设计思想叫做 异步等待 思想,后面的同步任务 需要等待到 前面的异步任务 执行完成之后才能执行,通过用 异步任务 包裹 同步任务 等待一个执行时机,达到相对同步的效果。
所以异步究竟解决了什么问题:
- 对于
js解析引擎来说,避免耗时任务 阻塞 线程 - 对于开发者来说,某些代码执行依赖不可能避免的异步任务的执行结果,存在一个依赖关系和代码先后执行顺序的问题,我们可以利用 手动构造异步环境 解决这个问题
关于异步等待的实际应用:可以参考这篇文章《Promise 实战篇:处理一个页面中多个回车事件的解决方案》
发布订阅 Promise
在上个例子中,我们通过使用 异步等待 的思想,解决乱序的问题,但是会带来很多弊端:
- 随着异步任务嵌套的增多,我们得不停地包裹 定时器
- 代码陷入 回调地狱
- 如果需要等待时间不可控的异步任务(网络请求,读写文件),这时候就无法手动控制 异步等待 时间了
那么最好的解决办法就是 Promise:
Promise.resolve()
.then(() => {
return new Promise(resolve => {
console.log(1);
resolve('完成');
});
})
.then(() => {
return new Promise(resolve => {
setTimeout(() => {
setTimeout(() => {
setTimeout(() => {
setTimeout(() => {
console.log(2);
resolve('完成');
});
});
});
});
});
})
.then(() => {
return new Promise(resolve => {
console.log(3);
resolve('完成');
});
});
// 1 2 3
在这个例子中,使用 then() return Promise 的方法,做到对代码的分隔
每当前一个任务执行完毕,则会立即发送一个通知,命令下个任务的开始,所以后一个任务订阅前一个任务执行完毕的命令,达到控制异步顺序的流程
如果想使用 Promise 对于异步程序顺序的管理,首先 必须 得使用这样的结构
p2 = p1.then(() => {
return new Promise((resolve) => {
// ...
resolve('完成了')
});
})
如果不 return 新的 Promise 实例,则会导致 p2 获取不到 resolve 的信息,尽管最后 then() 会对返回值进行 Promise.resolve() 包装。这样就实现了一个 发布订阅 的模型
异步函数封装
每次 new Promise无疑给我们增大了代码量,所以我们得创建一个生产 Promise 的工厂函数
type Done = () => void;
const delayConsole = (fn: (done: Done) => any) => {
return new Promise(resolve => {
fn(resolve as Done);
});
};
我们首先定义了一个函数 delayConsole,这个函数的作用是创造 Promise 实例,不仅如此,还接收一个回调函数,这个回调函数暴露出 done 的执行命令,在回调函数中调用 done,就表示传递开始执行下一个任务的信息
就像下面的例子表达的一样:
const fn1 = (done: Done) => {
console.log(1);
done();
};
const fn2 = (done: Done) => {
setTimeout(() => {
setTimeout(() => {
console.log(2);
done();
});
});
};
const fn3 = (done: Done) => {
console.log(3);
done();
};
(async function () {
for (const fn of [fn1, fn2, fn3]) {
await delayConsole(fn);
}
})();
// 1 2 3
我们在调用 fn1 fn2 fn3 使用 async~await 和 迭代 的方式简化 Promise 操作
总结
到此我们学会了管理异步顺序的两种方式:
- 异步等待
- 发布订阅
其中 发布订阅 结合 Promise 的方法更具有封装性,使得我们的程序可以复用。
结语
如果觉得本文不错的话,可以给作者点一个小小的赞,你的鼓励将是我前进的动力
如果本文对你有帮助或者你有不同的意见,欢迎在评论区留下你的足迹
Promise 相关文章推荐
- promise 中的细节:
《其实你不知道 Promise.then 》
《你真的明白 promise 的 then 吗?》
《await 中那些不为人知的细节》 - 专栏:
《从 v8 看 promise》