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