1.1 什么叫異步
異步(async)
是相對於同步(sync)
而言的,很好理解。
同步
就是一件事一件事的執行。只有前一個任務執行完畢,才能執行后一個任務。而異步
比如:
setTimeout(function cbFn(){ console.log('learnInPro'); }, 1000); console.log('sync things');
setTimeout就是一個異步任務
,當JS引擎順序執行到setTimeout的時候發現他是個異步任務,則會把這個任務掛起,繼續執行后面的代碼。直到1000ms后,回調函數cbFn才會執行,這就是異步,在執行到setTimeout的時候,JS並不會傻呵呵的等着1000ms執行cbFn回調函數,而是繼續執行了后面的代碼。
1.2 為啥要在JS中使用異步
由於javascript是單線程
的,只能在JS引擎的主線程上運行的,所以js代碼只能一行一行的執行,不能在同一時間執行多個js代碼任務,這就導致如果有一段耗時較長的計算,或者是一個ajax請求等IO操作,如果沒有異步的存在,就會出現用戶長時間等待,並且由於當前任務還未完成,所以這時候所有的其他操作都會無響應。
1.3 那為啥JS不設計成多線程的
這主要跟javascript的歷史有關,js最開始只是為了處理一些表單驗證和DOM操作而被創造出來的,所以主要為了語言的輕量和簡單采用了單線程
的模式。多線程模型
相比單線程
要復雜很多,比如多線程需要處理線程間資源的共享問題,還要解決狀態同步等問題。
如果JS是多線程的話,當你要執行往div中插入一個DOM的操作的同時,另一個線程執行了刪除這個div的操作,這個時候就會出現很多問題,我們還需要為此增加鎖機制等。
好,那么現在我們知道了單線程的JS為了不出現長時間等待的狀況,會使用異步來處理。比如當執行一個ajax操作的時候,當js發出請求后,不會傻了吧唧的在那里等着服務器數據返回,而是去繼續執行后面的任務,等到服務器數據返回以后再通知js引擎去處理。
那么常見的異步模式有哪些呢?
- 回調函數
- 事件監聽
- 發布/訂閱模式(又稱觀察者模式)
- promise
后來ES6中,引入了
Generator
函數;ES7中,async/await
更是將異步編程帶入了一個全新的階段。
這些異步模式我們會在后面詳細來說,這里我們有個概念就好。
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中異步的運行機制,我們翻回頭來詳細的了解一下常見的各種異步的編程模式。
- 回調函數
- 事件監聽
- 發布/訂閱模式
- Promise
- Generator
- async/await
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講完。
轉: https://www.cnblogs.com/learninpro/p/9166356.html