1.1 什麼叫異步
異步(async)
是相對於同步(sync)
而言的,很好理解。javascript
同步
就是一件事一件事的執行。只有前一個任務執行完畢,才能執行後一個任務。而異步
好比:前端
setTimeout(function cbFn(){
console.log('learnInPro');
}, 1000);
console.log('sync things');
setTimeout就是一個異步任務
,當JS引擎順序執行到setTimeout的時候發現他是個異步任務,則會把這個任務掛起,繼續執行後面的代碼。直到1000ms後,回調函數cbFn纔會執行,這就是異步,在執行到setTimeout的時候,JS並不會傻呵呵的等着1000ms執行cbFn回調函數,而是繼續執行了後面的代碼。vue
1.2 爲啥要在JS中使用異步
因爲javascript是單線程
的,只能在JS引擎的主線程上運行的,因此js代碼只能一行一行的執行,不能在同一時間執行多個js代碼任務,這就致使若是有一段耗時較長的計算,或者是一個ajax請求等IO操做,若是沒有異步的存在,就會出現用戶長時間等待,而且因爲當前任務還未完成,因此這時候全部的其餘操做都會無響應。java
1.3 那爲啥JS不設計成多線程的
這主要跟javascript的歷史有關,js最開始只是爲了處理一些表單驗證和DOM操做而被創造出來的,因此主要爲了語言的輕量和簡單採用了單線程
的模式。多線程模型
相比單線程
要複雜不少,好比多線程須要處理線程間資源的共享問題,還要解決狀態同步等問題。c++
若是JS是多線程的話,當你要執行往div中插入一個DOM的操做的同時,另外一個線程執行了刪除這個div的操做,這個時候就會出現不少問題,咱們還須要爲此增長鎖機制等。ajax
好,那麼如今咱們知道了單線程的JS爲了避免出現長時間等待的情況,會使用異步來處理。好比當執行一個ajax操做的時候,當js發出請求後,不會傻了吧唧的在那裏等着服務器數據返回,而是去繼續執行後面的任務,等到服務器數據返回之後再通知js引擎去處理。npm
那麼常見的異步模式有哪些呢?編程
- 回調函數
- 事件監聽
- 發佈/訂閱模式(又稱觀察者模式)
- promise
後來ES6中,引入了
Generator
函數;ES7中,async/await
更是將異步編程帶入了一個全新的階段。數組
這些異步模式咱們會在後面詳細來講,這裏咱們有個概念就好。promise
1.4 JS如何實現異步
具體JS是如何實現異步操做的呢?
答案就是JS的事件循環機制(Event Loop)
。
具體來講:
當JS解析執行時,會被引擎分爲兩類任務,同步任務(synchronous)
和 異步任務(asynchronous)
。
對於同步任務來講,會被推到執行棧按順序去執行這些任務。
對於異步任務來講,當其能夠被執行時,會被放到一個 任務隊列(task queue)
裏等待JS引擎去執行。
當執行棧中的全部同步任務完成後,JS引擎纔會去任務隊列裏查看是否有任務存在,並將任務放到執行棧中去執行,執行完了又會去任務隊列裏查看是否有已經能夠執行的任務。這種循環檢查的機制,就叫作事件循環(Event Loop)
。
對於任務隊列
,實際上是有更細的分類。其被分爲 微任務(microtask)隊列
& 宏任務(macrotask)隊列
宏任務: setTimeout、setInterval等,會被放在宏任務(macrotask)隊列。
微任務: Promise的then、Mutation Observer等,會被放在微任務(microtask)隊列。
Event Loop的執行順序是:
- 首先執行執行棧裏的任務。
- 執行棧清空後,檢查微任務(microtask)隊列,將可執行的微任務所有執行。
- 取宏任務(macrotask)隊列中的第一項執行。
- 回到第二步。
注意: 微任務隊列每次全執行,宏任務隊列每次只取一項執行。
咱們舉個例子:
setTimeout(() => {
console.log('我是第一個宏任務');
Promise.resolve().then(() => {
console.log('我是第一個宏任務裏的第一個微任務');
});
Promise.resolve().then(() => {
console.log('我是第一個宏任務裏的第二個微任務');
});
}, 0);
setTimeout(() => {
console.log('我是第二個宏任務');
}, 0);
Promise.resolve().then(() => {
console.log('我是第一個微任務');
});
console.log('執行同步任務');
最後的執行結果是:
- // 執行同步任務
- // 我是第一個微任務
- // 我是第一個宏任務
- // 我是第一個宏任務裏的第一個微任務
- // 我是第一個宏任務裏的第二個微任務
- // 我是第二個宏任務
1.5 JS異步編程模式
這裏咱們已經知道了JS中異步的運行機制,咱們翻回頭來詳細的瞭解一下常見的各類異步的編程模式。
1.5.1 回調函數
回調函數是異步操做最基本的方法。
好比:我有一個異步操做(asyncFn),和一個同步操做(normalFn)。
function asyncFn() {
setTimeout(() => {
console.log('asyncFn');
}, 0)
}
function normalFn() {
console.log('normalFn');
}
asyncFn();
normalFn();
// normalFn
// asyncFn
若是按照正常的JS處理機制來講,同步操做必定發生在異步以前。若是我想要將順序改變,最簡單的方式就是使用回調的方式處理。
function asyncFn(callback) {
setTimeout(() => {
console.log('asyncFn');
callback();
}, 0)
}
function normalFn() {
console.log('normalFn');
}
asyncFn(normalFn);
// asyncFn
// normalFn
1.5.2 事件監聽
另外一種思路是採用事件驅動模式。這種思路是說異步任務的執行不取決於代碼的順序,而取決於某個事件是否發生。
好比一個咱們註冊一個按鈕的點擊事件或者註冊一個自定義事件,而後經過點擊或者trigger的方式觸發這個事件。
1.5.3 發佈/訂閱模式(又稱觀察者模式)
這個重點講下,發佈/訂閱模式像是事件監聽模式的升級版。
在發佈/訂閱模式中,你能夠想象存在一個消息中心的地方,你能夠在那裏「註冊一條消息」,那麼被註冊的這條消息能夠被感興趣的若干人「訂閱」,一旦將來這條「消息被髮布」,則全部訂閱了這條消息的人都會獲得提醒。
這個就是發佈/訂閱模式的設計思路。接下來咱們一點一點實現一個簡單的發佈/訂閱模式。
首先咱們先實現一個消息中心。
// 先實現一個消息中心的構造函數,用來建立一個消息中心
function MessageCenter(){
var _messages = {}; // 全部註冊的消息都存在這裏
this.regist = function(){}; // 用來註冊消息的方法
this.subscribe = function(){}; // 用來訂閱消息的方法
this.fire = function(){}; // 用來發布消息的方法
}
這裏一個消息中心的雛形就建立好了,接下來咱們只要完善下regist,subscribe和fire這三個方法就行了。
function MessageCenter(){
var _messages = {};
// 對於regist方法,它只負責註冊消息,就只接收一個註冊消息的類型(標識)參數就行了。
this.regist = function(msgType){
// 判斷是否重複註冊
if(typeof _messages[msgType] === 'undefined'){
_messages[msgType] = []; // 數組中會存放訂閱者
}else{
console.log('這個消息已經註冊過了');
}
}
// 對於subscribe方法,須要訂閱者和已經註冊了的消息進行綁定
// 因爲訂閱者獲得消息後須要處理消息,因此他是一個個的函數
this.subscribe = function(msgType, subFn){
// 判斷是否有這個消息
if(typeof _messages[msgType] !== 'undefined'){
_messages[msgType].push(subFn);
}else{
console.log('這個消息還沒註冊過,沒法訂閱')
}
}
// 最後咱們實現下fire這個方法,就是去發佈某條消息,並通知訂閱這條消息的全部訂閱者函數
this.fire = function(msgType, args){
// msgType是消息類型或者說是消息標識,而args能夠設置這條消息的附加信息
// 仍是發佈消息時,判斷下有沒有這條消息
if(typeof _messages[msgType] === 'undefined') {
console.log('沒有這條消息,沒法發佈');
return false;
}
var events = {
type: msgType,
args: args || {}
};
_messages[msgType].forEach(function(sub){
sub(events);
})
}
}
這樣,一個簡單的發佈/訂閱模式就完成了,固然這只是這種模式的其中一種簡單實現,還有不少其餘的實現方式。
就此咱們就能夠用他來處理一些異步操做了。
var msgCenter = new MessageCenter();
msgCenter.regist('A');
msgCenter.subscribe('A', subscribeFn);
function subscribeFn(events) {
console.log(events.type, events.args);
}
// -----
setTimeout(function(){
msgCenter.fire('A', 'fire msg');
}, 1000);
// A, fire msg
咱們在這篇文章裏深刻講解了什麼是異步,爲何要有異步以及在JS中引擎是如何處理異步的,後面咱們講解了幾種異步編程模式並重點講了下發布/訂閱模式,
在下一章裏面咱們重點把另外幾種異步編程模式Promise,Generator,async/await講完。
額…另外就是若是你在學習前端的過程當中有任何問題想要諮詢,歡迎關注
LearnInPro的公衆號
,在上面隨時向我提問哦。👋