【JS基础】从JavaScript中的for...of说起(下) - async和await
写在前面
本文首发于公众号:【符合预期的CoyPan】
在上一篇文章中,梳理了javascript中的两个重要概念:iterator和generator,并且介绍了两者在异步操作中的应用。
【JS基础】从JavaScript中的for...of说起(上) - iterator 和 generator
在异步操作中使用iterator和generator是一件比较费劲的事情,而ES2017给我们提供了更为简便的async和await。
async和await
async
mdn上说:async function
声明用于定义一个返回 AsyncFunction
对象的异步函数。异步函数是指通过事件循环异步执行的函数,它会通过一个隐式的 Promise
返回其结果。
简单来说,如果你在一个函数前面使用了async关键字,那么这个函数就会返回一个promise。如果你返回的不是一个promise,JavaScript也会自动把这个值"包装"成Promise的resolve值。例如:
// 返回一个promise async function aa() { return new Promise(resolve => { setTimeout(function(){ resolve('aaaaaa'); }, 1000); }); } aa().then(res => { console.log(res); // 1s后输出 'aaaaaa' }); typeof aa === 'function'; // true Object.prototype.toString(aa) === '[object AsyncFunction]'; // true Object.prototype.toString(aa()) === '[object Promise]'; // true // 返回一个非promise async function a() { return 1; } const b = a(); console.log(b); // Promise {<resolved>: 1} a().then(res => { console.log(res); // 1 })
当 async
函数抛出异常时,Promise
的 reject 方法也会传递这个异常值。例如下面的例子:
async function a(){ return bbb; } a() .then(res => { console.log(res); }) .catch( e => { console.log(e); // ReferenceError: bbb is not defined });
await
await
操作符用于等待一个Promise
对象。它只能在异步函数 async function
中使用。await 表达式会暂停当前 async function
的执行,等待 Promise 处理完成。若 Promise 正常处理(fulfilled),其回调的resolve函数参数作为 await 表达式的值,继续执行 async function
。若 Promise 处理异常(rejected),await 表达式会把 Promise 的异常原因抛出。另外,如果 await 操作符后的表达式的值不是一个 Promise,则返回该值本身。看下面的例子:
const p = function() { return new Promise(resolve => { setTimeout(function(){ resolve(1); }, 1000); }); }; const fn = async function() { const res = await p(); console.log(res); const res2 = await 2; console.log(res2); }; fn(); // 1s后,会输出1, 紧接着,会输出2 // 把await放在try catch中捕获错误 const p2 = function() { return new Promise(resolve => { console.log(ppp); resolve(); }); }; const fn2 = async function() { try { await p2(); } catch (e) { console.log(e); // ppp is not defined } }; fn2();
当代码执行到await语句时,会暂停执行,直到await后面的promise正常处理。这和我们之前讲到的generator一样,可以让代码在某个地方中断。只不过,在generator中,我们需要手动写代码去执行generator,而await则是像一个自带执行器的generator。某种程度上,我们可以理解为:await就是generator的语法糖。看下面的代码:
const p = function() { return new Promise(resolve, reject=>{ setTimeout(function(){ resolve(1); }, 1000); }); }; const f = async function() { const res = await p(); console.log(res); }
我们使用babel对这段代码进行转化,得到以下的代码:
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"); }); }; } var p = function p() { return new Promise(resolve, function (reject) { setTimeout(function () { resolve(1); }, 1000); }); }; var f = function () { var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() { var res; return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return p(); case 2: res = _context.sent; console.log(res); case 4: case "end": return _context.stop(); } } }, _callee, this); })); return function f() { return _ref.apply(this, arguments); }; }();
通过变量名可以看到,babel也是将async await转换成了generator来进行处理的。
任务队列
以下的场景其实是很常见的:
我们有一堆任务,我们需要按照一定的顺序执行这一堆任务,拿到最终的结果。这里,把这一堆任务称为一个任务队列。
js中的队列其实就是一个数组。
同步任务队列
任务队列中的函数都是同步函数。这种情况比较简单,我们可以采用reduce很方便的遍历。
const fn1 = function(i) { return i + 1; }; const fn2 = function(i) { return i * 2; }; const fn3 = function(i) { return i * 100; }; const taskList = [fn1, fn2, fn3]; let a = 1; const res = taskList.reduce((sum, fn) => { sum = fn(sum); return sum; }, a); console.log(res); // 400
异步任务队列
任务队列中的函数都是异步函数。这里,我们假设所有的函数都是以Promise的形式封装的。现在,需要依次执行队列中的函数。假设异步任务队列如下:
const fn1 = function() { return new Promise( resolve => { setTimeout(function(){ console.log('fn1'); resolve(); }, 2000); }); }; const fn2 = function() { return new Promise( resolve => { setTimeout(function(){ console.log('fn2'); resolve(); }, 1000); }); }; const fn3 = function() { console.log('fn3'); return Promise.resolve(1); }; const taskList = [fn1, fn2, fn3];
可以使用正常的for循环或者for...of... 来遍历数组,并且使用async await来执行代码(注:不要使用forEach,forEach不支持这种场景)
// for循环 (async function(){ for(let i = 0; i < taskList.length; i++) { await taskList[i](); } })(); // for..of.. (async function(){ for(let fn of taskList) { await fn(); } })();
koa2洋葱模型实现原理
koa2,大家都不陌生了。koa2的洋葱模型,是怎么实现的呢?先来看下面的代码:
const Koa = require('koa'); const app = new Koa(); // logger app.use(async (ctx, next) => { console.log(1); await next(); console.log(2); const rt = ctx.response.get('X-Response-Time'); console.log(`${ctx.method} ${ctx.url} - ${rt}`); }); // x-response-time app.use(async (ctx, next) => { console.log(3); const start = Date.now(); await next(); console.log(4); const ms = Date.now() - start; ctx.set('X-Response-Time', `${ms}ms`); }); // response app.use(async ctx => { console.log(5); ctx.body = 'Hello World'; }); app.listen(3000); // 访问node时,代码输出如下: // 1 // 3 // 5 // 4 // 2 // GET / - 6ms
其实实现起来很简单,app.use就是将所有的回调函数都塞进了一个任务队列里面,调用await next()的时候,会直接执行队列里面下一个任务,直到下一个任务执行完成,才会接着执行后续的代码。我们来简单实现一下最基本的逻辑:
class TaskList { constructor(){ this.list = []; } use(fn) { fn && this.list.push(fn); } start() { const self = this; let idx = -1; const exec = function() { idx++; const fn = self.list[idx]; if(!fn) { return Promise.resolve(); } return Promise.resolve(fn(exec)) } exec(); } } const test1 = function() { return new Promise( resolve => { setTimeout(function(){ console.log('fn1'); resolve(); }, 2000); }); }; const taskList = new TaskList(); taskList.use(async next => { console.log(1); await next(); console.log(2); }); taskList.use(async next => { console.log(3); await test1(); await next(); console.log(4); }); taskList.use(async next => { console.log(5); await next(); console.log(6); }); taskList.use(async next => { console.log(7); }); taskList.start(); // 输出: 1、3、fn1、5、7、6、4、2
写在后面
可以看到,使用async和await进行异步操作,可以使代码看起来更为清晰,简单。我们可以用同步代码的方式来书写异步代码。本文还探究了前端开发中很常见的任务队列的相关问题。通过本文和上一篇文章,我自己也对js中的异步操作有了更深入,更全面的认识。符合预期。