实现Async/Await
要挑战的任务是使用JavaScript的generator生成器来实现Async/Await。
问题描述
下面是一个Async/Await函数的示例。
async function doSomething(value) {
const result1 = await fetchFromNetwork(value + '-1');
const result2 = await fetchFromNetwork(value + '-2');
try {
const result3 = await failedFetchFromNetwork();
} catch (error) {
console.error('Error fetching from network');
}
return result1 + ' ' + result2;
}
doSomething('http://google.com')
.then(r => console.log(`Got result: ${r}`))
.catch(console.error)
我们需要使用generator生成器和一个特别的封装函数“asynk”来实现同样功能。等效的示例为:
const doSomething = asynk(function* (value) {
const result1 = yield fetchFromNetwork(value + '-1');
const result2 = yield fetchFromNetwork(value + '-2');
try {
const result3 = yield failedFetchFromNetwork();
} catch (error) {
console.error('Error fetching from network');
}
return result1 + ' ' + result2;
});
doSomething('http://google.com')
.then(r => console.log(`Got result: ${r}`))
.catch(console.error)
关于“asynk“的注意事项:
- 它接收一个generator生成器函数并返回一个新函数;
- 当返回的函数被调用时,它应该返回一个Promise期约。Promise期约应当对generator生成器函数的返回值有所处理;
- 返回函数的类型特征应该和传入generator生成器函数的类型特征匹配。唯一的例外是,如果generator生成器函数返回一个非Promise期约的类型,返回函数应该返回一个与那个类型相对应的Promise期约。
待处理事项
- 如果愿意的话,你可以先实现无类型的方案。有些人觉得使用类型有所帮助,另外一些人则觉得后续添加类型更容易;
- 先关注控制流,然后才是参数值返回值可能有所帮助。
规则
- 你不能使用原生的Async/Await;
- 请不要直接查阅与”如何使用generator生成器实现Async/Await“相关的网上资料。
参考文献
下面是一些你会觉得有用的链接。请悉听尊便访问这些链接,但务必仅限于此。
下面的类型定义或许有所裨益:
interface Generator<T = unknown, TReturn = any, TNext = unknown> extends Iterator<T, TReturn, TNext> {
// NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
return(value: TReturn): IteratorResult<T, TReturn>;
throw(e: any): IteratorResult<T, TReturn>;
[Symbol.iterator](): Generator<T, TReturn, TNext>;
}
interface IteratorYieldResult<TYield> {
done?: false;
value: TYield;
}
interface IteratorReturnResult<TReturn> {
done: true;
value: TReturn;
}
type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;
TypeScript代码模板:
// See README.md for instructions.
// TODO: add type annotations.
function asynk(fn: any) {
// YOUR CODE HERE
}
// Playgroud for testing the code.
console.clear();
function* countUp() {
for (let i = 0; i < 10; i++) {
yield i;
}
}
const g = countUp();
console.log(g.next());
console.log(g.next());
// const playground = asynk(function* () {
// const result = yield Promise.resolve('hello');
// });
// playground().catch(console.error)
最终实现:
无类型的JavaScript版本:
function asynk(fn) {
// YOUR CODE HERE
return (...args) =>
new Promise((resolve, reject) => {
// Initialize the generator function, which might have signatures,
// Extract the next() queue fisrt, then iterate another initialized generator
const runner = fn(...args);
try {
let promiseCallbackQueue = [
(res, slaveRunner) => slaveRunner.next(res).value,
];
let result = runner.next();
// First round, collect promiseCallback
while (!result.done && String(result.value) === '[object Promise]') {
promiseCallbackQueue.push(
(res, slaveRunner) => slaveRunner.next(res).value
);
result = runner.next();
}
/**
Second round, iterate another generator promise chain and pass promised value, by using the trick of event loop
setTimeout -> MacroQueue
Promise resolve -> MicroQueue
For each setTimeout, its inner promise resolve will call in advance of the latter setTimeout, because of MicroQueue
Drawback: initialize the given generator for twice.
*/
let medium;
let pcqLen = promiseCallbackQueue.length;
const slaveRunner = fn(...args);
for (let i = 0; i < pcqLen; i++) {
setTimeout(() => {
const pm = promiseCallbackQueue[i](medium, slaveRunner);
medium = pm; // To get the final yield value, would be Promise fisrt
if (String(pm) === '[object Promise]') {
pm.then((res) => {
medium = res; // get the input value to yield function
});
} else {
resolve(medium);
}
}, 0);
}
} catch (e) {
// catch operation
return reject(e);
}
});
}
console.clear();
function* countUp() {
for (let i = 0; i < 10; i++) {
yield i;
}
}
const g = countUp();
console.log(g.next());
console.log(g.next());
const playground = asynk(function* () {
const result = yield Promise.resolve('hello');
return result;
});
playground()
.then((r) => console.log(r, 'yes'))
.catch(console.error);
const fetchFromNetwork = (val) => {
return Promise.resolve(val);
};
const failedFetchFromNetwork = (val) => {
return Promise.resolve(val);
// return Promise.reject(val);
};
const doSomething = asynk(function* (value) {
const result1 = yield fetchFromNetwork(value + '-1');
const result2 = yield fetchFromNetwork(value + '-2');
try {
const result3 = yield failedFetchFromNetwork().catch((err) => {
console.error(err);
});
} catch (error) {
console.error('Error fetching from network');
}
return result1 + " " + result2;
});
doSomething('http://google.com')
.then((r) => console.log(`Got result: ${r}`))
.catch(console.error);
// 打印结果:
// { value: 0, done: false }
// { value: 1, done: false }
// 'hello' 'yes'
// 'Got result: http://google.com-1 http://google.com-2'
思路:先使用一个while循环遍历generator生成器收集next次数,然后for循环再遍历generator生成器,前后传递生成器Promise.then得到的值,诀窍是使用setTimeout属于宏队列,promise属于微队列,同一次事件循环中setTimeout总会先于promise执行这一JS异步编程特性。
不足之处:generator生成器函数会被执行两次,如果在其中有声明console的话,会让人觉得有些奇怪,但是最终的返回值结果倒是正确。
含类型的TypeScript版本:
interface Generator<T = unknown, TReturn = any, TNext = unknown>
extends Iterator<T, TReturn, TNext> {
// NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
return(value: TReturn): IteratorResult<T, TReturn>;
throw(e: any): IteratorResult<T, TReturn>;
[Symbol.iterator](): Generator<T, TReturn, TNext>;
}
interface IteratorYieldResult<TYield> {
done?: false;
value: TYield;
}
interface IteratorReturnResult<TReturn> {
done: true;
value: TReturn;
}
type IteratorResult<T, TReturn = any> =
| IteratorYieldResult<T>
| IteratorReturnResult<TReturn>;
function asynk(fn: (...args: any) => Generator<Promise<any>, any, string>) {
// YOUR CODE HERE
return (...args: any) =>
new Promise((resolve, reject) => {
// Initialize the generator function, which might have signatures,
// Extract the next() queue fisrt, then iterate another initialized generator
const runner = fn(...args);
try {
let promiseCallbackQueue = [
(res: any, slaveRunner: any) => slaveRunner.next(res).value,
];
let result = runner.next();
// First round, collect promiseCallback
while (!result.done && String(result.value) === '[object Promise]') {
promiseCallbackQueue.push(
(res, slaveRunner) => slaveRunner.next(res).value,
);
result = runner.next();
}
/**
Second round, iterate another generator promise chain and pass promised value, by using the trick of event loop
setTimeout -> MacroQueue
Promise resolve -> MicroQueue
For each setTimeout, its inner promise resolve will call in advance of the latter setTimeout, because of MicroQueue
Drawback: initialize the given generator for twice.
*/
let medium: Promise<any> | string;
let pcqLen = promiseCallbackQueue.length;
const slaveRunner = fn(...args);
for (let i = 0; i < pcqLen; i++) {
setTimeout(() => {
const pm = promiseCallbackQueue[i](medium, slaveRunner);
medium = pm; // To get the final yield value, would be Promise fisrt
if (String(pm) === '[object Promise]') {
pm.then((res: string) => {
medium = res; // get the input value to yield function
});
} else {
resolve(medium);
}
}, 0);
}
} catch (e) {
// catch operation
return reject(e);
}
});
}
console.clear();
function* countUp() {
for (let i = 0; i < 10; i++) {
yield i;
}
}
const g = countUp();
console.log(g.next());
console.log(g.next());
const playground = asynk(function* () {
const result = yield Promise.resolve('hello');
return result;
});
playground()
.then(r => console.log(r, 'yes'))
.catch(console.error);
const fetchFromNetwork = (val: string) => {
return Promise.resolve(val);
};
const failedFetchFromNetwork = (val: string) => {
return Promise.resolve(val);
// return Promise.reject(val);
};
const doSomething = asynk(function* (value) {
const result1 = yield fetchFromNetwork(value + '-1');
const result2 = yield fetchFromNetwork(value + '-2');
try {
const result3 = yield failedFetchFromNetwork(value).catch(err => {
console.error(err);
});
console.log(result3);
} catch (error) {
console.error('Error fetching from network');
}
return result1 + ' ' + result2;
});
doSomething('http://google.com')
.then(r => console.log(`Got result: ${r}`))
.catch(console.error);
// 打印结果:
// { value: 0, done: false }
// { value: 1, done: false }
// { result3: undefined }
// 'hello' 'yes'
// { result3: 'http://google.com' }
// 'Got result: http://google.com-1 http://google.com-2'
处理原则: 让TypeScript校验不显红即可, 适当使用any和string.
其他实现参考:
Babel 7,面向chrome 54
输入:
const fetchFromNetwork = (val) => {
return Promise.resolve(val);
};
const failedFetchFromNetwork = (val) => {
return Promise.resolve(val);
// return Promise.reject(val);
};
async function doSomething(value) {
const result1 = await fetchFromNetwork(value + '-1');
const result2 = await fetchFromNetwork(value + '-2');
try {
const result3 = await failedFetchFromNetwork();
} catch (error) {
console.error('Error fetching from network');
}
return result1 + ' ' + result2;
}
doSomething('http://google.com')
.then(r => console.log(`Got result: ${r}`))
.catch(console.error)
输出:
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
try {
var info = gen[key](arg);
var value = info.value;
} catch (error) {
reject(error);
return;
}
if (info.done) {
resolve(value);
} else {
Promise.resolve(value).then(_next, _throw);
}
}
function _asyncToGenerator(fn) {
return function () {
var self = this,
args = arguments;
return new Promise(function (resolve, reject) {
var gen = fn.apply(self, args);
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
}
function _throw(err) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
}
_next(undefined);
});
};
}
const fetchFromNetwork = (val) => {
return Promise.resolve(val);
};
const failedFetchFromNetwork = (val) => {
return Promise.resolve(val);
// return Promise.reject(val);
};
function doSomething(_x) {
return _doSomething.apply(this, arguments);
}
function _doSomething() {
_doSomething = _asyncToGenerator(function* (value) {
const result1 = yield fetchFromNetwork(value + '-1');
const result2 = yield fetchFromNetwork(value + '-2');
try {
const result3 = yield failedFetchFromNetwork();
} catch (error) {
console.error('Error fetching from network');
}
return result1 + ' ' + result2;
});
return _doSomething.apply(this, arguments);
}
doSomething('http://google.com')
.then((r) => console.log(`Got result: ${r}`))
.catch(console.error);
精华之处:_next和_throw函数的递归调用。
TypeScript 4.9.5,面向ES2015
输入:
const fetchFromNetwork = (val) => {
return Promise.resolve(val);
};
const failedFetchFromNetwork = (val) => {
return Promise.resolve(val);
// return Promise.reject(val);
};
async function doSomething(value) {
const result1 = await fetchFromNetwork(value);
const result2 = await fetchFromNetwork(value);
try {
const result3 = await failedFetchFromNetwork();
} catch (error) {
console.error('Error fetching from network');
}
return result1 + result2;
}
doSomething('http://google.com')
.then(r => console.log(`Got result: ${r}`))
.catch(console.error)
输出:
var __awaiter =
(this && this.__awaiter) ||
function (thisArg, _arguments, P, generator) {
function adopt(value) {
return value instanceof P
? value
: new P(function (resolve) {
resolve(value);
});
}
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
}
function rejected(value) {
try {
step(generator['throw'](value));
} catch (e) {
reject(e);
}
}
function step(result) {
result.done
? resolve(result.value)
: adopt(result.value).then(fulfilled, rejected);
}
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
const fetchFromNetwork = (val) => {
return Promise.resolve(val);
};
const failedFetchFromNetwork = (val) => {
return Promise.resolve(val);
// return Promise.reject(val);
};
function doSomething(value) {
return __awaiter(this, void 0, void 0, function* () {
const result1 = yield fetchFromNetwork(value + '-1');
const result2 = yield fetchFromNetwork(value + '-2');
try {
const result3 = yield failedFetchFromNetwork();
} catch (error) {
console.error('Error fetching from network');
}
return result1 + ' ' + result2;
});
}
doSomething('http://google.com')
.then((r) => console.log(`Got result: ${r}`))
.catch(console.error);
精华之处: step函数的递归调用.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。