优雅的异步编程解决方案:async/await
虽然"Promise"一定程度上解决了回调地狱的问题,但过多的链式调用也会造成代码可读性不好,而且流程控制也不方便。于是乎 ES7 提出了终极异步编程解决方案-"async/await",可以使用同步的代码逻辑来编写异步的代码
先聊一聊 Generator
提到async/await就不得不说"Generator"(生成器),它出现在 async/await 之前的异步编程解决方案,async/await实际上是对Generator的封装。所以在学习async/await之前了解Generator是有必要的
Generator函数是 ES6 提供的一种异步编程解决方案。从语法上理解,它是一个状态机,封装了多个内部状态。执行执行 Generator 函数会返回一个遍历器对象
- 语法:
function* hwGenerator() {
yield "hello";
yield "world";
return "ending";
}
var hw = hwGenerator();
hw.next(); // {value: "hello", done: false}
hw.next(); // {value: "world", done: false}
hw.next(); // {value: "ending", done: true}
hw.next(); // {value: undefined, done: true}
首先,Generator形式上是一个普通的函数,它有两个特征。 function 关键字与函数名之间有一个*
号;另一个是函数内部使用yield
表达式,定义不同的状态。调用next
方法是把yield
后面的内容返回
yield表达式与return很相似,都是返回后面表达式的值。但也有区别,return语句一个函数里面只能执行一次 ,但 yield语句可以执行多次。yield还具有“记忆功能“(每次遇yield,函数就会暂停执行,下一次再从该位置继续向后执行),而 return没有这个功能。在Generator函数中return表达式返回是done为"true"时的"value"
yield表达式本身没有返回值,next方法接收一个参数,该参数会被当做上一个 yield表达式返的返回值
next或return返回的是一个对象,里面有两个属性value和done,其中done为true表示 Generator 函数已经执行完成,后面再次调用 next也是这个值
Generator 函数原型方法
-
Generator.prototype.throw 用于抛出错误,然后在 Generator 函数体内捕获,如果 Generator 函数体内没有捕获则抛出到 Generator 函数体外
-
Generator.prototype.return 返回给定的值并且终结 Generator 函数
function* fn(num) {
try {
const r1 = yield num;
const r2 = yield r1 + 1;
console.log(r2);
} catch (e) {
console.log("捕获错误", e);
}
}
var g = fn(1);
g.next(2); // {value: 1, done: false}
g.next(3); // {value: 4, done: false}
g.throw("出错了"); // 直接终止,done: true
g.next();
这里需要注意一点,r1 的值不会保留上一次的值(也就是 2),而是直接用这次
next
方法传入的参数作为值,所以g.next(3)
中yield r1+1
的 r1 为 3 而并非 2
那 Generator
函数内部是怎么实现的呢?我们借助babel
可以窥探到其中的原理,使用 babel 转换上面的代码得到如下的结果:
"use strict";
require("regenerator-runtime/runtime");
var _marked = regeneratorRuntime.mark(hwGenerator);
function hwGenerator() {
return regeneratorRuntime.wrap(function hwGenerator$(_context) {
while (1) {
switch ((_context.prev = _context.next)) {
case 0:
_context.next = 2;
return "hello";
case 2:
_context.next = 4;
return "world";
case 4:
return _context.abrupt("return", "ending");
case 5:
case "end":
return _context.stop();
}
}
}, _marked);
}
var hw = hwGenerator();
hw.next();
hw.next();
hw.next();
hw.next();
regenerator-runtime 是 facebook 的开源项目
regenerator
里的模块,主要是提供Generator
、async/await
的 ES5 实现,所以 Babel 转换时就直接使用了regenerator-runtime
来实现。
可以看到yield
表达式都被转换为switch-case
的形式了接下来,我们参考regenerator-runtime
来实现一个简单的Generator
,帮助我们更容易地理解背后的原理
function Generator(fn) {
if (!fn) {
throw new Error(" 'fn' is required");
}
if (typeof fn !== "function") {
throw new Error(" 'fn' must be a function ");
}
var context = {
prev: 0,
next: 0,
done: false,
stop: function() {
this.done = true;
}
};
return {
next: function() {
var result = fn(context);
if (result === undefined) return { value: undefined, done: true };
return {
value: result,
done: false
};
}
};
}
function helloWorld() {
return Generator(function(_context) {
while (1) {
switch ((_context.prev = _context.next)) {
case 0:
_context.next = 2;
return "hello";
case 2:
_context.next = 4;
return "world";
case 4:
_context.next = 5;
return "ending";
case 5:
case "end":
return _context.stop();
}
}
});
}
var hw = helloWorld();
hw.next(); // {value: "hello", done: false}
hw.next(); // {value: "world", done: false}
hw.next(); // {value: "ending", done: true}
hw.next(); // {value: undefined, done: true}
Generator 函数的流程管理
上面示例,我们都是手动调用 next 方法从而得到函数的结果,那能不能让 Generator 函数可以自动执行呢?这就涉及到 Generator函数的流程管理
实现 Generator 函数的流程管理有两种方式Thunk和Promise
Thunk
函数最初用于编译器参数求值策略-”传名调用“,Generator 函数出现后,它可用于 Generator 函数的自动执行。下面是一个简单的Thunk
函数实现自动执行的示例
var readFile = function(fileName, callback) {
setTimeout(function() {
console.log(fileName);
callback();
}, 1000);
};
function thunkify(fn) {
return function() {
var args = new Array(arguments.length);
var ctx = this;
for (var i = 0; i < args.length; ++i) {
args[i] = arguments[i];
}
return function(done) {
var called;
args.push(function() {
if (called) return;
called = true;
done.apply(null, arguments);
});
try {
fn.apply(ctx, args);
} catch (err) {
done(err);
}
};
};
}
var readFileThunkify = thunkify(readFile);
var gen = function*() {
var r1 = yield readFileThunkify("路径1");
console.log(r1);
var r2 = yield readFileThunkify("路径2");
console.log(r2);
};
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) {
return;
}
result.value(next);
}
}
- Promise 实现
var readFilePromise = function(fileName) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
console.log(fileName);
resolve("文件流");
}, 1000);
});
};
var gen = function*() {
var f1 = yield readFilePromise("文件1");
var f2 = yield readFilePromise("文件2");
console.log(f1;
console.log(f2;
};
function run(gen) {
var g = gen();
function next(data) {
var result = g.next(data);
if (result.done) {
return result.value;
}
result.value.then(function(data) {
next(data);
});
}
next();
}
run(gen);
通过上面代码可以看出,自动执行的本质就是递归调用 "next"函数
async 函数
聊完了Generator,接下步入正题来说下async函数
语法
async function helloWorld() {
const hw = await "Hello World"; // 普通变量
console.log(hw);
const p = await new Promise((resolved, reject) => {
// todo ……
resolved();
}).catch(e => console.error(e));
console.log(p);
return "Promise";
}
const result = helloWorld();
console.log(result);
- async 函数很简单,跟普通函数相比,多了个
async
和await
关键字 - async 函数执行的时,一旦遇到 await 就会先返回(把”执行权“交出去),等到异步操作完成,再接着执行函数后面的语句(收回”执行权“)
- async 函数返回一个 Promise 对象,所以可以是使用 Promise 的 then 方法,比如:
helloWorld().then(v=>console.log(v),e=>console.log(e))
- async 函数返回的 Promise 对象,只有函数内部的异步操作都完成后,才会发生状态改变,也就是执行 then 方法定义的回调函数参数
- await 命令只能用于 async 函数之中,如果用在普通函数就会报错,
- await 命令后面可以是一个 Promise 对象,也可以是普通的变量,如果是 Promise 则返回该对象的结果。反之则直接返回对应的值。
如果 Promise 对象变为 reject 状态,那么整个 async 函数都会中断执行。有些时候我们想虽然发生错误但也不要中断,这是可以将出错的 await 语句放到try...catch
里
async function say() {
try {
await Promise.reject("出错了");
} catch (e) {
console.error(e);
}
}
say().then(v => console.log(v));
- 需要注意如果将
forEach
方法参数改成async
函数的话,执行顺序不是依次执行的而是并发执行的
function handler(data, i) {
// todo
const t = i === 1 ? 1000 : 0;
setTimeout(() => {
console.log(data);
}, t);
}
async function foo() {
const datas = [1, 2, 3];
datas.forEach(async function(data, i) {
await handler(data, i);
});
}
如果想要得到依次执行的结果,可以使用 for 循环
function handler(data, i) {
// todo
const t = i === 1 ? 1000 : 0;
setTimeout(() => {
console.log(data);
}, t);
}
async function foo() {
const datas = [1, 2, 3];
for (let data of datas) {
await handler(data);
}
}
aync 函数的优势
- 更好的语义,
async
= 异步,await
= 等候 - 更广的实用性, await 命令后面可以是 Promise 和原始类型值
- 返回 Promise,可以通过
then
方法指定下一步操作
async 函数实现原理
前面说到"async/await"实际上是对"Generator"的封装,准备的说它是由 Generator函数 + 自动执行器构成,结合前面讲的知识,我们就可以模拟实现 "async/await"函数了
function run(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
if (typeof gen === "function") gen = gen.call(ctx);
if (!gen || typeof gen.next !== "function") return resolve(gen);
onResolved();
function onResolved(data) {
var ret;
try {
ret = gen.next(data);
} catch (e) {
return reject(e);
}
next(ret);
}
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
function toPromise(obj){
if(!obj && 'function' == typeof obj.then){
return obj
}else{
return Promise.resolve(obj)
}
}
function next(ret) {
if (ret.done) return resolve(ret.value);
var value= toPromise.call(ctx,ret.value);
if(value){
return value.then(onResolved, onRejected);
}
}
next();
});
}