事件循環機制EventLoop
Event Loop即事件循環,是解決javaScript單線程運行阻塞的一種機制。
一、EventLoop的相關概念
1、堆(Heap)
堆表示一大塊非結構化的內存區域,對象,數據被存放在堆中
2、棧(Stack)
棧在javascript中又稱執行棧,調用棧,是一種後進先出的數組結構,
Javascript
有一個 主線程(main thread
)和 調用棧(或執行棧call-stack
),主線各所有的任務都會被放到調用棧等待主線程執行。
JS調用棧採用的是後進先出的規則,當函數執行的時候,會被添加到棧的頂部,當執行棧執行完成後,就會從棧頂移出,直到棧內被清空。
舉個例子:
function foo(b) {
var a = 10;
return a + b + 11;
}
function bar(x) {
var y = 3;
return foo(x * y);
}
console.log(bar(7)); // 返回 42
當調用 bar 時,創建了第一個幀 ,幀中包含了 bar 的參數和局部變量。當 bar 調用 foo 時,第二個幀就被創建,並被壓到第一個幀之上,幀中包含了 foo 的參數和局部變量。當 foo 返回時,最上層的幀就被彈出棧(剩下 bar 函數的調用幀 )。當 bar 返回的時候,棧就空了。
這裏的堆棧,是數據結構的堆棧,不是內存中的堆棧(內存中的堆棧,堆存放引用類型的數據,棧存放基本類型的數據)
3、隊列(Queue)
隊列即任務隊列Task Queue
,是一種先進先出的一種數據結構。在隊尾添加新元素,從隊頭移除元素。
二、同步任務和異步任務
javascript是單線程。單線程就意味着,所有任務需要排隊,前一個任務結束,纔會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等着。
於是js所有任務分爲兩種:同步任務,異步任務
同步任務是調用立即得到結果的任務,同步任務在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;
異步任務是調用無法立即得到結果,需要額外的操作才能預期結果的任務,異步任務不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務可以執行了,該任務纔會進入主線程執行。
JS引擎遇到異步任務(DOM事件監聽、網絡請求、setTimeout計時器等),會交給相應的線程單獨去維護異步任務,等待某個時機(計時器結束、網絡請求成功、用戶點擊DOM),然後由 事件觸發線程 將異步對應的 回調函數 加入到消息隊列中,消息隊列中的回調函數等待被執行。
具體來說,異步運行機制如下:
- (1)所有同步任務都在主線程上執行,形成一個[執行棧]
- (2)主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
- (3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,於是結束等待狀態,進入執行棧,開始執行。
- (4)主線程不斷重複上面的第三步。
主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,所以整個的這種運行機制又稱爲Event Loop(事件循環)
舉個例子
console.log('script start')
setTimeout(() => {
console.log('timer 1 over')
}, 1000)
setTimeout(() => {
console.log('timer 2 over')
}, 0)
console.log('script end')
// script start
// script end
// timer 2 over
// timer 1 over
timer 2 over
0毫秒後添加到任務隊列隊尾,timer 1 over
1秒添加到任務隊列隊尾,等待主線程任務執行完,從隊頭依次執行任務隊列中的任務
三、宏任務和微任務
同步和異步的執行機制在ES5的情況下夠用了,但是ES6會有一些問題。
Promise同樣是用來處理異步的:
console.log('script start')
setTimeout(function() {
console.log('timer over')
}, 0)
Promise.resolve().then(function() {
console.log('promise1')
}).then(function() {
console.log('promise2')
})
console.log('script end')
// script start
// script end
// promise1
// promise2
// timer over
“promise 1” “promise 2” 在 “timer over” 之前打印了?
這裏有一個新概念:macrotask(宏任務) 和 microtask(微任務)。
所有任務分爲宏任務(macrotask )和微任務(microtask ) 兩種。
MacroTask(宏任務):* script
全部代碼、setTimeout
、setInterval
、setImmediate
(瀏覽器暫時不支持,只有IE10支持,具體可見MDN
)、I/O
、UI Rendering
。
MicroTask(微任務):* Process.nextTick(Node獨有)
、Promise
、Object.observe(廢棄)
、MutationObserver
(具體使用方式查看這裏)
在掛起任務時,JS 引擎會將所有任務按照類別分到這兩個隊列中,首先在 宏任務 的隊列中取出第一個任務,執行完畢後取出 微任務 隊列中的所有任務順序執行;之後再取 宏任務,周而復始,直至兩個隊列的任務都取完。
給個圖加深理解
Event Loop
四、異步編程的幾種方法
1、回調函數
這是異步編程最基本的方法。假定有兩個函數f1和f2,後者等待前者的執行結果。
如果f1是一個很耗時的任務,可以把f2寫成f1的回調函數。
function f1(callback){
setTimeout(function () {
// f1的任務代碼
callback();
}, 1000);
}
f1(f2);
採用這種方式,我們把同步操作變成了異步操作,f1不會堵塞程序運行,相當於先執行程序的主要邏輯,將耗時的操作推遲執行。
回調函數的優點是簡單、容易理解和部署,缺點是不利於代碼的閱讀和維護,各個部分之間高度耦合(Coupling),流程會很混亂,而回調函數有一個致命的弱點,就是容易寫出回調地獄
2、事件監聽
另一種思路是採用事件驅動模式。任務的執行不取決於代碼的順序,而取決於某個事件是否發生。
還是以f1和f2爲例。首先,爲f1綁定一個事件(這裏採用的jQuery的寫法)。
function f1(){
setTimeout(function () {
// f1的任務代碼
f1.trigger('done'); // 執行完成後,立即觸發done事件,從而開始執行f2
}, 1000);
}
f1.on('done', f2); // 當f1發生done事件,就執行f2
這種方法的優點是比較容易理解,可以綁定多個事件,每個事件可以指定多個回調函數,而且可以"去耦合"(Decoupling),有利於實現模塊化。缺點是整個程序都要變成事件驅動型,運行流程會變得很不清晰。
3、發佈/訂閱
假定,存在一個"信號中心",某個任務執行完成,就向信號中心"發佈"(publish)一個信號,其他任務可以向信號中心"訂閱"(subscribe)這個信號,從而知道什麼時候自己可以開始執行。這就叫做"發佈/訂閱模式"(publish-subscribe pattern),又稱"觀察者模式"(observer pattern)。
這個模式有多種實現,下面採用的是Ben Alman的Tiny Pub/Sub,這是jQuery的一個插件。
jQuery.subscribe("done", f2);
function f1(){
setTimeout(function () {
// f1的任務代碼
jQuery.publish("done") ; // f1執行完成後,向"信號中心"jQuery發佈"done"信號,從而引發f2的執行。
}, 1000);
}
jQuery.unsubscribe("done", f2); // f2完成執行後,也可以取消訂閱(unsubscribe)
這種方法的性質與"事件監聽"類似,但是明顯優於後者。因爲我們可以通過查看"消息中心",瞭解存在多少信號、每個信號有多少訂閱者,從而監控程序的運行
4、Promise對象
Promise
是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。它由社區最早提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise對象。
Promise對象有以下兩個特點。
(1)對象的狀態不受外界影響。
Promise
對象代表一個異步操作,有三種狀態:Pending
(進行中)、Resolved
(已完成,又稱Fulfilled)和Rejected
(已失敗)。
只有異步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態
(2)一旦狀態改變,就不會再變,任何時候都可以得到這個結果
Promise
對象的狀態改變,只有兩種可能:從Pending
變爲Resolved
和從Pending
變爲Rejected
。
只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果。就算改變已經發生了,你再對Promise
對象添加回調函數,也會立即得到這個結果
優點:將異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數。此外,Promise
對象提供統一的接口,使得控制異步操作更加容易。
缺點:首先,無法取消Promise
,一旦新建它就會立即執行,無法中途取消。其次,如果不設置回調函數,Promise
內部拋出的錯誤,不會反應到外部。第三,當處於Pending
狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。
Promise.prototype.then()
方法的作用是爲 Promise 實例添加狀態改變時的回調函數。第一個參數是Resolved
狀態的回調函數,第二個參數(可選)是Rejected
狀態的回調函數。
Promise.prototype.catch
方法是.then(null, rejection)
或.then(undefined, rejection)
的別名,用於指定發生錯誤時的回調函數
var promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 異步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
// Promise實例生成以後,可以用then方法分別指定Resolved狀態和Reject狀態的回調函數。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
5、Generator 函數
Generator
函數是ES6
提供的一種異步編程解決方案,語法行爲與傳統函數完全不同
Generator
函數有多種理解角度:
語法上,Generator
函數是一個狀態機,封裝了多個內部狀態。執行Generator
函數會返回一個遍歷器對象,可以依次遍歷Generator
函數內部的每一個狀態。
形式上,Generator
函數是一個普通函數,但是有兩個特徵。一是,function
關鍵字與函數名之間有一個星號;二是,函數體內部使用yield
表達式,定義不同的內部狀態。
yield語句
Generator
函數返回的遍歷器對象,yield
語句暫停,調用next方法恢復執行,如果沒遇到新的yeild
,一直運行到return
語句爲止,return
後面表達式的值作爲返回對象的value
值,如果沒有return
語句,一直運行到結束,返回對象的value
爲undefined
。
function* helloWorldGenerator() { // Generator 函數,該函數有三個狀態:hello,world 和 return 語句
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
//Generator 函數的調用,調用後並不執行,而是返回一個指向內容狀態的指針對象(即遍歷器對象Iterator Object)
// Generator 函數是分段執行的,yield表達式是暫停執行的標記,而next方法可以恢復執行,
hw.next()
// { value: 'hello', done: false } done爲false表示遍歷未結束
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true } done爲true表示遍歷結束
hw.next()
// { value: undefined, done: true } Generator 函數已經運行完畢,以後再調用next方法,返回的都是這個結果
6、async與await
ES2017提供了async
函數,使得異步操作變得更加方便。async
函數就是Generator
函數的語法糖。
async
函數就是將Generator
函數的星號(*
)替換成async
,將yield
替換成await
,僅此而已。
進一步說,async
函數完全可以看作多個異步操作,包裝成的一個Promise
對象,而await
命令就是內部then
命令的語法糖。
async
函數返回一個 Promise
對象,可以使用then
方法添加回調函數。當函數執行的時候,一旦遇到await
就會先返回,等到異步操作完成,再接着執行函數體內後面的語句。
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
。
參考鏈接:
https://www.jianshu.com/p/de7aba994523
https://blog.csdn.net/qq_39343308/article/details/86262045
https://juejin.im/post/5c3d8956e51d4511dc72c200
https://juejin.im/post/5c6515e0518825266c3ef852?utm_source=gold_browser_extension
lue);
}
asyncPrint(‘hello world’, 50);
上面代碼指定 50 毫秒以後,輸出`hello world`。
參考鏈接:
https://www.jianshu.com/p/de7aba994523
https://blog.csdn.net/qq_39343308/article/details/86262045
https://juejin.im/post/5c3d8956e51d4511dc72c200
https://juejin.im/post/5c6515e0518825266c3ef852?utm_source=gold_browser_extension
[https://github.com/ZavierTang/zavier-notes/blob/master/JavaScript/%E5%90%8C%E6%AD%A5%E4%B8%8E%E5%BC%82%E6%AD%A5%E3%80%81%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF%E4%B8%8E%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E3%80%81%E5%BE%AE%E4%BB%BB%E5%8A%A1%E4%B8%8E%E5%AE%8F%E4%BB%BB%E5%8A%A1.md](https://github.com/ZavierTang/zavier-notes/blob/master/JavaScript/同步與異步、事件循環與消息隊列、微任務與宏任務.md)