使用ES6生成器函数处理异步问题
对ES6的生成器函数熟悉之后,我们可以开始利用它做一些有意义的事情了,比如处理异步问题。
生成器强大的地方在于,它提供了一种单线程的,看似同步的代码风格,而将具体的异步实现细节隐藏起来。这能够使我们的代码读起来非常自然,也更加易于维护,虽然本质上还是异步代码。
基础用法
这里我们看下使用生成器处理异步问题的基本用法,假设我们已有的待改进的代码是这样的:
function makeAjaxCall(url, cb) {
// do some ajax
// call `cb(result)` when complete
}
makeAjaxCall('http://some.url.1', function (result1) {
var data = JSON.parse(result1);
makeAjaxCall('http://some.url.2/?id=' + data.id,
function (result2) {
var resp = JSON.parse(result2);
console.log('The value: ' + resp.value);
});
});
我们可以使用生成器函数来改进一下:
var it;
function request(url) {
makeAjaxCall(url, function (response) {
it.next(response);
});
}
function *main() {
var result1 = yeild request('http://some.url.1');
var data = JSON.parse(result1);
var result2 = yield request('http://some.url.2/?id=' + data.id);
var resp = JSON.parse(result2);
console.log('The value: ' + resp.value);
}
it = main();
it.next(); // git it all started
这样看起来确实自然了许多。这里yield __表达式总是调用异步ajax请求,但是假如我们本地缓存有我们需要的数据因而不需要调用ajax请求呢?也好办,我们可以这样修改下request函数:
var cache = {};
function request(url) {
if (cache[url]) {
setTimeout(function () {
it.next(cache[url]);
}, 0);
}
else {
makeAjaxCall(url, function (resp) {
cache[url] = resp;
it.next(resp);
})
}
}
这样就可以用同样的方式处理本地缓存的同步数据了,而我们在*main中的业务代码完全不用动,这必须归功于我们将具体的异步实现逻辑抽离到了生成器函数之外。
高级用法
前面讨论的基础用法对于简单的异步处理还说没问题,但是某些时候还是无法满足我们的需求,比如无法实现并行异步。我们这里采用一种新的更强大的模式:yield promises。
之前我们使用的是yield request(…),而request(…)没有返回任何值,因此这里实际上是yield undefined。我们对request(…)方法稍加修改,从而使yield request(…)返回promise:
function request(url) {
// Note: returning a promise now!
return new Promise(function (resolve, reject) {
makeAjaxCall(url, resolve);
});
}
我们的生成器函数依依然不变:
function *main() {
var result1 = yield request('http://some.url.1');
var data = JSON.parse(result1);
var result2 = yield request('http://some.url.2/?id=' + data.id);
var resp = JSON.parse(result2);
console.log('The value: ' + resp.value);
}
对于yield promises模式,生成器的用法如下:
function runGenerator(g) {
var it = g();
var res;
(function iterate(val) {
res = it.next(val);
if (!res.done) {
res.value.then(iterate);
}
})();
}
runGenerator(main);
接着我们看下如何在yield promises模式中处理本地缓存的同步数据,保持生成器函数不变,我们修改下request函数和runGenerator函数:
var cache = {};
function request(url) {
if (cache[url]) {
return cache[url];
}
else {
return new Promise(function (resolve, reject) {
makeAjaxCall(url, function (resp) {
cache[url] = resp;
resolve(resp);
});
});
}
}
function runGenerator(g) {
var it = g();
var res;
(function iterate(val) {
res = it.next(val);
if (!res.done) {
if (typeof res.value.then === 'function') {
res.value.then(iterate);
}
else {
setTimeout(function () {
iterate(res.value);
}, 0);
}
}
})();
}
runGenerator(main);
下面我们看下如果在yield promise中进行错误处理:
function request(url) {
return new Promise(function (resolve, reject) {
makeAjaxCall(url, function (err, text) {
if (err) reject(err);
else resolve(text);
})
})
}
function *main() {
try {
var result1 = yield request('http://some.url.1');
}
catch (err) {
console.log("Error: " + err);
return;
}
var data = JSON.parse(result1);
try {
var result2 = yield request('http://some.url.2/?id=' + data.id);
}
catch (err) {
console.log('Error: ' + err);
return;
}
var resp = JSON.parse(result2);
console.log('value: ' + resp.value);
}
runGenerator(main);
这里如果一个promise被reject了,那么失败信息会回传到生成器内部,然后就可以catch住进行错误处理了。下面我们看下如果在yield promise中进行异步并行处理:
function *main() {
var search_terms = yield Promise.all([
request('http://some.url.1'),
request('http://some.url.2'),
request('http://some.url.3')
]);
var search_results = yield request('http://some.url.4?search=' + search_terms.join('+'));
var resp = JSON.parse(search_results);
console.log('Search results: ' + resp.value);
}
runGenerator(main);
这里Promise.all([…])创建了一个包含三个子promise的并行promise。
ES7 async
ES7中又新增了另外一种函数:async function,其作用与runGenerator非常类似:
async function main() {
var result1 = await request('http://some.url.1');
var data = JSON.parse(result1);
var result2 = await request('http://some.url.2/?id=' + data.id);
var resp = JSON.parse(result2);
console.log('value: ' + resp.value);
}
main();
正如你所看到的,async function可以直接调用,在其内部使用的是await关键字而不是yield,来告诉async function等待promise返回之后再继续执行。