一提到非同步程式設計大家首先的想到的一定是回撥函式,這也是最常用的非同步程式設計的形式,但其實常用的還有Promise和Async函式,接下來就讓我們一起學習這幾種常用的非同步程式設計方法。
回撥函式
回撥函式就是把任務的第二段單獨寫在一個函式裡面,等到重新執行這個任務的時候,就直接呼叫這個函式,來看一個簡單的例子:
function print(name, callback) {
setTimeout(() => {
console.log(name)
if (callback) {
callback()
}
}, 1000)
}
print('a', function () {
print('b')
})
複製程式碼
上面這個例子中將print('b')放在print('a')的回撥函式中,這樣就能按順序依次列印a、b,但是回撥函式有一個很明顯的問題,就是當回撥函式巢狀過深時,會導致程式碼混亂,不夠清晰,這就是人們常說的對調地獄,來看下面這個例子:
function print(name, callback) {
setTimeout(() => {
console.log(name)
if (callback) {
callback()
}
}, 1000)
}
print('a', function () {
print('b', function () {
print('c', function () {
print('d')
})
})
})
複製程式碼
當我們想按順序依次列印a、b、c、d時,程式碼就變成上面的樣子,可以看到,我們的程式碼形成四層巢狀,如果還要加回撥函式就要繼續巢狀,這樣巢狀會越寫越深,越來越難以維護,此時我們就必須考慮用新的技術去改進,es6的Promise函式應運而生,接下來讓我們看Promise函式是如何改進這個問題的。
Promise
function print(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(name)
resolve()
}, 1000)
})
}
print('a').then(() => {
return print('b')
})
.then(() => {
return print('c')
})
.then(() => {
return print('d')
})
複製程式碼
和之前用回撥函式的形式相比,Promise函式寫法更加清晰,由回撥函式的巢狀呼叫變成了鏈式呼叫,但是Promise也有一個很嚴重的問題就是程式碼冗餘,原來的任務被Promise包裝了一下,不管什麼操作都是放在then函式裡面,導致程式碼的語以變差,有什麼更好的解決辦法呢?如果您對Promise函式還想有更深入的瞭解,可以去看阮一峰老師es6入門
Async
在正式使用非同步函式之前,先簡單的介紹一下它的用法,async通常與await一起使用,async函式返回一個Promise物件,可以使用then方法新增回撥函式。當函式執行的時候,一旦遇到await就會先返回,等到觸發的非同步操作完成,再接著執行函式體後面的語句。做了簡單的介紹後,接下來,我們來async函式是怎麼對Promise呼叫優化的。看下面的例子:
function print(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(name)
resolve()
}, 1000)
})
}
async function test () {
await print('a')
await print('b')
await print('c')
await print('d')
}
test()
複製程式碼
async函式來處理之前的問題,程式碼就是上面的這個例子中所展示的樣子,是不是感覺程式碼瞬間清晰了,而且程式碼更加好理解了,再仔細思考一下使用async非同步函式就很完美了嗎?其實async非同步函式也有其固有的問題,接下來我們就看看async非同步函式還有什麼問題需要解決。
錯誤捕獲
非同步函式第一個需要解決的問題就是錯誤捕獲的問題,讓我們看看一般情況下async非同步函式是怎麼做錯誤捕獲的,來看一個例子:
function print(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(name)
resolve()
}, 1000)
})
}
async function test () {
try {
await print('a')
} catch (err) {
console.log(err)
}
}
test()
複製程式碼
當使用上述形式的try,catch進行錯誤捕獲的時候,是不是覺得程式碼和使用Promise函式時一樣囉嗦,那有沒有好的解決辦法呢?讓我們來看另外一個例子:
function print(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(name)
resolve('a')
}, 1000)
})
}
async function test () {
let [ err, result ] = await to(print('a'))
if (err) throw err
return result
}
test()
複製程式碼
to.js:
function to(promise, errorExt) {
return promise
.then(function (data) { return [null, data]; })
.catch(function (err) {
if (errorExt) {
Object.assign(err, errorExt);
}
return [err, undefined];
});
}
export { to };
export default to; //歡迎加入全棧開發交流群一起學習交流:864305860
複製程式碼
上述例子中,將async非同步函式的錯誤處理封裝到了一個to.js中,這裡面其實只有一個簡單方法,傳入一個Promise物件,對Promise物件進行錯誤捕獲返回值,用解構的形式獲取返回值和錯誤,這樣就不需要反覆寫try catche做錯誤捕獲了。to.js是一個開源庫
非同步陷阱
什麼是非同步陷阱呢?在使用async非同步函式的時候,多個非同步操作是可以同時執行,但是有await命令變成了繼發的形式了,即必須等待前一個執行完了後一個才能執行,還是之前的例子:
function print(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(name)
resolve()
}, 1000)
})//歡迎加入全棧開發交流群一起學習交流:864305860
}
async function test () {
await print('a')
await print('b')
await print('c')
await print('d')
}
test()
複製程式碼
假設await print('a')、await print('b')、await print('c')、await print('d')這四個操作並沒有先後的邏輯關係,可以同時執行,那麼按照上面的寫法就會導致前一個執行完再執行下一個,整個執行過程中的等待時間會有4s,但是同時執行的等待時間就只有1s,這是在使用async非同步函式會經常忽略的一個問題,那麼怎麼解決呢?介紹一個我經常使用的辦法,看例子:
function print(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(name)
resolve('a')
}, 1000)
})
}
async function test () {
Promise.all([print('a'), print('b'), print('c'), print('d')])
}
test()
//歡迎加入全棧開發交流群一起學習交流:864305860
複製程式碼
其實解決辦法很簡單就是通過Promise.all()方法,將所有非同步操作作為引數陣列傳入,這樣print('a')、print('b')、print('c')、print('d')這四個非同步操作就可以併發執行了。
總結
這篇文章簡單的介紹了一些常用的非同步程式設計的方法,如果有錯誤或不嚴謹的地方,歡迎批評指正,如果喜歡,歡迎點贊收藏。
本次給大家推薦一個免費的學習群,裡面概括移動應用網站開發,css,html,webpack,vue node angular以及面試資源等。 對web開發技術感興趣的同學,歡迎加入Q群:864305860,不管你是小白還是大牛我都歡迎,還有大牛整理的一套高效率學習路線和教程與您免費分享,同時每天更新視訊資料。 最後,祝大家早日學有所成,拿到滿意offer,快速升職加薪,走上人生巔峰。