你可能知道,Javascript語言的執行環境是"單執行緒"(single thread)。
所謂"單執行緒",就是指一次只能完成一件任務。如果有多個任務,就必須排隊,前面一個任務完成,再執行後面一個任務,以此類推。
這種模式的好處是實現起來比較簡單,執行環境相對單純;壞處是隻要有一個任務耗時很長,後面的任務都必須排隊等著,會拖延整個程式的執行。常見的瀏覽器無響應(假死),往往就是因為某一段Javascript程式碼長時間執行(比如死迴圈),導致整個頁面卡在這個地方,其他任務無法執行。
為了解決這個問題,Javascript語言將任務的執行模式分成兩種:同步(Synchronous)和非同步(Asynchronous)。
1.回撥
回撥是非同步程式設計最基本的方法。
假定有兩個函式f1和f2,後者等待前者的執行結果。
f1();
f2();
複製程式碼
如果f1是一個很耗時的任務,可以考慮改寫f1,把f2寫成f1的回撥函式。
function f1(callback){
setTimeout(function () {
// f1的任務程式碼
callback();
}, 1000);
}
複製程式碼
執行程式碼就變成下面這樣
f1(f2);
複製程式碼
採用這種方式,我們把同步操作變成了非同步操作,f1不會堵塞程式執行,相當於先執行程式的主要邏輯,將耗時的操作推遲執行。 回撥函式的優點是簡單、容易理解和部署,缺點是不利於程式碼的閱讀和維護,各個部分之間高度耦合,流程會很混亂,而且每個任務只能指定一個回撥函式。
2.Promise
Promises
物件是CommonJS
工作組提出的一種規範,目的是為非同步程式設計提供統一介面。
簡單說,它的思想是, 每一個非同步任務返回一個Promise物件,該物件有一個then方法,允許指定回撥函式。 Promises的出現大大改善了非同步變成的困境,避免出現回撥地獄,巢狀層級得到改善。
基本Api
- Promise.resolve()
- Promise.reject()
- Promise.prototype.then()
- Promise.prototype.catch()
- Promise.all() // 所有的完成
- Promise.race() // 競速,完成一個即可
具體api的介紹請看 阮一峰 大神的 ECMAScript 6 入門 在這我舉幾個簡單的場景的實現
模擬兩個非同步請求
為了使程式碼簡介,promise的rejected
狀態的相關reject()和catch()方法省略
// 1請求
function getData1 () {
return new Promise(function (resolve, reject) {
setTimeout(() => {
console.log('1執行了')
resolve('請求到模擬資料1111拉')
}, 2000)
})
}
// 2請求
function getData2 (params) {
return new Promise(function (resolve, reject) {
setTimeout(() => {
console.log('2執行了')
resolve('請求到模擬資料22222拉!params:' + params)
}, 1500)
})
}
複製程式碼
promise 實現非同步回撥 非同步列隊
1請求完成後,把1的響應引數傳入2,在發2請求
function promiseDemo () {
getData1()
.then(res => {
return getData2(res)
})
.then(res => {
console.log(res)
})
}
promiseDemo()
// 1執行了
// 2執行了
// 請求到模擬資料22222拉!params:請求到模擬資料1111拉 用時 3500 ms
複製程式碼
promise.all() 實現非同步回撥 併發 所有的完成
1請求、2請求同時發,兩條響應都收到後在執行
function promiseDemo () {
Promise.all([getData1(), getData2()]).then(function (res) {
console.log(res)
})
}
// 2執行了
// 1執行了
// ["請求到模擬資料1111拉", "請求到模擬資料22222拉!params:undefined"] 用時 2000 ms
複製程式碼
promise.race() 實現非同步回撥 併發 競速
1請求、2請求同時發,其中一條收到請求就執行
function promiseDemo () {
Promise.race([getData1(), getData2()]).then(function (res) {
console.log(res)
})
}
// 2執行了
// 請求到模擬資料22222拉!params:undefined 用時 1500 ms
// 1執行了
複製程式碼
由此Promise物件還是很好用的,對於非同步的流程的控制得到了大大改善,通過
.then()
的方法可進行鏈式呼叫。 可是.then() .catch()
的使用也導致程式碼非常難看,巢狀也很深,所以async/await
就出來了
Async/await
Async/await 是Javascript編寫非同步程式的新方法。以往的非同步方法無外乎回撥函式和Promise。但是Async/await建立於Promise之上。
如何使用 Async 函式
我們還是來看一 看阮一峰 大神的 ECMAScript 6 入門 的例子
async function timeout(ms) {
await new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
}
asyncPrint('hello world', 50);
複製程式碼
上面程式碼指定50毫秒以後,輸出hello world。 進一步說,async函式完全可以看作多個非同步操作,包裝成的一個 Promise 物件,而await命令就是內部then命令的語法糖
我們看具體的示例
async 實現非同步回撥 非同步列隊
1請求完成後,把1的響應引數傳入2,在發2請求
上文中的promise 實現方法是通過then的鏈式呼叫,但是採用async會更加簡潔明瞭
async function asyncDemo () {
const r1 = await getData1()
const r2 = await getData2(r1)
console.log(r2)
}
// 1執行了
// 2執行了
// 請求到模擬資料22222拉!params:請求到模擬資料1111拉 用時 3500 ms
複製程式碼
用同步的書寫方式實現了非同步的程式碼。等待getData1的非同步函式執行完了後發返回值賦值給r1,傳入r2,在執行r2
async 非同步回撥 併發
1請求、2請求同時發,規定請求到達的順序
假如我們有一種這樣的業務需求,併發兩個請求,但是要規定收到請求的順序應該怎麼做的?這裡還是借鑑阮一峰大神的程式碼
async function asyncDemo2 () {
const arr = [getData1, getData2]
const textPromises = arr.map(async function (doc) {
const response = await doc()
return response
})
// 按次序輸出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
// 2執行了 (因為2是 1500ms後執行) 所以2先執行
// 1執行了
// 請求到模擬資料1拉 (for .. of )規定了輸出的順序
// 請求到模擬資料22222拉!params:undefined
複製程式碼
面程式碼中,雖然map方法的引數是async函式,但它是併發執行的,因為只有async函式內部是繼發執行,外部不受影響。後面的for..of迴圈內部使用了await,因此實現了按順序輸出
怎麼樣這麼BT的需求都能實現把。
async 總結
他使得非同步程式碼變的不再明顯也是一點弊端咯,不過根據實際情況選擇最合適的非同步程式設計才是最好的選擇。async 是 Generator 函式的語法糖。所以想更深入的理解其中內部原理的趕緊去看看 Generator
函式把