从事前端开发至今,异步问题经历了 Callback Hell 的绝望,Promise/Deffered 的规范混战,到 Generator 的所向披靡,到如今 Async/Await 为大众所接受,这其中 Promise 和 Async/Await 依然活跃代码中,对他们的认识和评价也经历多次反转,也有各自的拥趸,形成了一直延续至今的爱恨情仇,其背后的思考和启发,依旧值得我们深思。
预先声明:
本文的目标并不是引发大家的论战,也不想去推崇其中任何一种方式来作为前端异步的唯一最佳实践,想在介绍下 Promise 和 Async/Await 知识和背后的趣事的基础上,探究下这些争议下隐藏的共识。
Promise 是异步编程的一种解决方案,相对于传统的回调地狱更加合理和强大。
所谓 Promise,简单来说就是一个容器,里面存储个未来才会结束的时间的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。其内部状态如下:
状态之间的流转是不可逆的,代码书写如下:
function httpPromise(): Promise<{ success: boolean; data: any }> {
return new Promise((resolve, reject) => {
try {
setTimeout(() => {
resolve({ success: true, data: {} });
}, 1000);
} catch (error) {
reject(error);
}
});
}
httpPromise().then((res) => {}).catch((error) => {}).finally(() => {});
从语法角度上看,更加容易理解,但是美中不足的就是不够简洁,无法断点,冗余的匿名函数。
在刚入行的时候也去研究过《如何实现一个 Promise》这个课题,尝试写了下如下的代码。
class promise {
constructor(handler) {
this.resolveHandler = null;
this.rejectedHandler = null;
setTimeout(() => {
handler(this.resolveHandler, this.rejectedHandler);
}, 0);
}
then(resolve, reject) {
this.resolveHandler = resolve;
this.rejectedHandler = reject;
returnthis;
}
}
function getPromise() {
return new promise((resolve, reject) => {
setTimeout(() => {
resolve(20);
}, 1000);
});
}
getPromise().then((res) => {
console.log(res);
}, (error) => {
console.log(error);
});
虽然羞耻,但是不得不说当时还是挺满足的,后面发现无法解决异步注册的问题。
const promise1 = getPromise();
setTimeout(() => {
promise1.then((data) => {
console.log(data);
}).catch((error) => {
console.error(error);
});
}, 0);
function getFPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(20), 1000);
});
}
// 执行情况 报错
// Uncaught TypeError: promise1.then(...).catch is not a function
// Uncaught TypeError: resolve is not a function
// vs 官方Promise实现
const promise2 = getFPromise();
setTimeout(() => {
promise2.then((data) => {
console.log(data);
}).catch((error) => {
console.error(error);
});
}, 0);
// 执行情况,符合预期
// 20
对于这一部分有兴趣的同学可以自行查找标准版的实现方案,不过在这个探索过程中确实勾起对基础知识的兴趣,这也是本文去挖掘这部分知识的初衷。
接下来看看 Async/Await 吧。
Async/Await 并不是什么新鲜的概念,事实的确如此。
早在 2012 年微软的 C# 语言发布 5.0 版本时,就正式推出了 Async/Await 的概念,随后在 Python 和 Scala 中也相继出现了 Async/Await 的身影。再之后,才是我们今天讨论的主角,ES 2016 中正式提出了 Async/Await 规范。
以下是一个在 C# 中使用 Async/Await 的示例代码:
public async Task<int> SumPageSizesAsync(IList<Uri> uris)
{
int total = 0;
foreach (var uri in uris) {
statusText.Text = string.Format("Found {0} bytes ...", total);
var data = await new WebClient().DownloadDataTaskAsync(uri);
total += data.Length;
}
statusText.Text = string.Format("Found {0} bytes total", total);
return total;
}
再看看在 JavaScript 中的使用方法:
async function httpRequest(value) {
const res = await axios.post({ ...value });
return res;
}
好的设计总是会想借鉴的,不寒碜。
其实在前端领域,也有不少类 Async/Await 的实现,其中不得不提到的就是知名网红之一的老赵写的 wind.js ,站在今天的角度看,windjs 的设计和实现不可谓不超前。
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。
这里引用阮一峰老师的描述:
async 函数是什么?一句话,它就是 Generator 函数的语法糖。
前文有一个 Generator 函数,依次读取两个文件。
const fs = require('fs');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
const gen = function* () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
上面代码的函数 gen
可以写成 async
函数,就是下面这样。
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
一比较就会发现,async
函数就是将 Generator 函数的星号(*
)替换成 async
,将 yield
替换成 await
,仅此而已。
相对于 Generator 的改进主要集中集中在:
到这里大家会发现,Async/Await 本质也是 Promise 的语法糖:Async 函数返回了 Promise 对象。
来看下实际 Babel 转化的代码,方便大家理解下
async function test() {
const img = await fetch('tiger.jpg');
}
// babel 转换后
'use strict';
var test = function() {
var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee() {
var img;
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case0:
_context.next = 2;
return fetch('tiger.jpg');
case2:
img = _context.sent;
case3:
case'end':
return _context.stop();
}
}
}, _callee, this);
}));
return function test() {
return _ref.apply(this, arguments);
};
}();
function _asyncToGenerator(fn) {
return function() {
var gen = fn.apply(this, arguments);
return new Promise(function(resolve, reject) {
function step(key, arg) {
try {
var info = gen[key](arg);
var value = info.value;
} catch (error) {
reject(error);
return;
}
if (info.done) {
resolve(value);
} else {
return Promise.resolve(value).then(function(value) {
step("next", value);
}, function(err) {
step("throw", err);
});
}
}
return step("next");
});
};
}
不难看出最终还是转换成基于 Promise 的调用,但是本来的三行代码被转换成 52 行代码,在一些场景下就带来了成本。
例如 Vue3 并没有采用?.(可选链式操作符符号)的原因:
虽然使用?很简洁,但是实际打包下反而更加冗余了,增加了包的体积,影响 Vue3 的加载速度,这也是 Async/Await 这类简洁语法的痛点。
暂时不考虑深层次的运行性能,仅仅考虑代码使用方式来看,Async/Await 是否完美无缺?
以 “请求 N 次重试” 的实现为例:
/**
* @description: 限定次数来进行请求
* @example: 例如在5次内获取到结果
* @description: 核心要点是完成tyscript的类型推定,其次高阶函数
* @param T 指定返回数据类型,M指定参数类型
*/
export default function getLimitTimeRequest<T>(task: any, times: number) {
// 获取axios的请求实例
let timeCount = 0;
async function execTask(resolve, reject, ...params: any[]): Promise<void> {
if (timeCount > times) {
reject(newError('重试请求失败'));
}
try {
const data: T = await task(...params);
if (data) {
resolve(data);
} else {
timeCount++;
execTask(resolve, reject, params);
}
} catch (error) {
timeCount++;
execTask(resolve, reject, params);
}
}
return function <M>(...params: M[]): Promise<T> {
return new Promise((resolve, reject) => {
execTask(resolve, reject, ...params);
});
};
}
常见的实现思路是将 Promise 的 Resolve、Reject 的句柄传递到迭代函数中,来控制 Promise 的内部状态转化,那如果用 Async/Await 如何做?很明显并不好做,暴露了它的一些不足:
当然,站在 EMCA 规范的角度来看,有些需求可能比较少见,但是如果纳入规范中,也可以减少前端程序员在挑选异步流程控制库时的纠结了。
针对前端异步的处理方案,Promise 和 Async/Await 都是优秀的处理方案,但是美中不足的是有一定的不足,随着前端工程化的深入,一定会有更好的方案来迎合解决这些问题,大家不要失望,未来还是可期的。
从 Promise 和 Async/Await 的演进和纠结中,大家实际能够感到前端人对 JavaScript 世界的辛苦耕作和奇思妙想,这种思维和方式也可以沉淀到我们日常的需求开发中去,善于求索,辩证的去使用它们,追求更加极致的方案。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8