寫在前面
本文首發於公眾號:【符合預期的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中的非同步操作有了更深入,更全面的認識。符合預期。