摘要:在語言中,函數替換的不是表達式,而是多參數函數,將其替換成一個只接受回調函數作為參數的單參數函數。為什么里面必須使用函數呢,因為我們需要確保傳入的值只有一個,利用其回調函數,來進行遞歸自動控制函數的流程,接收和交還程序的執行權
前言
這篇文章主要是梳理一下自己對阮一峰大神寫的關于async/await文章,有寫得不對的地方以及理解得不對的地方,各位大佬請指錯!
對比簡單對比傳統異步,promise異步,async異步
下文都會以setTimeout來進行異步展示,方便理解。
傳統的回調
setTimeout(callback,1000); function callback(){ console.log("拿到結果了!"); }
setTimeout函數傳入了兩個參數(1000/callback),setTimeout被調用的時候,主線程不會等待1秒,而是先執行別的任務。callback這個函數就是一個回調函數,即當1秒后,主線程會重新調用callback(這里也不再啰嗦去說event Loop方面的知識了);
那么,當我們異步函數需要嵌套的時候呢。比如這種情況:
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,好看點了!
但是還是不方便!
我們用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的原型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函數來實現的!
用Generator函數封裝異步請求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函數Thunk函數的誕生是源于一個編譯器設計的問題:求值策略,即函數的參數到底應該何時求值。
看下邊的代碼,請思考什么時候進行求值:
var x = 1; function f(m) { return m * 2; } f(x + 5);
試問:x+5這個表達式應該什么時候求值
傳值調用(call by value),即在進入函數體之間,先計算x+5的值,再將這個值(6)傳入函數f,例如c語言,這種做法的好處是實現比較簡單,但是有可能會造成性能損失。
傳名調用(call by name),即直接將表達式(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函數,我在那一節中沒有給大家說明這一條:
yield命令后面的必須是 Thunk 函數。
為什么Generator里面必須使用thunk函數呢,因為我們需要確保傳入的值只有一個,利用其回調函數,來進行遞歸自動控制Generator函數的流程,接收和交還程序的執行權;
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/94660.html
摘要:事件循環背景是一門單線程非阻塞的腳本語言,單線程意味著,代碼在執行的任何時候,都只有一個主線程來處理所有的任務。在意識到該問題之際,新特性中的可以讓成為一門多線程語言,但實際開發中使用存在著諸多限制。這個地方被稱為執行棧。 事件循環(Event Loop) 背景 JavaScript是一門單線程非阻塞的腳本語言,單線程意味著,JavaScript代碼在執行的任何時候,都只有一個主線程來...
摘要:三即生成器,它是生成器函數返回的一個對象,是中提供的一種異步編程解決方案而生成器函數有兩個特征,一是函數名前帶星號,二是內部執行語句前有關鍵字調用一個生成器函數并不會馬上執行它里面的語句,而是返回一個這個生成器的迭代器對象。 文章來自微信公眾號:前端工坊(fe_workshop),不定期更新有趣、好玩的前端相關原創技術文章。 如果喜歡,請關注公眾號:前端工坊版權歸微信公眾號所有,轉載請...
摘要:由于是單線程的,這些方法就會按順序被排列在一個單獨的地方,這個地方就是所謂執行棧。事件隊列每次僅執行一個任務,在該任務執行完畢之后,再執行下一個任務。 Event Loop 是 JavaScript 異步編程的核心思想,也是前端進階必須跨越的一關。同時,它又是面試的必考點,特別是在 Promise 出現之后,各種各樣的面試題層出不窮,花樣百出。這篇文章從現實生活中的例子入手,讓你徹底理解 E...
摘要:如何避免內存泄露內存泄漏很常見,特別是前端去寫后端程序,閉包運用不當,循環引用等都會導致內存泄漏。有的時候很難避免一些可能產生內存泄漏的問題,可以利用每次調用都在一個沙箱環境下調用,用完回收調。 某一天用戶反饋打開的頁面白屏幕,怎么定位到產生錯誤的原因呢?日常某次發布怎么確定發布會沒有引入bug呢?此時捕獲到代碼運行的bug并上報是多么的重要。 既然捕獲錯誤并上報是日常開發中不可缺少的...
閱讀 2571·2021-11-24 09:38
閱讀 2601·2019-08-30 15:54
閱讀 915·2019-08-30 15:52
閱讀 1909·2019-08-30 15:44
閱讀 2713·2019-08-30 13:48
閱讀 768·2019-08-29 16:21
閱讀 996·2019-08-29 14:03
閱讀 2212·2019-08-28 18:15