Async:简洁优雅的异步之道
前言
在异步处理方案中,目前最为简洁优雅的便是 async
函数(以下简称A函数)。经过必要的分块包装后,A函数能使多个相关的异步操作如同同步操作一样聚合起来,使其相互间的关系更为清晰、过程更为简洁、调试更为方便。它本质是 Generator
函数的语法糖,通俗的说法是使用G函数进行异步处理的增强版。
尝试
学习A函数必须有 Promise
基础,最好还了解 Generator
函数,有需要的可查看延伸小节。
为了直观的感受A函数的魅力,下面使用 Promise
和A函数进行了相同的异步操作。该异步的目的是获取用户的留言列表,需要分页,分页由后台控制。具体的操作是:先获取到留言的总条数,再更正当前需要显示的页数(每次切换到不同页时,总数目可能会发生变化),最后传递参数并获取到相应的数据。
let totalNum = 0; // Total comments number.
let curPage = 1; // Current page index.
let pageSize = 10; // The number of comment displayed in one page.
// 使用A函数的主代码。
async function dealWithAsync() {
totalNum = await getListCount();
console.log('Get count', totalNum);
if (pageSize * (curPage - 1) > totalNum) {
curPage = 1;
}
return getListData();
}
// 使用Promise的主代码。
function dealWithPromise() {
return new Promise((resolve, reject) => {
getListCount().then(res => {
totalNum = res;
console.log('Get count', res);
if (pageSize * (curPage - 1) > totalNum) {
curPage = 1;
}
return getListData()
}).then(resolve).catch(reject);
});
}
// 开始执行dealWithAsync函数。
// dealWithAsync().then(res => {
// console.log('Get Data', res)
// }).catch(err => {
// console.log(err);
// });
// 开始执行dealWithPromise函数。
// dealWithPromise().then(res => {
// console.log('Get Data', res)
// }).catch(err => {
// console.log(err);
// });
function getListCount() {
return createPromise(100).catch(() => {
throw 'Get list count error';
});
}
function getListData() {
return createPromise([], {
curPage: curPage,
pageSize: pageSize,
}).catch(() => {
throw 'Get list data error';
});
}
function createPromise(
data, // Reback data
params = null, // Request params
isSucceed = true,
timeout = 1000,
) {
return new Promise((resolve, reject) => {
setTimeout(() => {
isSucceed ? resolve(data) : reject(data);
}, timeout);
});
}
对比 dealWithAsync
和 dealWithPromise
两个简单的函数,能直观的发现:使用A函数,除了有 await
关键字外,与同步代码无异。而使用 Promise
则需要根据规则增加很多包裹性的链式操作,产生了太多回调函数,不够简约。另外,这里分开了每个异步操作,并规定好各自成功或失败时传递出来的数据,近乎实际开发。
1 登堂
1.1 形式
A函数也是函数,所以具有普通函数该有的性质。不过形式上有两点不同:一是定义A函数时, function
关键字前需要有 async
关键字(意为异步),表示这是个A函数。二是在A函数内部可以使用 await
关键字(意为等待),表示会将其后面跟随的结果当成异步操作并等待其完成。
以下是它的几种定义方式。
// 声明式
async function A() {}
// 表达式
let A = async function () {};
// 作为对象属性
let o = {
A: async function () {}
}; // 作为对象属性的简写式
let o = {
async A() {}
}; // 箭头函数
let o = {
A: async () => {}
};
1.2 返回值
执行A函数,会固定的返回一个 Promise
对象。
得到该对象后便可监设置成功或失败时的回调函数进行监听。如果函数执行顺利并结束,返回的P对象的状态会从等待转变成成功,并输出 return
命令的返回结果(没有则为 undefined
)。如果函数执行途中失败,JS会认为A函数已经完成执行,返回的P对象的状态会从等待转变成失败,并输出错误信息。
// 成功执行案例
A1().then(res => {
console.log('执行成功', res); //
}); async function A1() {
let n = 1 * 10;
return n;
} // 失败执行案例
A2().catch(err => {
console.log('执行失败', err); // i is not defined.
}); async function A2() {
let n = 1 * i;
return n;
}
1.3 await
只有在A函数内部才可以使用 await
命令,存在于A函数内部的普通函数也不行。
引擎会统一将 await
后面的跟随值视为一个 Promise
,对于不是 Promise
对象的值会调用 Promise.resolve()
进行转化。即便此值为一个 Error
实例,经过转化后,引擎依然视其为一个成功的 Promise
,其数据为 Error
的实例。
当函数执行到 await
命令时,会暂停执行并等待其后的 Promise
结束。如果该P对象最终成功,则会返回成功的返回值,相当将 awaitxxx
替换成 返回值
。如果该P对象最终失败,且错误没有被捕获,引擎会直接停止执行A函数并将其返回对象的状态更改为失败,输出错误信息。
最后,A函数中的 returnx
表达式,相当于 returnawaitx
的简写。
// 成功执行案例
A1().then(res => {
console.log('执行成功', res); // 约两秒后输出100。
}); async function A1() {
let n1 = await 10;
let n2 = await new Promise(resolve => {
setTimeout(() => {
resolve(10);
}, 2000);
});
return n1 * n2;
} // 失败执行案例
A2().catch(err => {
console.log('执行失败', err); // 约两秒后输出10。
}); async function A2() {
let n1 = await 10;
let n2 = await new Promise((resolve, reject) => {
setTimeout(() => {
reject(10);
}, 2000);
});
return n1 * n2;
}
2 入室
2.1 继发与并发
对于存在于JS语句( for
, while
等)的 await
命令,引擎遇到时也会暂停执行。这意味着可以直接使用循环语句处理多个异步。
以下是处理继发的两个例子。A函数处理相继发生的异步尤为简洁,整体上与同步代码无异。
// 两个方法A1和A2的行为结果相同,都是每隔一秒输出10,输出三次。 async function A1() {
let n1 = await createPromise();
console.log('N1', n1);
let n2 = await createPromise();
console.log('N2', n2);
let n3 = await createPromise();
console.log('N3', n3);
} async function A2() {
for (let i = 0; i< 3; i++) {
let n = await createPromise();
console.log('N' + (i + 1), n);
}
} function createPromise() {
return new Promise(resolve => {
setTimeout(() => {
resolve(10);
}, 1000);
});
}
接下来是处理并发的三个例子。A1函数使用了 Promise.all
生成一个聚合异步,虽然简单但灵活性降低了,只有都成功和失败两种情况。A3函数相对A2仅仅为了说明应该怎样配合数组的遍历方法使用 async
函数。重点在A2函数的理解上。
A2函数使用了循环语句,实际是继发的获取到各个异步值,但在总体的时间上相当并发(这里需要好好理解一番)。因为一开始创建 reqs
数组时,就已经开始执行了各个异步,之后虽然是逐一继发获取,但总花费时间与遍历顺序无关,恒等于耗时最多的异步所花费的时间(不考虑遍历、执行等其它的时间消耗)。
// 三个方法A1, A2和A3的行为结果相同,都是在约一秒后输出[10, 10, 10]。
async function A1() {
let res = await Promise.all([createPromise(), createPromise(), createPromise()]);
console.log('Data', res);
}
async function A2() {
let res = [];
let reqs = [createPromise(), createPromise(), createPromise()];
for (let i = 0; i< reqs.length; i++) {
res[i] = await reqs[i];
}
console.log('Data', res);
}
async function A3() {
let res = [];
let reqs = [9, 9, 9].map(async (item) => {
let n = await createPromise(item);
return n + 1;
});
for (let i = 0; i< reqs.length; i++) {
res[i] = await reqs[i];
}
console.log('Data', res);
} function createPromise(n = 10) {
return new Promise(resolve => {
setTimeout(() => {
resolve(n);
}, 1000);
});
}
2.2 错误处理
一旦 await
后面的 Promise
转变成 rejected
,整个 async
函数便会终止。然而很多时候我们不希望因为某个异步操作的失败,就终止整个函数,因此需要进行合理错误处理。注意,这里所说的错误不包括引擎解析或执行的错误,仅仅是状态变为 rejected
的 Promise
对象。
处理的方式有两种:一是先行包装 Promise
对象,使其始终返回一个成功的 Promise
。二是使用 try.catch
捕获错误。
// A1和A2都执行成,且返回值为10。
A1().then(console.log);
A2().then(console.log); async function A1() {
let n;
n = await createPromise(true);
return n;
} async function A2() {
let n;
try {
n = await createPromise(false)
} catch (e) {
n = e;
}
return n;
} function createPromise(needCatch) {
let p = new Promise((resolve, reject) => {
reject(10);
});
return needCatch ? p.catch(err => err) : p;
}
2.3 实现原理
前言中已经提及,A函数是使用G函数进行异步处理的增强版。既然如此,我们就从其改进的方面入手,来看看其基于G函数的实现原理。A函数相对G函数的改进体现在这几个方面:更好的语义,内置执行器和返回值是 Promise
。
更好的语义。G函数通过在 function
后使用 *
来标识此为G函数,而A函数则是在 function
前加上 async
关键字。在G函数中可以使用 yield
命令暂停执行和交出执行权,而A函数是使用 await
来等待异步返回结果。很明显, async
和 await
更为语义化。
// G函数
function* request() {
let n = yield createPromise();
} // A函数
async function request() {
let n = await createPromise();
} function createPromise() {
return new Promise(resolve => {
setTimeout(() => {
resolve(10);
}, 1000);
});
}
内置执行器。调用A函数便会一步步自动执行和等待异步操作,直到结束。如果需要使用G函数来自动执行异步操作,需要为其创建一个自执行器。通过自执行器来自动化G函数的执行,其行为与A函数基本相同。可以说,A函数相对G函数最大改进便是内置了自执行器。
// 两者都是每隔一秒钟打印出10,重复两次。
// A函数
A();
async function A() {
let n1 = await createPromise();
console.log(n1);
let n2 = await createPromise();
console.log(n2);
} // G函数,使用自执行器执行。
spawn(G); function* G() {
let n1 = yield createPromise();
console.log(n1);
let n2 = yield createPromise();
console.log(n2);
} function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
} function createPromise() {
return new Promise(resolve => {
setTimeout(() => {
resolve(10);
}, 1000);
});
}
2.4 执行顺序
在了解A函数内部与包含它外部间的执行顺序前,需要明白两点:一为 Promise
的实例方法是推迟到本轮事件末尾才执行的后执行操作,详情请查看链接。二为 Generator
函数是通过调用实例方法来切换执行权进而控制程序执行顺序,详情请查看链接。理解好A函数的执行顺序,能更加清楚的把握此三者的存在。
先看以下代码,对比A1、A2和A3方法的结果。
F(A1); // 接连打印出:1 3 4 2 5。
F(A2); // 接连打印出:1 3 2 4 5。
F(A3); // 先打印出:1 3 2,隔两秒后打印出:4 9。 function F(A) {
console.log(1);
A().then(console.log);
console.log(2);
} async function A1() {
console.log(3);
console.log(4);
return 5;
} async function A2() {
console.log(3);
let n = await 5;
console.log(4);
return n;
} async function A3() {
console.log(3);
let n = await createPromise();
console.log(4);
return n;
} function createPromise() {
return new Promise(resolve => {
setTimeout(() => {
resolve(9);
}, 2000);
});
}
从结果上可归纳出一些表面形态。执行A函数,会即刻执行其函数体,直到遇到 await
命令。遇到 await
命令后,执行权会转向A函数外部,即不管A函数内部执行而开始执行外部代码。执行完外部代码(本轮事件)后,才继续执行之前 await
命令后面的代码。
归纳到此已成功一半,之后着手分析其成因。如果客官您对本楼有所了解,那一定不会忘记‘自执行器’这位大婶吧?估计是忘记了。A函数的本质就是带有自执行器的G函数,所以探究A函数的执行原理就是探究使用自执行器的G函数的执行原理。想起了?
再看下面代码,使用相同逻辑的G函数会得到与A函数相同的结果。
F(A); // 先打印出:1 3 2,隔两秒后打印出:4 9。
F(() => {
return spawn(G);
}); // 先打印出:1 3 2,隔两秒后打印出:4 9。 function F(A) {
console.log(1);
A().then(console.log);
console.log(2);
} async function A() {
console.log(3);
let n = await createPromise();
console.log(4);
return n;
} function* G() {
console.log(3);
let n = yield createPromise();
console.log(4);
return n;
} function createPromise() {
return new Promise(resolve => {
setTimeout(() => {
resolve(9);
}, 2000);
});
} function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
自动执行G函数时,遇到 yield
命令后会使用 Promise.resolve
包裹其后的表达式,并为其设置回调函数。无论该 Promise
是立刻有了结果还是过某段时间之后,其回调函数都会被推迟到在本轮事件末尾执行。之后再是下一步,再下一步。同样的道理适用于A函数,当遇到 await
命令时(此处略去三五字),所以有了如此这般的执行顺序。谢幕。
Async:简洁优雅的异步之道的更多相关文章
- async 更优雅异步体验
上一篇<让 Generator 自启动>介绍了通过起动器让 Generator 跑起来,而本篇采用 async 实现更优雅的异步编程. 从例子开始 借用上一篇例子中的例子说起. funct ...
- SpringBoot系列——@Async优雅的异步调用
前言 众所周知,java的代码是同步顺序执行,当我们需要执行异步操作时我们需要创建一个新线程去执行,以往我们是这样操作的: /** * 任务类 */ class Task implements Run ...
- 用 async/await 来处理异步
昨天看了一篇vue的教程,作者用async/ await来发送异步请求,从服务端获取数据,代码很简洁,同时async/await 已经被标准化,是时候学习一下了. 先说一下async的用法,它作为一个 ...
- 用async/ await来发送异步
昨天看了一篇vue的教程,作者用async/ await来发送异步请求,从服务端获取数据,代码很简洁,同时async/await 已经被标准化,是时候学习一下了. 先说一下async的用法,它作为一个 ...
- 【转】用 async/await 来处理异步
原文地址:https://www.cnblogs.com/SamWeb/p/8417940.html 昨天看了一篇vue的教程,作者用async/ await来发送异步请求,从服务端获取数据,代码很简 ...
- 如何用python“优雅的”调用有道翻译?
前言 其实在以前就盯上有道翻译了的,但是由于时间问题一直没有研究(我的骚操作还在后面,记得关注),本文主要讲解如何用python调用有道翻译,讲解这个爬虫与有道翻译的js“斗争”的过程! 当然,本文仅 ...
- Revealjs网页版PPT让你复制粘贴另类装逼,简洁优雅又低调,不懂编程也看过来
Revealjs网页版PPT让你复制粘贴另类装逼,简洁优雅又低调,不懂编程也看过来 要了解一个新知识我们可以从三个方面入手:是什么,有什么用,怎么用.下面我们就从这三个方面进行讲解Reveal.js噢 ...
- vue中用 async/await 来处理异步
原文作者:https://www.cnblogs.com/SamWeb/p/8417940.html 昨天看了一篇vue的教程,作者用async/ await来发送异步请求,从服务端获取数据,代码很简 ...
- 用 async/await 来处理异步(转)
昨天看了一篇vue的教程,作者用async/ await来发送异步请求,从服务端获取数据,代码很简洁,同时async/await 已经被标准化,是时候学习一下了. 先说一下async的用法,它作为一个 ...
随机推荐
- 设置电脑中的某个程序不弹出UAC用户控制提示的方法
有用户发现在电脑开机后总是会弹出UAC用户账户控制窗口,这是因为电脑中的某个程序设置了开机启动,这样就会在开机后启动该程序时出现UAC提示.如果想要省略该提示,可以在电脑中设置该程序不弹出UAC用户控 ...
- C#支付宝多次回调问题
//必须删除掉页面上的默认内容,不然支付宝会多次回调 <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml&q ...
- C# 缓存操作类
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.W ...
- git 恢复单个文件的历史版本
首先查看该文件的历史版本信息:git log Default@2x.png 记录下需要恢复的commit版本号:如 9aa51d89799716aa68cff3f30c26f8815408e926 恢 ...
- 安装logstash和logstash-input-jdbc
一.安装logstash 1.mac 下直接 brew install logstash 二.安装logstash-input-jdbc 直接在logstash的安装目录bin下运行 ./logsta ...
- pymouse 点击指定坐标点
from pymouse import PyMouse mouse = PyMouse() mouse.click(,)
- Oracle(转换函数)
在数据库中主要使用数据类型:字符,数字,日期(时间戳),所以这三种数据类型之间需要实现转换操作. 常用转换函数: 3.1.TO_CHAR()函数 将数据类型变为字符串. 日期格式化标记: 在TO_CH ...
- 力扣(LeetCode) 9.回文数
判断一个整数是否是回文数.回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数. 示例 1: 输入: 121 输出: true 示例 2: 输入: -121 输出: false 解释: 从左向 ...
- JavaWeb知识点总结
>一: 创建Web项目项目说明:1.java Resources:java源文件2.WebContent:网页内容html.css.js.jsp.资源.配置文件等 HTML:Hyper Text ...
- 《UnityShader入门精要》学习笔记之渲染流水线
第一种分类方式: 图形管道(如下7步): 顶点数据 : 由3D模型传递的三角形网格 顶点着色 : 编写CG程序对各个顶点进行着色 生成几何图元 : 连接特定的顶点生成几何图元,例如连接三个顶点生成一个 ...