共计 5404 个字符,预计需要花费 14 分钟才能阅读完成。
写在前面
本文首发于公众号:【符合预期的 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’
});
Object.prototype.toString(aa) === ‘[object Object]’; // true
typeof aa === ‘function’; // 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 中的异步操作有了更深入,更全面的认识。符合预期。