前言
本篇文章適合前端架構師,或者進階的前端開發人員;我在面試vmware前端架構師的時候,被問到關於callback,promise,generator,async-await的問題。
首先我們回顧一下javascript異步的發展歷程。
ES6 以前:
回調函數(callback);nodejs express 中常用,ajax中常用。
ES6:
promise對象; nodejs最早有bluebird promise的雛形,axios中常用。
generator函數;nodejs koa框架使用率很高。
ES7:
async/await語法; 當前最常用的異步語法,nodejs koa2 完全使用該語法。
回調函數callback
回調函數實際就是一個參數;將一個函數當做參數傳到另一個函數里,當那個函數執行完后,再執行傳進去的這個函數;這個過程就叫做回調。
回調字面也好理解,就是先處理本體函數,再處理回調的函數,舉個例子,方便大家理解。
function A(callback){ console.log("我是主體函數"); callback(); } function B(){ console.log("我是回調函數"); } A(B); /*輸出結果 我是主體函數 我是回調函數 */
上面的例子很好理解,首先執行主體函數A,打印結果:我是主題函數;然后執行回調函數callback 也就是B,打印結果:我是回調函數。
promise對象
promise 對象用於一個異步操作的最終完成(或最終失敗)及其結果的表示。
簡單地說就是處理一個異步請求。我們經常會做些斷言,如果我贏了你就嫁給我,如果輸了我就嫁給你之類的斷言。這就是promise的中文含義:斷言,一個成功,一個失敗。
舉個例子,方便大家理解:
promise構造函數的參數是一個函數,我們把它稱為處理器函數,處理器函數接收兩個函數reslove和reject作為其參數,當異步操作順利執行則執行reslove函數, 當異步操作中發生異常時,則執行reject函數。通過resolve傳入得的值,可以在then方法中獲取到,通過reject傳入的值可以在chatch方法中獲取到。
因為then和catch都返回一個相同的promise對象,所以可以進行鏈式調用。
function readFileByPromise("a.txt"){ //顯示返回一個promise對象 return new Promise((resolve,reject)=>{ fs.readFile(path,"utf8",function(err,data){ if(err) reject(err); else resolve(data); }) }) } //書寫方式二 readFileByPromise("a.txt").then( data =>{ //打印文件中的內容 console.log(data); }).catch( error =>{ //拋出異常, console.log(error); })
generator函數
ES6的新特性generator函數(面試的時候掛在這里),中文譯為生成器,在以前一個函數中的代碼要么被調用,要么不被調用,還不存在能暫停的情況,generator讓代碼暫停成待執行,定義一個生成器很簡單,在函數名前加個*號,使用上也與普通函數有區別。
舉個例子,方便大家理解:
function *Calculate(a,b){ let sum=a+b; console.log(sum); let sub=a-b; console.log(sub); }
上面便是一個簡單的generator聲明例子。
generator函數不能直接調用,直接調用generator函數會返回一個對象,只有調用該對象的next()方法才能執行函數里的代碼。
let gen=Calculate(2,7);
執行該函數:
gen.next(); /*打印 9 -5 */
其實單獨介紹generator並沒有太大的價值,要配合key yield,才能真正發揮generator的價值。yield能將生Generator函數的代碼邏輯分割成多個部分,下面改寫上面的生成器函數。
function *Calculate(a,b){ let sum=a+b; yield console.log(sum); let sub=a-b; yield console.log(sub); } let gen=Calculate(2,7); gen.next(); /*輸出 9*/
可以看到這段代碼執行到第一個yield處就停止了,如果要讓里邊所有的代碼都執行完就得反復調用next()方法
let gen=Calculate(2,7); gen.next(); gen.next(); /*輸出 9 -5*/
在用一個例子,來說明generator函數與回調函數的區別:
回調函數:
fs.readFile("a.txt",(err,data)=>{ if(!err){ console.log(data); fs.readFile("b.txt",(err,data)=>{ if(!err) console.log(data); }) } })
這是一個典型的回調嵌套,過多的回調嵌套造成代碼的可讀性和可維護性大大降低,形成了令人深惡痛絕的回調地獄,試想如果有一天讓你按順序讀取10個文件,那就得嵌套10層,再或者需求變更,讀取順序要變了先讀b.txt,再度a.txt那改來真的不要太爽。
generator函數:
function readFile(path) { fs.readFile(path,"utf8",function(err,data){ it.next(data); }) } function *main() { var result1 = yield readFile("a.txt"); console.log(result1); var result2 = yield readFile("b.txt"); console.log(result2); var result3 = yield readFile("c.txt"); console.log(result3); } var it = main(); it.next();
generator函數的強大在於允許你通過一些實現細節來將異步過程隱藏起來,依然使代碼保持一個單線程、同步語法的代碼風格。這樣的語法使得我們能夠很自然的方式表達我們程序的步驟/語句流程,而不需要同時去操作一些異步的語法格式。
async-await
async函數返回一個promise對象,如果在async函數中返回一個直接量,async會通過Promise.resolve封裝成Promise對象。
我們可以通過調用promise對象的then方法,獲取這個直接量。
async function test(){ return "Hello World"; } var result=test(); console.log(result); //打印Promise { 'Hello World' }
那如過async函數不返回值,又會是怎么樣呢?
async function test(){ } var result=test(); console.log(result); //打印Promise { undefined }
await會暫停當前async的執行,await會阻塞代碼的執行,直到await后的表達式處理完成,代碼才能繼續往下執行。
await后的表達式既可以是一個Promise對象,也可以是任何要等待的值。
如果await等到的是一個 Promise 對象,await 就忙起來了,它會阻塞后面的代碼,等着 Promise 對象 resolve,然后得到 resolve 的值,作為 await 表達式的運算結果。
上邊你看到阻塞一詞,不要驚慌,async/await只是一種語法糖,代碼執行與多個callback嵌套調用沒有區別,本質並不是同步代碼,它只是讓你思考代碼邏輯的時候能夠以同步的思維去思考,避開回調地獄,簡而言之-async/await是以同步的思維去寫異步的代碼,所以async/await並不會影響node的並發數,大家可以大膽的應用到項目中去!
如果它等到的不是一個 Promise 對象,那 await 表達式的運算結果就是它等到的東西。
舉個例子,方便大家理解:
function A() { return "Hello "; } async function B(){ return "World"; } async function C(){ //等待一個字符串 var s1=await A(); //等待一個promise對象,await的返回值是promise對象resolve的值,也就是"World" var s2=await B(); console.log(s1+s2); } C(); //打印"Hello World"