1
头图

实现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“的注意事项:

  1. 它接收一个generator生成器函数并返回一个新函数;
  2. 当返回的函数被调用时,它应该返回一个Promise期约。Promise期约应当对generator生成器函数的返回值有所处理;
  3. 返回函数的类型特征应该和传入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

Babel Repl

输入:

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

TypeScript Playground

输入:

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函数的递归调用.


冒泡的马树
194 声望14 粉丝

曾痴迷于赛博朋克世界的代码玩家一枚,