Javascript語言的執行環境是單線程。即一次只能完成一個任務。如有多個任務則需排隊逐個執行——前一個任務完成,再執行後一個任務。javascript
這種執行模式實現簡單,執行環境相對單純。但隨着前端業務日漸複雜,事務和請求等日漸增多,這種單線程執行方式在複雜的業務下勢必效率低下,只要有一個任務耗時很長,後面的任務都必須排隊等着,會拖延整個程序的執行。常見的瀏覽器無響應(假死),每每就是由於某一段Javascript代碼長時間運行(好比死循環),致使整個頁面卡在這個地方,其餘任務沒法執行。css
爲避免和解決這種問題,JS語言將任務執行模式分爲異步和同步。同步模式」就是上一段的模式,後一個任務等待前一個任務結束,而後再執行,程序的執行順序與任務的排列順序是一致的、同步的;」異步模式」則徹底不一樣,每個任務有一個或多個回調函數(callback),前一個任務結束後,不是執行後一個任務,而是執行回調函數,後一個任務則是不等前一個任務結束就執行,因此程序的執行順序與任務的排列順序是不一致的、異步的。前端
「異步模式」很是重要。在瀏覽器端,耗時很長的操做都應該異步執行,避免瀏覽器失去響應,最好的例子就是Ajax操做。在服務器端,」異步模式」甚至是惟一的模式,由於執行環境是單線程的,若是容許同步執行全部http請求,服務器性能會急劇降低,很快就會失去響應。java
異步編程最基本方法。git
首先須要聲明,回調函數只是一種實現,並非異步模式特有的實現。回調函數一樣能夠運用到同步(阻塞)的場景下以及其餘一些場景。github
回調函數的英文定義:A callback is a function that is passed as an argument to another function and is executed after its parent function has completed。ajax
字面上的理解,回調函數就是一個參數,將這個函數做爲參數傳到另外一個函數裏面,當那個函數執行完以後,再執行傳進去的這個函數。這個過程就叫作回調。編程
在JavaScript中,回調函數具體的定義爲: 函數A做爲參數(函數引用)傳遞到另外一個函數B中,而且這個函數B執行函數A。咱們就說函數A叫作回調函數。若是沒有名稱(函數表達式),就叫作匿名回調函數。數組
用一個通俗的生活例子比喻一下就是:約會結束後你送你女友回家,離別時,你確定會說:「到家了給我發條信息,我很擔憂你。」 而後你女友回家之後還真給你發了條信息。其實這就是一個回調的過程。你留了個參數函數(要求女友給你發條信息)給你女友,而後你女友回家,回家的動做是主函數。她必須先回到家之後,主函數執行完了,再執行傳進去的函數,而後你就收到一條信息了。promise
假定有兩個函數f1和f2,後者等待前者的執行結果。
f1();
f2();
複製代碼
若f1是一個很耗時的任務,能夠考慮改寫f1,把f2寫成f1的回調函數。
function f1(callback){setTimeout(function () {// f1的任務代碼callback();}, 1000);}複製代碼
執行代碼就變成下面這樣:
f1(f2);
複製代碼
採用這種方式,咱們把同步操做變成了異步操做,f1不會堵塞程序運行,至關於先執行程序的主要邏輯,將耗時的操做推遲執行。
另外一個例子:
//定義主函數,回調函數做爲參數
function A(callback) {
callback();
console.log('我是主函數');
}
//定義回調函數
function B(){
setTimeout("console.log('我是回調函數')", 3000);//模仿耗時操做
}
//調用主函數,將函數B傳進去
A(B);
//輸出結果
我是主函數
我是回調函數
複製代碼
上面的代碼中,咱們先定義了主函數和回調函數,而後再去調用主函數,將回調函數傳進去。
定義主函數的時候,咱們讓代碼先去執行callback()回調函數,但輸出結果倒是後輸出回調函數的內容。這就說明了主函數不用等待回調函數執行完,能夠接着執行本身的代碼。因此通常回調函數都用在耗時操做上面。好比ajax請求,好比處理文件等。
再來一個更俗的例子:
<strong>問:你有事去隔壁寢室找同窗,發現人不在,你怎麼辦呢?</strong><strong>方法1</strong>,每隔幾分鐘再去趟隔壁寢室,看人在不<strong>方法2</strong>,拜託與他同寢室的人,看到他回來時叫一下你 前者是輪詢,後者是回調。 那你說,我直接在隔壁寢室等到同窗回來能夠嗎? 能夠啊,只不過這樣本來你能夠省下時間作其餘事,如今必須浪費在等待上了。把原來的非阻塞的異步調用變成了阻塞的同步調用。 JavaScript的回調是在異步調用場景下使用的,使用回調性能好於輪詢。複製代碼
對於回調函數,通常在同步情境下是最後執行的,而在異步情境下有可能不執行,由於事件沒有被觸發或者條件不知足,因此請忽略上上個例子中的小問題,並非必定回調函數就要執行。
同時補充回調函數應用場合和優缺點:
回調函數這種方式的優勢是比較容易理解,能夠綁定多個事件,每一個事件能夠指定多個回調函數,並且能夠」去耦合「,有利於實現模塊化。缺點是整個程序都要變成事件驅動型,運行流程會變得很不清晰。
隨着ES6標準的發佈,處理異步數據流的解決方案又有了新的變化。promise就是這其中的一個。咱們都知道,在傳統的ajax請求中,當異步請求之間的數據存在依賴關係的時候,就可能產生很難看的多層回調,這樣會使代碼邏輯很容易形成混亂不便於閱讀和後期維護,俗稱」回調地獄」(callback hell)。另外一方面,每每錯誤處理的代碼和正常的業務代碼耦合在一塊兒,形成代碼會極其難看。爲了讓編程更美好,咱們就須要引入promise
來下降異步編程的複雜性。
因此某種程度上說,promise是對上面說到的回調函數處理異步編程的一個進階方案。首先Promise是CommandJS提出的一種規範,其目的是爲異步編程提供統一接口。
簡單說,Promise的思想是,每個異步任務返回一個Promise對象,該對象有一個then方法,容許指定回調函數。形如這種形式:
f1().then(f2);
複製代碼
對於函數f1,使用Jquery實現如下改寫:
function f1(){var dfd = $.Deferred();setTimeout(function () {// f1的任務代碼dfd.resolve();}, 500);return dfd.promise;}複製代碼
這樣寫的優勢在於,回調函數變成了鏈式寫法,程序的流程能夠看得很清楚,並且有一整套的配套方法,能夠實現許多強大的功能。這也就是Promise處理異步編程的其中的一個方便之處。
再舉一個制定多個回調函數的例子,其形式爲:
f1().then(f2).then(f3);
複製代碼
當指定發生錯誤時的回調函數,其形式爲:
f1().then(f2).fail(f3);
複製代碼
在此補充一點,promise中,若是一個任務已經完成,再添加回調函數,該回調函數會當即執行。因此,你不用擔憂是否錯過了某個事件或信號。這種方法的缺點就是編寫和理解,都相對比較難。
展開談論一下Promise:Promise實際上就是一個特殊的Javascript對象,反映了」異步操做的最終值」。」Promise」直譯過來有預期的意思,所以,它也表明了某種承諾,即不管你異步操做成功與否,這個對象最終都會返回一個值給你。
代碼示例
const promise = new Promise((resolve, reject) => {
$.ajax('https://github.com/users', (value) => {
resolve(value);
}).fail((err) => {
reject(err);
});
});
promise.then((value) => {
console.log(value);
},(err) => {
console.log(err);
});
//也能夠採起下面這種寫法
promise.then(value => console.log(value)).catch(err => console.log(err));
複製代碼
上面的例子,會在Ajax請求成功後調用resolve
回調函數來處理結果,若是請求失敗則調用reject
回調函數來處理錯誤。Promise對象內部包含三種狀態,分別爲pending,fulfilled和rejected。這三種狀態能夠類比於咱們日常在ajax數據請求過程的pending,success,error。一開始請求發出後,狀態是Pending,表示正在等待處理完畢,這個狀態是中間狀態並且是單向不可逆的。成功得到值後狀態就變爲fulfilled,而後將成功獲取到的值存儲起來,後續能夠經過調用then
方法傳入的回調函數來進一步處理。而若是失敗了的話,狀態變爲rejected,錯誤能夠選擇拋出(throw)或者調用reject
方法來處理。
Promise基本語法以下
Promise實例必須實現then這個方法
then()必須能夠接收兩個函數做爲參數
then()返回的必須是一個Promise實例
eg
<script src="https://cdn.bootcss.com/bluebird/3.5.1/bluebird.min.js"></script>//若是低版本瀏覽器不支持Promise,經過cdn這種方式
<script type="text/javascript">
function loadImg(src) {
var promise = new Promise(function (resolve, reject) {
var img = document.createElement('img')
img.onload = function () {
resolve(img)
}
img.onerror = function () {
reject('圖片加載失敗')
}
img.src = src
})
return promise
}
var src = 'https://www.imooc.com/static/img/index/logo_new.png'
var result = loadImg(src)
result.then(function (img) {
console.log(1, img.width)
return img
}, function () {
console.log('error 1')
}).then(function (img) {
console.log(2, img.height)
})
</script>
做者:浪裏行舟
連接:https://juejin.im/post/5b1962616fb9a01e7c2783a8
來源:掘金
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
複製代碼
Promise還能夠作更多的事情,好比,有若干個異步任務,須要先作任務1,若是成功後再作任務2,任何任務失敗則再也不繼續並執行錯誤處理函數。要串行執行這樣的異步任務,不用Promise須要寫一層一層的嵌套代碼。
有了Promise,咱們只須要簡單地寫job1.then(job2).then(job3).catch(handleError);
其中job一、job2和job3都是Promise對象。
好比咱們想實現第一個圖片加載完成後,再加載第二個圖片,若是其中有一個執行失敗,就執行錯誤函數:
var src1 = 'https://www.imooc.com/static/img/index/logo_new.png'
var result1 = loadImg(src1) //result1是Promise對象
var src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg'
var result2 = loadImg(src2) //result2是Promise對象
result1.then(function (img1) {
console.log('第一個圖片加載完成', img1.width)
return result2 // 鏈式操做
}).then(function (img2) {
console.log('第二個圖片加載完成', img2.width)
}).catch(function (ex) {
console.log(ex)
})複製代碼
Promise的經常使用方法
除了串行執行若干異步任務外,Promise還能夠並行執行異步任務。試想一個頁面聊天系統,咱們須要從兩個不一樣的URL分別得到用戶的我的信息和好友列表,這兩個任務是能夠並行執行的,用Promise.all()實現以下:
var p1 = new Promise(function (resolve, reject) {
setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 600, 'P2');
});
// 同時執行p1和p2,並在它們都完成後執行then:
Promise.all([p1, p2]).then(function (results) {
console.log(results); // 得到一個Array: ['P1', 'P2']
});複製代碼
有些時候,多個異步任務是爲了容錯。好比,同時向兩個URL讀取用戶的我的信息,只須要得到先返回的結果便可。這種狀況下,用Promise.race()實現:
var p1 = new Promise(function (resolve, reject) {
setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 600, 'P2');
});
Promise.race([p1, p2]).then(function (result) {
console.log(result); // 'P1'
});複製代碼
因爲p1執行較快,Promise的then()將得到結果'P1'。p2仍在繼續執行,但執行結果將被丟棄。
總結:Promise.all接受一個promise對象的數組,待所有完成以後,統一執行success;
Promise.race接受一個包含多個promise對象的數組,只要有一個完成,就執行success。
對上面的例子作下修改,加深對這二者的理解:
var src1 = 'https://www.imooc.com/static/img/index/logo_new.png'
var result1 = loadImg(src1)
var src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg'
var result2 = loadImg(src2)
Promise.all([result1, result2]).then(function (datas) {
console.log('all', datas[0])//<img src="https://www.imooc.com/static/img/index/logo_new.png">
console.log('all', datas[1])//<img src="https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg">
})
Promise.race([result1, result2]).then(function (data) {
console.log('race', data)//<img src="https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg">
})複製代碼
若是咱們組合使用Promise,就能夠把不少異步任務以並行和串行的方式組合起來執行。
Promise.reject(reason): 返回一個新的promise
對象,用reason值直接將狀態變爲rejected
。
const promise2 = new Promise((resolve, reject) => {
reject('Failed');
});
const promise2 = Promise.reject('Failed');
複製代碼
上面兩種寫法是等價的。
Promise.resolve(value): 返回一個新的promise對象,這個promise對象是被resolved的。與reject相似,下面這兩種寫法也是等價的。
const promise2 = new Promise((resolve, reject) => {
resolve('Success');
});
const promise2 = Promise.resolve('Success');
複製代碼
then 利用這個方法訪問值或者錯誤緣由。其回調函數就是用來處理異步處理返回值的。
catch 利用這個方法捕獲錯誤,並處理。
簡介
語法
用promise示例和asyn/await示例兩段代碼演示:
promise
const makeRequest = () =>
getJSON()
.then(data => {
console.log(data)
return "done"
})
makeRequest()
複製代碼
async/await
const makeRequest = async () => {
console.log(await getJSON())
return "done"
}
makeRequest()
複製代碼
它們有一些細微不一樣:
函數前面多了一個aync關鍵字。await關鍵字只能用在aync定義的函數內。async函數會隱式地返回一個promise,該promise的reosolve值就是函數return的值。(示例中reosolve值就是字符串」done」)
第1點暗示咱們不能在最外層代碼中使用await,由於不在async函數內。
// 不能在最外層代碼中使用await
await makeRequest()
// 這是會出事情的
makeRequest().then((result) => {
// 代碼
})
複製代碼
await getJSON()表示console.log會等到getJSON的promise成功reosolve以後再執行。
相對於promise,async/await的優點有哪些
1.簡潔
由示例可知,使用Async/Await明顯節約了很多代碼。咱們不須要寫.then,不須要寫匿名函數處理Promise的resolve值,也不須要定義多餘的data變量,還避免了嵌套代碼。這些小的優勢會迅速累計起來,這在以後的代碼示例中會更加明顯。
2.錯誤處理
Async/Await讓try/catch能夠同時處理同步和異步錯誤。在下面的promise示例中,try/catch不能處理JSON.parse的錯誤,由於它在Promise中。咱們須要使用.catch,這樣錯誤處理代碼很是冗餘。而且,在咱們的實際生產代碼會更加複雜。
const makeRequest = () => {
try {
getJSON()
.then(result => {
// JSON.parse可能會出錯
const data = JSON.parse(result)
console.log(data)
})
// 取消註釋,處理異步代碼的錯誤
// .catch((err) => {
// console.log(err)
// })
} catch (err) {
console.log(err)
}
}
複製代碼
使用aync/await的話,catch能處理JSON.parse錯誤
const makeRequest = async () => {
try {
// this parse may fail
const data = JSON.parse(await getJSON())
console.log(data)
} catch (err) {
console.log(err)
}
}複製代碼
下面示例中,須要獲取數據,而後根據返回數據決定是直接返回,仍是繼續獲取更多的數據。
const makeRequest = () => {
return getJSON()
.then(data => {
if (data.needsAnotherRequest) {
return makeAnotherRequest(data)
.then(moreData => {
console.log(moreData)
return moreData
})
} else {
console.log(data)
return data
}
})
}
複製代碼 |
這些代碼看着就頭痛。嵌套(6層),括號,return語句很容易讓人感到迷茫,而它們只是須要將最終結果傳遞到最外層的Promise。
上面的代碼使用async/await編寫能夠大大地提升可讀性:
const makeRequest = async () => {
const data = await getJSON()
if (data.needsAnotherRequest) {
const moreData = await makeAnotherRequest(data);
console.log(moreData)
return moreData
} else {
console.log(data)
return data
}
}
複製代碼 |
你極可能遇到過這樣的場景,調用promise1,使用promise1返回的結果去調用promise2,而後使用二者的結果去調用promise3。你的代碼極可能是這樣的:
const makeRequest = () => {
return promise1()
.then(value1 => {
return promise2(value1)
.then(value2 => {
return promise3(value1, value2)
})
})
}
複製代碼 |
若是promise3不須要value1,能夠很簡單地將promise嵌套鋪平。若是你忍受不了嵌套,你能夠將value 1 & 2 放進Promise.all來避免深層嵌套:
const makeRequest = () => {
return promise1()
.then(value1 => {
return Promise.all([value1, promise2(value1)])
})
.then(([value1, value2]) => {
return promise3(value1, value2)
})
}
複製代碼 |
這種方法爲了可讀性犧牲了語義。除了避免嵌套,並無其餘理由將value1和value2放在一個數組中。
使用async/await的話,代碼會變得異常簡單和直觀。
const makeRequest = async () => {
const value1 = await promise1()
const value2 = await promise2(value1)
return promise3(value1, value2)
}
複製代碼 |
下面示例中調用了多個Promise,假設Promise鏈中某個地方拋出了一個錯誤:
const makeRequest = () => {
return callAPromise()
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => {
throw new Error("oops");
})
}
makeRequest()
.catch(err => {
console.log(err);
// output
// Error: oops at callAPromise.then.then.then.then.then (index.js:8:13)
})
複製代碼 |
Promise鏈中返回的錯誤棧沒有給出錯誤發生位置的線索。更糟糕的是,它會誤導咱們;錯誤棧中惟一的函數名爲callAPromise,然而它和錯誤沒有關係。(文件名和行號仍是有用的)。
然而,async/await中的錯誤棧會指向錯誤所在的函數:
const makeRequest = async () => {
await callAPromise()
await callAPromise()
await callAPromise()
await callAPromise()
await callAPromise()
throw new Error("oops");
}
makeRequest()
.catch(err => {
console.log(err);
// output
// Error: oops at makeRequest (index.js:7:9)
})
複製代碼 |
在開發環境中,這一點優點並不大。可是,當你分析生產環境的錯誤日誌時,它將很是有用。這時,知道錯誤發生在makeRequest比知道錯誤發生在then鏈中要好。
最後一點,也是很是重要的一點在於,async/await可以使得代碼調試更簡單。2個理由使得調試Promise變得很是痛苦:
const markRequest = () => {
return callAPromise ()
.then (() => callAPromise())
.then (() => callAPromise())
.then (() => callAPromise())
.then (() => callAPromise())
}
複製代碼
const markRequest = async () => {
await callAPromise()
await callAPromise()
await callAPromise()
await callAPromise()
await callAPromise()
}複製代碼
對於經常使用的不一樣異步編程處理方案,我的觀點是針對不一樣的業務場景可根據狀況選擇合適高效的方案,各有優點劣勢,不必頂一個踩一個,雖然技術不斷髮展優化,但有些技術不至於淘汰如此之快,存在即合理。