這篇文章主要是梳理一下本身對阮一峯大神寫的關於async/await
文章,有寫得不對的地方以及理解得不對的地方,各位大佬請指錯!promise
簡單對比傳統異步
,promise異步
,async異步
異步
下文都會以setTimeout
來進行異步展現,方便理解。async
傳統的回調函數
setTimeout(callback,1000); function callback(){ console.log("拿到結果了!"); }
setTimeout
函數傳入了兩個參數(1000
/callback
),setTimeout
被調用的時候,主線程不會等待1秒,而是先執行別的任務。callback
這個函數就是一個回調函數,即當1秒後,主線程會從新調用callback
(這裏也再也不囉嗦去說event Loop方面的知識了
);oop
那麼,當咱們異步函數須要嵌套的時候呢。好比這種狀況:性能
setTimeout(function(){ console.log("第一個異步回調了!") setTimeout(function(){ console.log("第二個異步回調了!") setTimeout(function(){ console.log("第三個異步回調了!") setTimeout(function(){ console.log("第四個異步回調了!") setTimeout(function(){ console.log("第五個異步回調了!") },1000); },1000); },1000); },1000); },1000);
OK,想死不?線程
咱們用promise來處理設計
function timeout(ms) { return new Promise((resolve, reject) => { setTimeout(resolve, ms, "finish"); }); } timeout(2000) .then(value => { console.log("第一層" + value); return timeout(2000); }) .then(value => { console.log("第二層" + value); return timeout(2000); }) .then(value => { console.log("第三層" + value); return timeout(2000); }) .then(value => { console.log("第四層" + value); return timeout(2000); }) .then(value => { console.log("第五層" + value); return timeout(2000); }) .catch(err => { console.log(err); });
OK,好看點了!指針
可是仍是不方便!code
咱們用async/await來處理:
function timeout(ms) { return new Promise((resolve, reject) => { setTimeout(resolve, ms, "finish"); }); } async function asyncTimeSys(){ await timeout(1000); console.log("第一層異步結束!") await timeout(1000); console.log("第二層異步結束!") await timeout(1000); console.log("第三層異步結束!") await timeout(1000); console.log("第四層異步結束!") await timeout(1000); console.log("第五層異步結束!") return "all finish"; } asyncTimeSys().then((value)=>{ console.log(value); });
OK,舒服了!
在這個asyncTimeSys
函數裏面,全部的異步操做,寫的跟同步函數沒有什麼兩樣!
async
函數究竟是什麼?其實他就是Genarator函數(生成器函數)的語法糖而已!
- 內置執行器。
Generator 函數的執行必須靠執行器,因此纔有了co模塊,而async函數自帶執行器。也就是說,async函數的執行,與普通函數如出一轍。徹底不像 Generator 函數,須要調用next方法,或者用co模塊,才能真正執行,獲得最後結果。
- 更好的語義。
async和await,比起星號和yield,語義更清楚了。async表示函數裏有異步操做,await表示緊跟在後面的表達式須要等待結果。
- 更廣的適用性。
co模塊約定,yield命令後面只能是 Thunk 函數或 Promise 對象,而async函數的await命令後面,能夠是 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時等同於同步操做)。
- 返回值是 Promise。
async函數的返回值是 Promise 對象,這比 Generator 函數的返回值是 Iterator 對象方便多了。你能夠用then方法指定下一步的操做。
進一步說,async函數徹底能夠看做多個異步操做,包裝成的一個 Promise 對象,而await命令就是內部then命令的語法糖。
其實,async函數就是一個由Generator封裝的異步環境,其內部是經過交換函數執行權,以及thunk函數來實現的!
OK,咱們簡單的封裝一個:
function timeout(ms) { return new Promise((resolve, reject) => { setTimeout(resolve, ms, "finish"); }); } function *times(){ let result =yield timeout(1000); return "second next" } let gen = times(); //拿到生成器函數,gen能夠理解爲指針 let firstYield = gen.next(); //firstYield此時爲gen指針指向的第一個yield右邊的表達式,此時timeout(1000)被執行 console.log(firstYield); // firstYield = {value:Pomise,done:false}; //接下來就是將firstYield中的value裏的promise拿出來,做爲正常的Promise調用,以下: firstYield.value.then(()=>{ //當timeout異步結束以後,執行如下代碼,再將gen指針執行下一個yield,因爲如下沒有yield了,因此gen.next()的value爲return裏的東西 console.log("timeout finish"); console.log(gen.next()); //{value: "second next", done: true} }).catch((err)=>{ });
這樣封裝有什麼用呢,yield返回回來的東西,仍是得像promise那樣調用。
咱們先來看看同步的代碼,先讓它長得像async和await那樣子:
function* times() { yield console.log(1); yield console.log(2); yield console.log(3); return "second next"; } let gen = times(); let result = gen.next(); while (!result.done) { result = gen.next(); }
OK,很是像了,可是,這是同步的。異步請求必須得等到第一個yield執行完成以後,才能去執行第二個yield。咱們若是改爲異步,確定會形成無限循環。
那麼,times生成器裏面若是都是異步的話,咱們應該怎麼調用呢?
function timeout(ms) { return new Promise((resolve, reject) => { setTimeout(resolve, ms, "finish"); }); } function *times(){ yield timeout(2000); yield timeout(2000); yield timeout(2000); return "finish all!"; } let gen = times(); let gen1 = gen.next(); gen1.value.then(function(data){ console.log(data+" one"); let gen2 = gen.next(); gen2.value.then(function(data){ console.log(data+" two"); let gen3 = gen.next(); gen3.value.then(function(data){ console.log(data+" three"); }) }) });
仔細觀察能夠發現,其實每個value的.then()
方法都會傳入一個相同的回調函數,這意味着咱們可使用遞歸來流程化管理整個異步流程;
改造一下這個上邊的代碼;
function timeout(ms) { return new Promise((resolve, reject) => { setTimeout(resolve, ms, "finish"); }); } function* times() { yield timeout(2000); yield timeout(2000); yield timeout(2000); return "finish all!"; } function run(fn){ let gen = fn(); function next(){ console.log("finish"); let result = gen.next(); if(result.done) return; result.value.then(next); } next(); } run(times);
OK,如今咱們可使用run
函數,使得生成器函數times
裏的異步請求,一步接着一步往下執行。
那麼,這個run
函數裏邊的next究竟是什麼呢,它實際上是一個thunk函數
;
Thunk函數的誕生是源於一個編譯器設計的問題:求值策略,即函數的參數到底應該什麼時候求值。
看下邊的代碼,請思考何時進行求值:
var x = 1; function f(m) { return m * 2; } f(x + 5);
試問:x+5
這個表達式應該何時求值
x+5
的值,再將這個值(6
)傳入函數f
,例如c語言,這種作法的好處是實現比較簡單,可是有可能會形成性能損失。x+5
)傳入函數體,只在用到它的時候求值。OK,thunk
函數到底是什麼:
編譯器的傳名調用實現,每每就是將參數放到一個臨時函數之中,再將這個臨時函數轉入函數體,這個臨時函數就叫作Thunk函數。
將上邊的代碼進行改造:
var thunk = function () { return x + 5; }; function f(thunk) { return thunk() * 2; }
js中的傳名調用是什麼呢,與真正的thunk
有什麼區別呢?
JavaScript 語言是傳值調用,它的 Thunk 函數含義有所不一樣。在 JavaScript 語言中,Thunk 函數替換的不是表達式,而是多參數函數,將其替換成一個只接受回調函數做爲參數的單參數函數。
網上對於thunk
的演示都是使用的fs
模塊的readFile
方法來進行演示
// 正常版本的readFile(多參數版本) fs.readFile(fileName, callback); // Thunk版本的readFile(單參數版本) var Thunk = function (fileName) { return function (callback) { return fs.readFile(fileName, callback); }; }; var readFileThunk = Thunk(fileName); readFileThunk(callback);
其實,任何函數,只要參數有回調函數,就能寫成Thunk
函數的形式。下面是一個簡單的Thunk
函數轉換器。
讓咱們用setTimeout
來進行一次演示:
//正常版本的setTimeout; setTimeout(function(data){ console.log(data); },1000,"finish"); //thunk版本的setTimeout let thunk = function(time){ return function(callback){ return setTimeout(callback,time,"finish"); } } let setTimeoutChunk = thunk(1000); setTimeoutChunk(function(data){ console.log(data); });
如今回頭看一看用Generator函數封裝異步請求
這一節中最後一個實例中,咱們封裝的timeout
函數,他其實就是一個thunk
函數,我在那一節中沒有給你們說明這一條:
爲何Generator
裏面必須使用thunk
函數呢,由於咱們須要確保傳入的值只有一個,利用其回調函數,來進行遞歸自動控制Generator
函數的流程,接收和交還程序的執行權;