什么是同步和異步?
你可能知道, JavaScript 語言 的執(zhí)行環(huán)境是“單線程”
所謂“單線程”, 就是指一次只能完成一件任務(wù), 如果有多個任務(wù), 就必須排隊, 前面一個任務(wù)完成, 再執(zhí)行后面一個任務(wù), 以此類推
例如現(xiàn)實(shí)生活中的排隊
這種模式的好處是實(shí)現(xiàn)起來比較簡單, 執(zhí)行環(huán)境相對單純, 壞處是只要有一個任務(wù)耗時很長, 后面的任務(wù)都必須排隊等著, 會拖延整個程序的執(zhí)行
常見的瀏覽器無響應(yīng)(假死), 往往就是因為某一段 JavaScript 代碼長時間運(yùn)行(比如死循環(huán)), 導(dǎo)致整個頁面卡在這個地方, 其他任務(wù)無法執(zhí)行
為了解決這個問題, JavaScript 語言將任務(wù)的執(zhí)行模式分成兩種
- 同步(Synchronous)
- 異步(Asynchronous)
這里的 “同步”和“異步” 與我們現(xiàn)實(shí)中的同步、異步恰恰相反
例如:
- 一邊吃飯一邊打電話, 我們認(rèn)為這是同時進(jìn)行(同步執(zhí)行)的, 但在計算機(jī)中, 這種行為叫做異步執(zhí)行
- 吃飯的同時, 必須吃完飯才能打電話, 我們認(rèn)為這是不能同時進(jìn)行(異步執(zhí)行)的, 但在計算機(jī)中, 這種行為我們叫做同步執(zhí)行
至于為什么, 那你要問英文單詞了, 例如 異步(Asynchronous) 翻譯成中文是異步的, 但在計算機(jī)中, 表示的是我們認(rèn)知的同時執(zhí)行的
什么時候我們需要異步處理事件?
- 一種很常見的場景自然就是網(wǎng)絡(luò)請求了
- 我們封裝一個網(wǎng)絡(luò)請求的函數(shù), 因為不能立即拿到結(jié)果, 所以不能像簡單的 3 + 4 = 7 一樣立刻獲得結(jié)果
- 所以我們往往會傳入另一個函數(shù) (回調(diào)函數(shù) callback), 在數(shù)據(jù)請求成功之后, 再將得到的數(shù)據(jù)以參數(shù)的形式傳遞給回調(diào)函數(shù)
JavaScript 和 Node.js 中的異步操作都會在最后執(zhí)行, 例如 ajax、readFile、writeFile、setTimeout 等
獲取異步操作的值只能使用回調(diào)函數(shù)的方式, 異步操作都是最后執(zhí)行
回調(diào)函數(shù)
回調(diào)函數(shù)的方式獲取異步操作內(nèi)的數(shù)據(jù)
function sum(a, b, callback) {
console.log(1)
setTimeout(function () {
callback(a + b)
}, 1000)
console.log(2)
}
sum(10, 20, function (res) {
console.log(res)
})
// log: 1 2 30
這種方式雖然看似沒什么問題, 但是, 當(dāng)網(wǎng)絡(luò)請求非常復(fù)雜時, 就會出現(xiàn)回調(diào)地獄
ok, 我們用一個非??鋸埖陌咐齺碚f明
$.ajax('url1', function (data1) {
$.ajax(data1['url2'], function (data2) {
$.ajax(data2['url3'], function (data3) {
$.ajax(data3['url4'], function (data4) {
console.log(data4)
})
})
})
})
- 我們需要通過一個 url1 向服務(wù)器請求一個數(shù)據(jù) data1, data1 中又包含了下一個請求的 url2
- 我們需要通過一個 url2 向服務(wù)器請求一個數(shù)據(jù) data2, data2 中又包含了下一個請求的 url3
- 我們需要通過一個 url3 向服務(wù)器請求一個數(shù)據(jù) data3, data3 中又包含了下一個請求的 url4
- 發(fā)送網(wǎng)絡(luò)請求 url4, 獲取最終的數(shù)據(jù) data4
上面的代碼有什么問題?
- 正常情況下, 不會有什么問題, 可以正常運(yùn)行并且獲取我們想要的數(shù)據(jù)
- 但是, 這樣的代碼閱讀性非常差, 而且非常不利于維護(hù)
- 如果有多個異步同時執(zhí)行, 無法確認(rèn)他們的執(zhí)行順序, 所以通過嵌套的方式能保證代碼的執(zhí)行順序問題
- 我們更加期望的是一種更加優(yōu)雅的方式來進(jìn)行這種異步操作
Promise
什么是 Promise ?
ES6 中有一個非常重要和好用的特性就是 Promise
Promise 到底是做什么的?
- Promise 是異步編程的一種解決方案, 比傳統(tǒng)的解決方案回調(diào)函數(shù)和事件更合理和更強(qiáng)大
所謂 Promise, 簡單說就是一個容器, 里面保存著某個未來才會結(jié)束的事件(通常是一個異步操作)的結(jié)果
為了解決回調(diào)地獄所帶來的問題, ES6 里引進(jìn)了 Promise, 有了 Promise 對象, 就可以將異步操作以同步操作的流程表達(dá)出來, 避免了層層嵌套的回調(diào)函數(shù)
Promise 對象提供統(tǒng)一的接口, 使得控制異步操作更加容易
Promise 的特點(diǎn)
Promise 對象有以下兩個特點(diǎn)
- 對象的狀態(tài)不受外界影響, Promise 對象代表一個異步操作, 有三種狀態(tài): pending(進(jìn)行中)、fulfill(已成功) 和 rejected(已失敗), 只有異步操作的結(jié)果, 可以決定當(dāng)前是哪一種狀態(tài), 任何其他操作都無法改變這個狀態(tài), 這也是 Promise 這個名字的由來, 它的英語意思就是 “承諾”, 表示其他手段無法改變
- 一旦狀態(tài)改變, 就不會再變, 任何時候都可以得到這個結(jié)果, Promise 對象的狀態(tài)改變, 只有兩種可能: 從 pending 變?yōu)?fulfill 和 從 pending 變?yōu)?rejected, 只要這兩種情況發(fā)生, 狀態(tài)就凝固了, 不會再發(fā)生改變, 會一直保持這個結(jié)果, 這時就稱為 resolved(已定型), 如果改變已經(jīng)發(fā)生了, 你再對 Promise 對象添加回調(diào)函數(shù), 也會立即得到這個結(jié)果, 這與事件(Event)完全不同, 事件的特點(diǎn)是, 如果你錯過了它, 再去監(jiān)聽, 是得不到結(jié)果的
Promise 的缺點(diǎn)
- 首先, 無法取消 Promise, 一旦新建它就會立即執(zhí)行, 無法中途取消
- 其次, 如果不設(shè)置回調(diào)函數(shù), Promise 內(nèi)部拋出的錯誤, 不會反應(yīng)到外部
- 第三, 當(dāng)處于 pending 狀態(tài)時, 無法得知目前進(jìn)展到哪一個階段(剛剛開始還是即將完成)
Promise 的三種狀態(tài)
- pending : 等待(wait)狀態(tài), 比如正在進(jìn)行網(wǎng)絡(luò)請求, 或者定時器沒有到時間
- fulfilled : 滿足狀態(tài), 當(dāng)我們主動調(diào)用 resolve 時, 就處于該狀態(tài), 并且回調(diào) .then()
- rejected : 拒絕狀態(tài), 當(dāng)我們主動調(diào)用 reject 時, 就處于該狀態(tài), 并且回調(diào) .catch()
Promise 基本用法
ES6 規(guī)定, Promise 對象是一個構(gòu)造函數(shù), 用來生成 Promise 實(shí)例
new Promise((resolve, reject) => {
// ... 某些異步代碼
if (/* 異步操作成功 */){
resolve(data); // data 里是異步執(zhí)行后的返回值
} else {
reject(error); // error 里是異步執(zhí)行錯誤后的錯誤信息
}
}).then(data => {
// 這里對 data 就可以進(jìn)行數(shù)據(jù)拿取操作了
console.log('success')
}).catch(error => {
console.log('failure')
})
Promise 構(gòu)造函數(shù)接受一個函數(shù)作為參數(shù), 該函數(shù)的兩個參數(shù)分別是 resolve 和 reject
它們是兩個函數(shù), 由 JavaScript 引擎提供, 不需要自己部署
resolve
- resolve 函數(shù)的作用是將 Promise 對象的狀態(tài)從 “未完成”變?yōu)椤俺晒Α?即從 pending 變?yōu)?fulfilled), 在異步操作成功時調(diào)用, 并將異步操作的結(jié)果, 作為參數(shù)傳遞出去
reject
- reject 函數(shù)的作用是將 Promise 對象的狀態(tài)從 “未完成”變?yōu)椤笆 ?即從 pending 變?yōu)?rejected), 在異步操作失敗時調(diào)用, 并將異步操作報出的錯誤, 作為參數(shù)傳遞出去
then 方法還可以接受兩個回調(diào)函數(shù)作為參數(shù), 合并 .catch()
promise.then(data => {
// 這里對 data 就可以進(jìn)行數(shù)據(jù)拿取操作了
console.log('success')
}, error => {
console.log('failure')
})
- 第一個回調(diào)函數(shù)是 Promise 對象的狀態(tài)變?yōu)?fulfilled 時調(diào)用
- 第二個回調(diào)函數(shù)是 Promise 對象的狀態(tài)變?yōu)?rejected 時調(diào)用
- 其中, 第二個回調(diào)函數(shù)是可選的, 不一定要提供, 這兩個函數(shù)都接受Promise 對象傳出的值作為參數(shù)
一般來說, 調(diào)用 resolve 或 reject 以后, Promise 的使命就完成了, 后繼操作應(yīng)該放到 then 方法里面, 而不應(yīng)該直接寫在 resolve 或 reject 的后面
所以, 最好在將它們加上 return 語句, 這樣就不會有意外
new Promise((resolve, reject) => {
return resolve(1);
// 后面的語句不會執(zhí)行
console.log(2);
})
Promise 鏈?zhǔn)秸{(diào)用
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success1')
}, 1000)
}).then(res => {
console.log(res) // success1
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success2')
}, 1000)
})
}).then(res => {
console.log(res) // success2
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success3')
}, 1000)
})
}).then(res => {
console.log(res) // success3
})
Promise 鏈?zhǔn)秸{(diào)用簡寫
如果我們希望數(shù)據(jù)直接包裝成 Promise.resolve, 那么在 then 中可以直接返回數(shù)據(jù)
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success1')
}, 1000)
}).then(res => {
console.log(res) // success1
return 'success2'
}).then(res => {
console.log(res) // success2
return 'success3'
}).then(res => {
console.log(res) // success3
})
Promise.prototype.finally()
finally()方法用于指定不管 Promise 對象最后狀態(tài)如何, 都會執(zhí)行的操作
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
上面代碼中, 不管promise最后的狀態(tài), 在執(zhí)行完then或catch指定的回調(diào)函數(shù)以后, 都會執(zhí)行finally方法指定的回調(diào)函數(shù)
finally方法的回調(diào)函數(shù)不接受任何參數(shù), 這意味著沒有辦法知道前面的 Promise 狀態(tài)到底是fulfilled還是rejected, 這表明, finally方法里面的操作, 應(yīng)該是與狀態(tài)無關(guān)的, 不依賴于 Promise 的執(zhí)行結(jié)果
Promise.all()
Promise.all()方法用于將多個 Promise 實(shí)例, 包裝成一個新的 Promise 實(shí)例
const p = Promise.all([p1, p2])
上面代碼中, Promise.all()方法接受一個數(shù)組作為參數(shù), p1、p2都是 Promise 實(shí)例, Promise.all()方法的參數(shù)可以不是數(shù)組, 但必須具有 Iterator 接口, 且返回的每個成員都是 Promise 實(shí)例
p的狀態(tài)由p1、p2決定, 分成兩種情況
- 只有
p1、p2的狀態(tài)都變成fulfilled,p的狀態(tài)才會變成fulfilled, 此時p1、p2的返回值組成一個數(shù)組, 傳遞給p的回調(diào)函數(shù) - 只要
p1、p2之中有一個被rejected,p的狀態(tài)就變成rejected, 此時第一個被reject的實(shí)例的返回值, 會傳遞給p的回調(diào)函數(shù)
/* 兩個異步操作狀態(tài)都為 fulfilled */
var p1 = new Promise((resolve, reject) => {
resolve('request1')
})
var p2 = new Promise((resolve, reject) => {
resolve('request2')
})
Promise.all([p1, p2])
.then(res => console.log(res)) // ['request1', 'request2']
.catch(e => console.log(e))
/* 其中有一個異步操作狀態(tài)為 rejected */
var p1 = new Promise((resolve, reject) => {
resolve('request1')
})
var p2 = new Promise((resolve, reject) => {
reject('request2 error')
})
Promise.all([p1, p2])
.then(res => console.log(res))
.catch(e => console.log(e)) // 'request2 error'
注意, 如果作為參數(shù)的 Promise 實(shí)例, 自己定義了catch方法, 那么它一旦被rejected, 并不會觸發(fā)Promise.all()的catch方法
const p1 = new Promise((resolve, reject) => {
resolve('request1')
})
const p2 = new Promise((resolve, reject) => {
throw new Error('報錯了')
}).catch(e => e)
Promise.all([p1, p2])
.then(res => console.log(res)) // ['request1', Error: 報錯了]
.catch(e => console.log(e))
上面代碼中, p1 會 resolved, p2 首先會 rejected, 但是 p2 有自己的catch方法, 該方法返回的是一個新的 Promise 實(shí)例, p2 指向的實(shí)際上是這個實(shí)例
該實(shí)例執(zhí)行完catch方法后, 也會變成 resolved, 導(dǎo)致Promise.all()方法參數(shù)里面的兩個實(shí)例都會resolved, 因此會調(diào)用then方法指定的回調(diào)函數(shù), 而不會調(diào)用catch方法指定的回調(diào)函數(shù)
如果 p2 沒有自己的catch方法, 就會調(diào)用Promise.all()的catch方法
Promise.race()
Promise.race()方法同樣是將多個 Promise 實(shí)例, 包裝成一個新的 Promise 實(shí)例
const p = Promise.race([p1, p2])
只要
p1、p2之中有一個實(shí)例率先改變狀態(tài),p的狀態(tài)就跟著改變那個率先改變的 Promise 實(shí)例的返回值, 就傳遞給
p的回調(diào)函數(shù)Promise.race()方法的參數(shù)與Promise.all()方法一樣
下面是一個例子
/* 第一個異步操作率先完成, 并且狀態(tài)為 fulfilled */
Promise.race([
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('request success')
}, 1000)
}),
new Promise((resolve, reject) => {
setTimeout(() => {
reject('request timeout')
}, 2000)
})
])
.then(res => console.log(res)) // request success
.catch(e => console.log(e))
/* 第二個異步操作先完成, 并且狀態(tài)為 rejected */
Promise.race([
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('request success')
}, 1000)
}),
new Promise((resolve, reject) => {
setTimeout(() => {
reject('request timeout')
}, 500)
})
])
.then(res => console.log(res))
.catch(e => console.log(e)) // request timeout