超级面板
文章目录
最新文章
最近更新
文章分类
标签列表
文章归档

ES6 - Generator 函数和 Async 函数

Generator 函数

Generator (生成器)函数是 ES6 提供的新的解决异步回调嵌套的特性,先看个实例:

function* test(){
yield 1; //执行顺序:3
yield 2; //执行顺序:6
return 3; //执行顺序:9
}

var g = test(); //执行顺序:1
var rt;
rt = g.next(); //执行顺序:2
console.log(rt); // 执行顺序:4 输出:Object {value: 1, done: false}

rt = g.next(); //执行顺序:5
console.log(rt); // 执行顺序:7 输出:Object {value: 2, done: false}

rt = g.next(); //执行顺序:8
console.log(rt); // 执行顺序:10 输出:Object {value: 3, done: true}

rt = g.next(); //执行顺序:11
console.log(rt); // 执行顺序:12 输出:Object {value: undefined, done: true}

上面出现的几个关键字的作用:

  • function* 声明一个生成器函数,通过生成器函数可以得到一个迭代器
  • yeild 关键字可以保存 Generator 函数的执行位置,并跳出到调用此函数的地方继续执行, yeild 关键字只能用于 Generator 函数, 否则报错
  • 通过此函数的 next 函数,可以继续返回之前暂停的地方继续执行,next 函数返回值是一个包含两个属性的对象,value 属性是 yield 后面语句的结果,done 属性代表是否结束

Generator 的运行流程为:

  • 每次执行生成器函数(var g = test()),都会生成一个迭代器对象(g
  • 每次调用迭代器对象的 next 方法(g.next()),都会促使迭代器对象向下执行,直到遇到 yield 关键字后,保存当前状态并跳出迭代器,并把 yield 关键字后的表达式的结果作为 value 的值返回
  • 外围函数继续执行(console.log(rt)),直到下次调用迭代器的 next 函数,则继续从迭代器停止的位置开始继续执行,直到下一个 yield 语句。
  • 如果直到运行结束没有下一个 yield 语句则迭代器结束,设置 done 字段的值为 true ,此时,如果存在 return 语句,则 return 语句的返回值为 value 属性的值,否则 value 的值为 undefined
  • 此时迭代器已经结束,再次调用 next() 结果始终为: {value: undefined, done: true}

Generator.prototype.next()

next() 方法可以接受一个参数向生成器传值(参数可以省略),并返回一个包含属性 donevalue 的对象:

gen.next([value])

  • [value][Any=undefined]:修改上一次 yield 语句的返回值,如果是第一次调用则不起作用。
  • return[{value, done}]: value 表示本次 yield 语句的值,done 表示是否结束。

注意 如果 value 参数省略则传递给当前 yield 语句的值为 undefined

function* double(x) {
while(true) {
x = 2 * (yield x);
}
}

var t = double(3);
t.next() // Object {value: 3, done: false}
t.next() // Object {value: NaN, done: false}
t.next(4) // Object {value: 8, done: false}

在以上示例中,第二次执行 next() 未传递参数,此时实际上等价于 x = 2 * undefined 因此此时 value 属性的值为 NAN ,第三次执行时,传递了参数 4 此时的 value2 * 4

Generator.prototype.throw()

throw() 方法用来向生成器抛出异常,如果生成器函数包含 try...catch 语句块,则被其捕获,否则异常抛到外部函数:

var g = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获', e);
}
yield console.log("This line will be logged");
};

var i = g();
i.next();

try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}

// 内部捕获 a
// This line will be logged
// 外部捕获 b

由上述示例可以看到,在触发 throw 语句时,如果 generate 内部处理了这个错误,则会执行了一次 next 语句。

刚才提到,如果 generate 函数跑出的错误没有被 generate 函数内部捕获处理,错误会被交给外部函数,此时,生成器函数将执行结束,即再次调用其 next 方法,则始终返回 {value:undefined, done: true} ,此生成器函数不再会被执行:

var gen = function* () {
yield console.log("This line will be logged.");
throw new Error("Something is error.")
yield console.log("This line will not be logged.");
yield console.log("This line also will not be logged.");
};

var g = gen();

try {
g.next();
g.next();
g.next();
} catch (err) {
console.log(err)
}

// Object {value: undefined, done: false}
// Error: Something is error.
// Object {value: undefined, done: true}

Generator.prototype.return()

return() 方法返回给定的值并提前结束生成器

function* gen() { 
yield 1;
yield 2;
yield 3;
}

var g = gen();

g.next(); // { value: 1, done: false }
g.return("foo"); // { value: "foo", done: true }
g.next(); // { value: undefined, done: true }

Async 函数

ES2017 标准引入了 async 函数,更加简化了异步操作,而 async 函数实际上是 Generator 函数的语法糖。

async 函数返回一个 Promise 对象,可以使用 then 回调处理函数的返回值,async 函数如果包括 await 表达式,则会暂停执行,等待 await 表达式执行完毕后继续执行,如果 async 函数内部有多个 await ,则只有当所有的 await 执行完成或者遇到未捕获的错误才会结束。

function delay(time, value){
return new Promise(resolve => {
setTimeout(() => {
console.log(`After ${time}ms return ${value}.`);
resolve(value);
}, time)
})
}

async function add(x) {
var a = await delay(200, 1);
var b = await delay(300, 2);
return x + a + b;
}

add(2).then(d=>console.log(d));

// After 200ms return 1.
// After 300ms return 2.
// 5

await 语句

await 命令通常跟一个 Promise 对象,如果后面不是一个 Promise,则被转换成一个立即 resolvePromise 对象,如果 await 后面的 Promise 状态变为 reject ,则 async 停止执行,且其返回的 Promise 状态为 reject

async function test() {
await Promise.reject("reject");
console.log("This line will not run"); // not log
}

test().then(d => console.log(d)).catch(e => console.log(e));

// reject

await 并发

多个 await 的命令实际上是逐个调用的,即只有在上一个调用完成之后才会进行下一个的调用:

function delay(time, value){
return new Promise(resolve => {
setTimeout(() => {
console.log(`After ${time}ms return ${value}.`);
resolve(value);
}, time)
})
}

async function add(x) {
var a = await delay(300, 1);
console.log('Waiting.');
var b = await delay(200, 2);
return x + a + b;
}

add(2).then(d=>console.log(d));

// After 300ms return 1.
// Wait delay(300, 1) finished.
// After 200ms return 2.
// 5

由以上示例可以看到,第一个 await 执行时间较长,log('Waiting.') 语句等待其执行完毕后才执行,如果两个 await 是相互独立的操作最好让他们同时触发,可以减少操作耗时:

function delay(time, value){
return new Promise(resolve => {
setTimeout(() => {
console.log(`After ${time}ms return ${value}.`);
resolve(value);
}, time)
})
}

// 原始继发的写法
async function add(x) {
var t1 = new Date().getTime();
var a = await delay(300, 1);
var b = await delay(200, 2);
var t2 = new Date().getTime();
console.log(`Async run Time is ${t2 - t1}ms.`);
return x + a + b;
}

// 改进1:先调用 promise 后赋值
async function add1(x) {
var t1 = new Date().getTime();
var delayA = delay(300, 1);
var delayB = delay(200, 2);
var a = await delayA;
var b = await delayB;
var t2 = new Date().getTime();
console.log(`Async run Time is ${t2 - t1}ms.`);
return x + a + b;
}

// 改进2:使用 Promise.all
async function add2(x) {
var t1 = new Date().getTime();
var [a, b] = await Promise.all([delay(300, 1), delay(200, 2)])
var t2 = new Date().getTime();
console.log(`Async run Time is ${t2 - t1}ms.`);
return x + a + b;
}

add(2).then(d=>console.log(d));
// After 300ms return 2.
// After 200ms return 1.
// Async run Time is 1852ms.
// 5

add1(2).then(d=>console.log(d));
// After 200ms return 2.
// After 300ms return 1.
// Async run Time is 360ms.
// 5

add2(2).then(d=>console.log(d));
// After 200ms return 2.
// After 300ms return 1.
// Async run Time is 381ms.
// 5

由上例可以看出,此时耗时更短的 delayB 先完成,并且总耗时也最短。

重写 Promise回调链:

先看一个示例:

// 使用纯 Promise 的写法
function getProcessedData(url) {
return downloadData(url) // returns a promise
.catch(e => {
return downloadFallbackData(url) // returns a promise
})
.then(v => {
return processDataInWorker(v); // returns a promise
});
}

// 使用 async 的写法
async function getProcessedData(url) {
let v:
try {
v = await downloadData(url);
} catch (e) {
v = await downloadFallbackData(url);
}
return processDataInWorker(v);
}

从上述示例可以看出,使用 Async 处理异步的流程写起来非常的直观,就像同步的代码一样。

异常处理

如果 await 后面的 promise 出错或者 reject ,此 await 后面的语句将会被跳过,此时可以将 await 语句放到 try...catch 代码块中:

async function test() {
try {
await Promise.reject("reject");
// 如果有多个 await语句,都可以放进来
} catch(e) {

}
console.log("This line will run.");
}

test().then(d=>console.log(d)).catch(e=>console.log(e));

// This line will run.
// undefined. 因为 async 函数没有返回任何值

使用 try...catch 结构,可以实现多次尝试:

function delay(time, value){
return new Promise((resolve, reject) => {
setTimeout(() => {
var random = Math.random();
if (random > 0.6) {
resolve(value);
} else {
reject(value)
}
}, time)
})
}

async function test() {
const MAX_TRY_TIMES = 5;
for (var i = 0; i < MAX_TRY_TIMES; i ++) {
console.log(`第 ${i + 1} 次尝试`)
try {
await delay(100, 1);
break;
} catch(err) {

}
}
console.log(`第 ${i + 1} 次尝试成功`); // 3
}

test();

// 第 1 次尝试
// 第 2 次尝试
// 第 2 次尝试成功

Async 的实现

前文提到 asyncgenerate 函数的语法糖,也就是说,async 函数实质上是对 generate 函数的封装,在这里将尝试使用 generate 来生成 async 的实现:

首先需要把 async 的函数内容定义转化为 generate 函数,这一步很简单,只需要把 async 关键字去掉,function 关键字后面加 * ,代码中所有的 await 关键字改为 yield 关键字:

var asyncFunc = async function(x) {
var a = await delay(300, 1);
var b = await delay(200, 2);
return x + a + b;
}

// 转化为
var generateFunc = function* (x) {
var a = yield delay(300, 1);
var b = yield delay(200, 2);
return x + a + b;
};

但是此时 generateFunc 还不能被执行,还需要为 generateFunc 创建一个执行器 generateFuncRunner
而 async 函数返回一个 Promise ,可以很容易写出执行器 generateFuncRunner 的函数签名:

var asyncFunc = function() {
return generateFuncRunner(generateFunc);
};

function generateFuncRunner(fn) {
return new Promise(function (resolve, reject) {
// run generate
});
}

接下来就是编写一个 generate 的运行器了,具体思路在注释里,不再详细说明:

function generateFuncRunner(fn) {
return new Promise(function (resolve, reject) {
// 获取迭代器对象
var gen = fn();

// 递归运行该迭代器对象
function step(args) {
try {
var next = gen.next(args);
} catch(err) {
reject(err);
}
// generate fn 已经结束,直接 resolve
if (next.done) {
resolve(next.value);

// generate fn 未结束,递归向后执行直到 fn 结束
} else {

// 返回一个立即 resolve 的 promise 处理下一处 yield
return Promise.resolve(next.value).then(function(value) {
step(value);

// 捕获 generate 函数的下一个 yield 处可能发生的异常
}, function(err) {
gen.throw(err);
})
}
}
});
}

这样就实现了 async 函数,下面附上 babelasync 的转码作为参考(针对本实例稍有修改):

let asyncFunc = (() => {
var _ref = _asyncToGenerator(function* (x) {
var a = yield delay(300, 1);
var b = yield delay(200, 2);
return x + a + b;
});

return function asyncFunc() {
return _ref.apply(this, arguments);
};
})();

function _asyncToGenerator(fn) {
return function () {
return new Promise(function (resolve, reject) {
var gen = fn();
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");
});
};
}

参考