Web架構師養成系列共15篇,每週更新一篇,主要分享、探討目前大前端領域(前端、後端、移動端)企業中正在用的各類成熟的、新的技術。部分文章也會分析一些框架的底層實現,讓咱們作到知其然知其因此然。前端
本篇爲第二篇,上一篇:撩課-Web架構師養成系列第一篇ajax
本篇文章閱讀須要時長:約15分鐘編程
關於"異步",咱們能夠這麼理解: 一個任務拆分紅兩段,先執行第一段,而後轉而執行其餘任務,等到某個時間點,再回過頭執行第二段。後端
好比,你要作土豆燉牛肉,當開始煮牛肉的時候發現土豆沒了,能夠先讓牛肉煮着,而後去買土豆、洗好、切好,再把土豆放到鍋裏一塊兒煮。api
這種不連續的執行,就叫作異步。相應地,若是是連續的執行,那麼就叫作同步。數組
異步編程的目標就是讓代碼的執行更加相似於同步編程,開發中比較經常使用的方式主要包括: 1) 回調函數實現 2) 發佈訂閱、通知 3) 事件監聽 4)Promise/A+ 和 生成器函數 5)async/await 在ES6以前,咱們更多地是使用回調函數來實現異步編程。
回調函數就是把任務拆解成兩部分,把任務的第二部份單獨寫在一個函數裏面,等到執行完其它任務從新執行這個任務的時候,就直接調用這個函數,從而達到異步效果。 案例以下: /** * 土豆燉牛肉 * @param step1 牛肉 * @param callback 回調 */ let cook = (step1, callback) => { // 1. 煮牛肉 console.log(`燒水煮${step1}`); // 2. 放入土豆(5秒後執行) setTimeout(() => { let step2 = '放入土豆'; callback(step2); }, 5000) }; // 1. 先煮牛肉 cook('牛肉', (data) => { console.log(data); }); // 2. 作其它事, 5s後放入土豆 console.log('買土豆'); console.log('洗土豆'); console.log('切土豆');
運行結果 雖然回調函數可以實現異步,可是回調函數存在如下問題: 1) 回調地獄問題 異步多級依賴的狀況下會層層嵌套,代碼難以閱讀的維護; 2)可能會形成多個異步在某一時刻獲取全部異步的結果; 3) 異步不支持try/catch 回調函數是在下一事件環中取出, 因此通常在回調函數的第一個參數都是用來預置錯誤對象 4)不能經過return返回結果
promise,承諾。在代碼中咱們能夠這麼理解:此處我先許下個承諾,過了必定時間後我帶給你一個結果。promise
那麼,在這一段時間中作什麼?
咱們能夠進行異步操做,好比請求網絡數據、耗時運算、讀寫本地文件等
1) Pending Promise對象實例建立時候的初始狀態 2) Fulfilled 成功的狀態 3) Rejected 失敗的狀態 好比:你發郵件給老闆說要加工資,這時候你就要"等待"他的郵件回覆,他能夠立馬給你回覆,若是贊成了,表示"成功";若是不一樣意,表示"失敗",固然他也能夠一直不一樣意;可是這期間不影響你作其它事情。 在實際開發中,咱們能夠經過then 方法,來指定Promise 對象的狀態改變時肯定執行的操做,resolve 時執行第一個函數(onFulfilled),reject 時執行第二個函數(onRejected)。 來,一塊兒認識下promise的幾種操做方式和經常使用方法: 構建Promise // promise的方法會馬上執行; // 兩個輸出都會打印 let promise = new Promise(() => { console.log('喜歡IT'); }); console.log('就上撩課'); promise也能夠表明將來的一個值 一個promise實例能夠屢次調用then,當成功後會將結果依次執行。 let promise = new Promise((resolve, reject) => { ajax.get(BASEURL + 'api/goods/', (err, data)=>{ if (err) return reject(err); resolve(data); }) }); promise.then(data => { console.log(data); }); promise.then(data => { console.log(data); }); promise也能夠表明一個不用返回的值 // 表明一個用於不會返回的值 let promise = new Promise((resolve, reject) => { }); promise.then(data => { console.log(data); }); Promise.resolve 返回一個Promise實例,這個實例處於resolve狀態。 Promise.resolve('成功獲取結果').then(data=>{ console.log(data); }); Promise.reject 返回一個Promise實例,這個實例處於reject狀態。 Promise.reject('獲取結果失敗').then(data=>{ console.log(data); },err=>{ console.log(err); }) Promise.race 該方法用於接收一個數組,數組內都是Promise實例,返回一個Promise實例,這個Promise實例的狀態轉移取決於參數的Promise實例的狀態變化。 當參數中任何一個實例處於resolve狀態時,返回的Promise實例會變爲resolve狀態。若是參數中任意一個實例處於reject狀態,返回的Promise實例變爲reject狀態。 Promise.race( [readFiles('./a.txt'), readFiles('./b.txt')]).then(data=>{ console.log({data}) },(err)=>{ console.log(err) });
咱們再用promise實現上面發郵件加工資的案例:微信
在必定時間後(假設5s後),老闆回覆了郵件,能夠是如下兩種狀況: let addWages = ()=>{ return new Promise((resolve, reject) => { setTimeout(function () { // 公司帳戶餘額 let currentMoney = 9999999999; // 公司帳戶餘額 > 100w if (currentMoney > 1000000) { resolve('贊成加薪'); } else { resolve('不一樣意加薪'); } }, 5000) }) }; addWages().then(data => { console.log(data); }, data => { console.log(data); }); // 運行結果:贊成加薪 狀況二 : 公司帳戶已經沒錢,無法加工資了,表現形式以下: let addWages = ()=>{ return new Promise((resolve, reject) => { throw new Error('你表現不夠優秀!'); }) }; addWages().then(data => { console.log(data); }, data => { console.log('這裏輸出:' + data); }); 咱們能夠採用then的第二個參數捕獲reject返回結果或者捕獲失敗,固然也能夠經過.catch函數進行捕獲。
前面的案例描述已經驗證了promise支持catch,此外,經過promise也可以返回結果給外部。咱們再一塊兒看看promise如何解決回調地獄和同步異步結果。網絡
案例場景:在文檔a.txt中存放正文檔b.txt的路徑,在文檔b.txt中存放正文檔c.txt的路徑, 咱們要取出文檔c.txt裏面的內容。 構造函數實現: /* a.txt -> b.txt -> c.txt -> 輸出內容*/ let fs = require('fs'); let readFiles = ()=>{ // 回調1 fs.readFile('./a.txt','utf8', (err,data)=>{ if(err) return console.log(err); // 回調2 fs.readFile(data,'utf8',function(err,data){ if(err) return console.log(err); // 回調3 fs.readFile(data,'utf8',function(err,data){ if(err) return console.log(err); console.log(data); }) }) }) }; /* 調用輸出結果: 喜歡IT, 就上撩課(itlike.com) */ readFiles(); 經過promise解決回調地獄: let fs = require('fs'); // 1. 初始化promise let readFiles =(filePath)=>{ return new Promise((resolve,reject)=>{ fs.readFile(filePath,'utf8',(err,data)=>{ if(err) return reject(err); resolve(data); }) }) }; // 2. 相似於鏈式的調用方式 readFiles('./a.txt').then((data)=>{ return readFile(data); }).then((data)=>{ // 獲取b.txt中內容 return readFile(data); }).then((data)=>{ // 輸出c.txt中內容 console.log(data) }).catch((err)=>{ console.log(err) }); 3.2 在同一時刻同步全部異步產生的結果 該場景在實際開發中有不少應用場景,好比:咱們要提交一個操做時,須要結合以前的兩個異步請求的結果才能進行。 再好比:你要進行下一個運算時,須要前面兩個異步運算的結果才能進行。咱們仍是經過讀取文件的案例來進行舉例。 常規方式實現: let fs = require('fs'); // 1. 統一輸出全部異步產生的結果 let allContent = {}; let logAllContent = (key,data)=>{ allContent[key] = data; if(Object.keys(allContent).length === 2){ console.log(allContent) } }; // 2. 分別異步讀取文件中的內容 fs.readFile('./a.txt', 'utf8', function (err, data) { if (err) return console.log(err); logAllContent(data); }); fs.readFile('./b.txt', 'utf8', function (err, data) { if (err) return console.log(err); logAllContent(data); }); 這樣的方式雖然解決了問題,可是你不知道最終結果是在哪一個異步函數中輸出,並且你須要在全部的異步函數中都去調用打印方法。 promise方式大大簡化: 藉助promise.all()方法,無論哪一個promise誰先完成,該方法會按照數組裏面的順序將結果返回。 let fs = require('fs'); let readFiles = (filePath)=>{ return new Promise(function(resolve,reject){ fs.readFile(filePath,'utf8', (err,data)=>{ if(err) return reject(err); resolve(data); }) }) }; Promise.all( [readFiles('./a.txt'), readFiles('./b.txt')] ).then(([data])=>{ console.log({data}) });
藉助Promise已經能夠幫助咱們很好解決異步編程的問題,但還不是那麼的行雲流水、一鼓作氣,咱們更但願編寫異步代碼可以像寫同步代碼同樣直觀、簡單。架構
在下一篇咱們會講些更好、更靈活的異步編程方案,敬請期待。獲取資料、交流可加我微信:yejh9522 一塊兒探討學習。