Web架構師養成系列共15篇,每週更新一篇,主要分享、探討目前大前端領域(前端、後端、移動端)企業中正在用的各類成熟的、新的技術。部分文章也會分析一些框架的底層實現,讓咱們作到知其然知其因此然。前端
本篇爲第二篇,上一篇:撩課-Web架構師養成系列第一篇ajax
本篇文章閱讀須要時長:約15分鐘編程
關於"異步",咱們能夠這麼理解: 一個任務拆分紅兩段,先執行第一段,而後轉而執行其餘任務,等到某個時間點,再回過頭執行第二段。後端
好比,你要作土豆燉牛肉,當開始煮牛肉的時候發現土豆沒了,能夠先讓牛肉煮着,而後去買土豆、洗好、切好,再把土豆放到鍋裏一塊兒煮。api
這種不連續的執行,就叫作異步。相應地,若是是連續的執行,那麼就叫作同步。數組
異步編程的目標就是讓代碼的執行更加相似於同步編程,開發中比較經常使用的方式主要包括:promise
1) 回調函數實現
2) 發佈訂閱、通知
3) 事件監聽
4)Promise/A+ 和 生成器函數
5)async/await
複製代碼
在ES6以前,咱們更多地是使用回調函數來實現異步編程。bash
回調函數就是把任務拆解成兩部分,把任務的第二部份單獨寫在一個函數裏面,等到執行完其它任務從新執行這個任務的時候,就直接調用這個函數,從而達到異步效果。微信
案例以下:網絡
/**
* 土豆燉牛肉
* @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,承諾。在代碼中咱們能夠這麼理解:此處我先許下個承諾,過了必定時間後我帶給你一個結果。
那麼,在這一段時間中作什麼? 咱們能夠進行異步操做,好比請求網絡數據、耗時運算、讀寫本地文件等
1) Pending
Promise對象實例建立時候的初始狀態
2) Fulfilled
成功的狀態
3) Rejected
失敗的狀態
複製代碼
好比:你發郵件給老闆說要加工資,這時候你就要"等待"他的郵件回覆,他能夠立馬給你回覆,若是贊成了,表示"成功";若是不一樣意,表示"失敗",固然他也能夠一直不一樣意;可是這期間不影響你作其它事情。
在實際開發中,咱們能夠經過then 方法,來指定Promise 對象的狀態改變時肯定執行的操做,resolve 時執行第一個函數(onFulfilled),reject 時執行第二個函數(onRejected)。
來,一塊兒認識下promise的幾種操做方式和經常使用方法:
// promise的方法會馬上執行;
// 兩個輸出都會打印
let promise = new Promise(() => {
console.log('喜歡IT');
});
console.log('就上撩課');
複製代碼
一個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);
});
複製代碼
// 表明一個用於不會返回的值
let promise = new Promise((resolve, reject) => { });
promise.then(data => {
console.log(data);
});
複製代碼
返回一個Promise實例,這個實例處於resolve狀態。
Promise.resolve('成功獲取結果').then(data=>{
console.log(data);
});
複製代碼
返回一個Promise實例,這個實例處於reject狀態。
Promise.reject('獲取結果失敗').then(data=>{
console.log(data);
},err=>{
console.log(err);
})
複製代碼
該方法用於接收一個數組,數組內都是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)
});
複製代碼
該場景在實際開發中有不少應用場景,好比:咱們要提交一個操做時,須要結合以前的兩個異步請求的結果才能進行。
再好比:你要進行下一個運算時,須要前面兩個異步運算的結果才能進行。咱們仍是經過讀取文件的案例來進行舉例。
常規方式實現:
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 一塊兒探討學習。