本文首發於公衆號:【符合預期的CoyPan】
在上一篇文章中,梳理了javascript中的兩個重要概念:iterator和generator,而且介紹了二者在異步操做中的應用。javascript
【JS基礎】從JavaScript中的for...of提及(上) - iterator 和 generator前端
在異步操做中使用iterator和generator是一件比較費勁的事情,而ES2017給咱們提供了更爲簡便的async和await。java
mdn上說:async function
聲明用於定義一個返回 AsyncFunction
對象的異步函數。異步函數是指經過事件循環異步執行的函數,它會經過一個隱式的 Promise
返回其結果。node
簡單來講,若是你在一個函數前面使用了async關鍵字,那麼這個函數就會返回一個promise。若是你返回的不是一個promise,JavaScript也會自動把這個值"包裝"成Promise的resolve值。例如:segmentfault
// 返回一個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
操做符用於等待一個Promise
對象。它只能在異步函數 async function
中使用。await 表達式會暫停當前 async function
的執行,等待 Promise 處理完成。若 Promise 正常處理(fulfilled),其回調的resolve函數參數做爲 await 表達式的值,繼續執行 async function
。若 Promise 處理異常(rejected),await 表達式會把 Promise 的異常緣由拋出。另外,若是 await 操做符後的表達式的值不是一個 Promise,則返回該值自己。看下面的例子: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的語法糖。看下面的代碼:babel
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對這段代碼進行轉化,獲得如下的代碼:app
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來進行處理的。koa
如下的場景實際上是很常見的:
咱們有一堆任務,咱們須要按照必定的順序執行這一堆任務,拿到最終的結果。這裏,把這一堆任務稱爲一個任務隊列。
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的洋蔥模型,是怎麼實現的呢?先來看下面的代碼:
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(); // 輸出: 一、三、fn一、五、七、六、四、2
能夠看到,使用async和await進行異步操做,可使代碼看起來更爲清晰,簡單。咱們能夠用同步代碼的方式來書寫異步代碼。本文還探究了前端開發中很常見的任務隊列的相關問題。經過本文和上一篇文章,我本身也對js中的異步操做有了更深刻,更全面的認識。符合預期。