优雅的异步编程解决方案: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里的模块,主要是提供Generatorasync/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 函数很简单,跟普通函数相比,多了个 asyncawait关键字
  • 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();
  });
}
posted @ 2021-03-16 06:25  Khadron  阅读(282)  评论(0编辑  收藏  举报