一、含义
在早期,ES6 最初的异步处理方案引入 Promise 对象,后经过升级成更好的异步编程解决方案:Generator 函数。
但是,Generator 函数是不能自动执行的,返回的是一个遍历器对象。需要借助 Thunk 函数或者 co 模块实现自动流程管理。
为了使得异步操作变得更加合理,ES2017标准引入了 async 函数。相较于 Generator 函数,async 函数有一下几点优势:
1. 内置执行器
async 函数自带执行器,不需要像 Generator 函数需要 co 模块实现自动流程管理。像调用普通函数一样:asyncReadFile() ,只要一行,Generator 函数需要调用 next 方法或者借助 co 模块才能得到最终结果。
2. 更好的语义性
async 和 await 比起星号和 yield,语义更清楚。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
3. 更广的适用性
co 模块约定,yield 命令后面只能是 Thunk 函数 或 Promise对象,而 async 函数的 await 命令后面,可以是 Promise 对象和原始类型的值(数值、字符串、布尔值,但这时等同于同步操作)。
4. 返回值是 Promise
async 函数的 返回值是 Promise对象,这比 Generator 函数的返回值是 Iterator 对象方便许多,可以用 then 方法指定下一步操作。
通俗讲,async 函数是 Generator 函数的语法糖,是由多个异步操作包装成的一个 Promise 对象,而 await 命令就是内部 then 命令的语法糖。
二、用法
1、多种使用形式
// 函数声明
async function foo() {};
// 函数表达式
const = foo = async function() {};
// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...);
// calss 的方法
class Storage {
constructor() {
this.cachePromise = caches.open('yuan');
}
async getYuan(name) {
const cache = await this.cachePromise;
return cache.match(`/yuan/${ name } .png`);
}
}
const storage = new Storage();
storage.getYuan('monkey').then(...);
// 箭头函数
const foo = async () => {};
2、返回 Promise 对象
async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数
async function f() {
return "hello world";
}
f().then(v => console.log(v)); // hello world
3、Promise 对象的状态变化
只有 async 函数内部的 await 命令执行完,才会执行 then 方法指定的回调函数:
async function getTitle(url) {
let response = await fetch(url);
let html = response.text(0;
return html..match(/<title>([\s\S]+<\title>/i)[1];
}
getTitle("//www.greatytc.com/writer#/notebooks/15137721/notes/24616981/preview").then(...);
上述代码中 getTitle 函数内部的3个操作:抓取网页、取出文本。匹配页面标题。只有执行完这三个操作,才能执行 then 方法里的操作。
4、await 命令
await 命令后面是一个 Promise 对象,如果不是,会自动被转为一个 resolve 的 Promise 对象:
async function f() {
return await 'yuan';
}
f().then(v => console.log(v)); // yuan
await 命令后没的 Promise 对象如果变为 reject 状态,则 reject 的参数会被 catch 方法的回调函数接收到:
async function foo() {
await Promise.reject('报错了');
}
foo()
.then(v => console.log(v))
.catch( e => console.log(e))
// 浏览器抛出错误
只要有一个 await 命令后面的 Promise 对象变为 reject,那么整个 async 函数都会中断。
三、使用注意点
1、await 命令后面的 Promise 对象运行的结果可能是 reject 的状态,所以最好把 await 命令放在 try...catch 代码块里,或者在
await 命令后加一个 catch 方法:
// await 命令写在 try...catch
async function f() {
try {
await somePromise();
}
catch(err) {
console.log(err);
}
}
// 在 await 后加 catch
async function f() {
await somePromise()
.catch (function (err) {
console.log(err)
});
}
2、多个 await 名利后面的异步操作如果不存在继发关系,最好让他们同时出发
let foo = await getFoo();
let bar = await getBar();
3、await 命令只能用在 async 函数之中,如果用在普通函数中会报错
4、多个请求并发执行,可以使用 Promise.all 方法:
async function dbFunc(db) {
let docs = [{}, {}, {}];
let promises = doc.map((doc) = > db.post(doc));
let results = await Promise.all(promises);
console.log(results);
}
四、异步应用对比:Promise、Generator、async
以 setTimeout 来模拟异步操作:
function foo (obj) {
return new Promise((resolve, reject) => {
window.setTimeout(() => {
let data = {
height: 180
}
data = Object.assign({}, obj, data)
resolve(data)
}, 1000)
})
}
function bar (obj) {
return new Promise((resolve, reject) => {
window.setTimeout(() => {
let data = {
talk () {
console.log(this.name, this.height);
}
}
data = Object.assign({}, obj, data)
resolve(data)
}, 1500)
})
}
两个函数都返回了 Promise 对象,使用 Object.assign() 方法合并传递过来的参数。
Promise 实现异步:
function main () {
return new Promise((resolve, reject) => {
const data = {
name: 'keith'
}
resolve(data)
})
}
main().then(data => {
foo(data).then(res => {
bar(res).then(data => {
return data.talk() // keith 180
})
})
})
此方法的缺点:语义不好,不够直观,使用多个 then 方法,有可能出现回调地狱。
Generator 函数实现异步:
function *gen () {
const data = {
name: 'keith'
}
const fooData = yield foo(data)
const barData = yield bar(fooData)
return barData.talk()
}
function run (gen) {
const g = gen()
const next = data => {
let result = g.next(data)
if (result.done) return result.value
result.value.then(data => {
next(data)
})
}
next()
}
run(gen)
Generator 函数相较于 Promise 实现异步操作,优点在于:异步过程同步化,避免回调地狱的出现。缺点在于:需要借助run 函数或者 co 模块实现流程的自动管理。
async 函数实现异步:
async function main () {
const data = {
name: 'keith'
}
const fooData = await foo(data)
const barData = await bar(fooData)
return barData
}
main().then(data => {
data.talk()
})
async 函数优点在于:实现了执行器的内置,代码量减少,且异步过程同步化,语义化更强。
五、应用实例
1、从豆瓣API上获取数据:
var fetchDoubanApi = function() {
return new Promise((resolve, reject) = >{
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
var response;
try {
response = JSON.parse(xhr.responseText);
} catch(e) {
reject(e);
}
if (response) {
resolve(response, xhr.status, xhr);
}
} else {
reject(xhr);
}
}
};
xhr.open('GET', 'https://api.douban.com/v2/user/aisk', true);
xhr.setRequestHeader("Content-Type", "text/plain");
xhr.send(data);
});
};
(async function() {
try {
let result = await fetchDoubanApi();
console.log(result);
} catch(e) {
console.log(e);
}
})();
2、根据电影名称,下载对应的海报
import fs from 'fs';
import path from 'path';
import request from 'request';
var movieDir = __dirname + '/movies',
exts = ['.mkv', '.avi', '.mp4', '.rm', '.rmvb', '.wmv'];
// 读取文件列表
var readFiles = function() {
return new Promise(function(resolve, reject) {
fs.readdir(movieDir,
function(err, files) {
resolve(files.filter((v) = >exts.includes(path.parse(v).ext)));
});
});
};
// 获取海报
var getPoster = function(movieName) {
let url = `https: //api.douban.com/v2/movie/search?q=${encodeURI(movieName)}`;
return new Promise(function(resolve, reject) {
request({
url: url,
json: true
},
function(error, response, body) {
if (error) return reject(error);
resolve(body.subjects[0].images.large);
})
});
};
// 保存海报
var savePoster = function(movieName, url) {
request.get(url).pipe(fs.createWriteStream(path.join(movieDir, movieName + '.jpg')));
};
(async() = >{
let files = await readFiles();
// await只能使用在原生语法
for (var file of files) {
let name = path.parse(file).name;
console.log(`正在获取【$ {
name
}】的海报`);
savePoster(name, await getPoster(name));
}
console.log('=== 获取海报完成 ===');
})();
六、结语
本章,我们需要对比 Promise 和 Generator 异步操作的优势了解 async 函数的由来,掌握 async 函数的用法及其注意事项,再结合相应的实例深入了解 async 函数的实现原理。下一章,我们来了解 ES6 另一个语法糖: class,尽情期待吧!
章节目录
1、ES6中啥是块级作用域?运用在哪些地方?
2、ES6中使用解构赋值能带给我们什么?
3、ES6字符串扩展增加了哪些?
4、ES6对正则做了哪些扩展?
5、ES6数值多了哪些扩展?
6、ES6函数扩展(箭头函数)
7、ES6 数组给我们带来哪些操作便利?
8、ES6 对象扩展
9、Symbol 数据类型在 ES6 中起什么作用?
10、Map 和 Set 两数据结构在ES6的作用
11、ES6 中的Proxy 和 Reflect 到底是什么鬼?
12、从 Promise 开始踏入异步操作之旅
13、ES6 迭代器(Iterator)和 for...of循环使用方法
14、ES6 异步进阶第二步:Generator 函数
15、JavaScript 异步操作进阶第三步:async 函数
16、ES6 构造函数语法糖:class 类