Pines-Cheng / blog

技术博客
https://pines-cheng.github.io/blog/
546 stars 42 forks source link

async/await 小结 #59

Open Pines-Cheng opened 5 years ago

Pines-Cheng commented 5 years ago

ES7 引入的 async/await 是 JavaScript 异步编程的一个重大改进,提供了在不阻塞主线程的情况下使用同步代码异步访问资源的能力。

Chrome 55 中默认情况下启用异步函数(Async functions),坦率地讲,这个特性相当实用。 可以利用它们像编写同步代码那样编写基于 Promise 的代码,而且还不会阻塞主线程,还能够大大提高代码的可读性。

Async/Await 起源

早在 2012 年微软的 C# 语言发布 5.0 版本时,就正式推出了 Async/Await 的概念,随后在 Python 和 Scala 中也相继出现了 Async/Await 的身影。再之后,才是我们今天讨论的主角,ES 2016 中正式提出了 Async/Await 规范。

其实在前端领域,也有不少类 Async/Await 的实现,其中不得不提到的就是知名网红之一的老赵写的 wind.js,站在今天的角度看,windjs 的设计和实现不可谓不超前。

Async/Await 实现

根据 Async/Await 的规范 中的描述 —— 一个 Async 函数总是会返回一个 Promise —— 不难看出 Async/Await 和 Promise 存在千丝万缕的联系。这也是为什么很多的同学都说,Async/Await 不过是一个语法糖。

单谈规范太枯燥,我们还是看看实际的代码。下面是一个最基础的 Async/Await 例子:

async function test() {
  const img = await fetch('tiger.jpg');
}

使用 Babel 转换后:

'use strict';

var test = function() {
    var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee() {
        var img;
        return regeneratorRuntime.wrap(function _callee$(_context) {
            while (1) {
                switch (_context.prev = _context.next) {
                    case 0:
                        _context.next = 2;
                        return fetch('tiger.jpg');

                    case 2:
                        img = _context.sent;

                    case 3:
                    case 'end':
                        return _context.stop();
                }
            }
        }, _callee, this);
    }));

    return function test() {
        return _ref.apply(this, arguments);
    };
}();

function _asyncToGenerator(fn) {
    return function() {
        var gen = fn.apply(this, arguments);
        return new Promise(function(resolve, reject) {
            function step(key, arg) {
                try {
                    var info = gen[key](arg);
                    var value = info.value;
                } catch (error) {
                    reject(error);
                    return;
                }
                if (info.done) {
                    resolve(value);
                } else {
                    return Promise.resolve(value).then(function(value) {
                        step("next", value);
                    }, function(err) {
                        step("throw", err);
                    });
                }
            }
            return step("next");
        });
    };
}

不难看出,Async/Await 的实现被转换成了基于 Promise 的调用。值得注意的是,原来只需 3 行代码即可解决的问题,居然被转换成了 52 行代码,这还是基于执行环境中已经存在 regenerator 的前提之一。如果要在兼容性尚不是非常理想的 Web 环境下使用,代码 overhead 的成本不得不纳入考虑。

返回值

无论是你否使用了 await,异步函数都会返回 Promise。该 Promise resolves 时返回异步函数返回的任何值,rejects 时返回异步函数抛出的任何值。

因此,对于:

// wait ms milliseconds
function wait(ms) {
  return new Promise(r => setTimeout(r, ms));
}

async function hello() {
  await wait(500);
  return 'world';
}

…调用 hello() 返回的 Promise 会在 fulfills 时返回 "world"。

async function foo() {
  await wait(500);
  throw Error('bar');
}

调用 foo() 返回的 Promise 会在 rejects 时返回 Error('bar')。

示例:流式传输响应

异步函数在更复杂示例中对比更加强烈。假设我们想在流式传输响应的同时记录数据块日志,并返回数据块最终大小。

以下是使用 Promise 编写的代码:

function getResponseSize(url) {
  return fetch(url).then(response => {
    const reader = response.body.getReader();
    let total = 0;

    return reader.read().then(function processResult(result) {
      if (result.done) return total;

      const value = result.value;
      total += value.length;
      console.log('Received chunk', value);

      return reader.read().then(processResult);
    })
  });
}

这段代码通过在 processResult 内递归调用来实现异步循环? 这样编写的代码可能让人觉看看起来“高大上”,但是实在是不太直观,谈不上简约优雅。

我们再用异步函数来改进上面这段代码:

async function getResponseSize(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  let result = await reader.read();
  let total = 0;

  while (!result.done) {
    const value = result.value;
    total += value.length;
    console.log('Received chunk', value);
    // get the next result
    result = await reader.read();
  }

  return total;
}

“高大上”的代码不见了,让人头疼的异步循环被替换成可靠却单调乏味的 while 循环, 但代码的可读性大大提高。

避免太过串行化

虽然 await 可以让你的代码看起来像是同步的,但请记住,它们仍然是异步的,要避免太过串行化。

async function series() {
  await wait(500);
  await wait(500);
  return "done!";
}

以上代码执行完毕需要 1000 毫秒,再看看这段代码:

async function parallel() {
  const wait1 = wait(500);
  const wait2 = wait(500);
  await wait1;
  await wait2;
  return "done!";
}

以上代码只需 500 毫秒就可执行完毕,因为两个 wait 是同时发生的。

示例:按顺序输出获取的数据

假定我们想获取一系列网址,并尽快按正确顺序将它们记录到日志中。

以下是使用 Promise 编写的代码:

function logInOrder(urls) {
  // fetch all the URLs
  const textPromises = urls.map(url => {
    return fetch(url).then(response => response.text());
  });

  // log them in order
  textPromises.reduce((chain, textPromise) => {
    return chain.then(() => textPromise)
      .then(text => console.log(text));
  }, Promise.resolve());
}

是的,没错,这里使用 reduce 来链接 Promise 序列。看起来很”高大上“,不过可读性嘛,见仁见智吧,我个人是不太喜欢这种需要 ”二次思考“ 的代码啦。

不过,如果使用异步函数改写以上代码,又容易让代码变得过于循序:

不推荐的编码方式 - 过于循序

async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}

代码简洁得多,但我的第二次获取要等到第一次获取读取完毕才能开始,以此类推。 其执行效率要比并行执行获取的 Promise 示例低得多。 幸运的是,还有一种理想的中庸之道:

推荐的编码方式 - 可读性强、并行效率高

async function logInOrder(urls) {
  // fetch all the URLs in parallel
  const textPromises = urls.map(async url => {
    const response = await fetch(url);
    return response.text();
  });

  // log them in sequence
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}

在本例中,以并行方式获取和读取网址,但将的 reduce 部分替换成标准单调乏味但可读性强的 for 循环。

缺点

异常处理

正如在上文中提到的,async 函数默认会返回一个 Promise,这也意味着 Promise 中存在的问题 async 函数也会遇到,那就是 —— 默认会静默的吞掉异常。

所以,虽然 Async/Await 能够使用 try...catch... 这种符合同步习惯的方式进行异常捕获,你依然不得不手动给每个 await 调用添加 try...catch... 语句,否则,async 函数返回的只是一个 reject 掉的 Promise 而已。

控制流

虽然处理异步问题的技术一直在进步,但是在实际工程实践中,我们对异步操作的需求也在不断扩展加深,这也是为什么各种 flow control 的库一直兴盛不衰的原因之一。

Async/Await 在处理异步问题的有一定的优越性,但也存在一些不足:

当然,站在 EMCA 规范的角度来看,有些需求可能比较少见,但是如果纳入规范中,也可以减少前端程序员在挑选异步流程控制库时的纠结了。

参考

fantasy-wc commented 5 years ago

good