zoukankan      html  css  js  c++  java
  • 优雅的异步编程解决方案: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();
      });
    }
    
  • 相关阅读:
    CF-911E.Stack Sorting(栈)
    随机算法 && CodeForces
    CF-579D."Or" Game(或运算)
    CF-242E.XOR on Segment(异或线段树)
    莫队 && 洛谷 P1494 [国家集训队]小Z的袜子
    洛谷 P4168 [Violet]蒲公英(分块)
    分块 && 洛谷 P2801 教主的魔法
    启发式合并 && U41492 树上数颜色
    使用mysqlbinlog server远程备份binlog的脚本
    mysqldump备份过程中都干了些什么
  • 原文地址:https://www.cnblogs.com/Khadron/p/14541225.html
Copyright © 2011-2022 走看看