事件循環機制EventLoop

事件循環機制EventLoop

Event Loop即事件循環,是解決javaScript單線程運行阻塞的一種機制。

一、EventLoop的相關概念

img

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,是一種先進先出的一種數據結構。在隊尾添加新元素,從隊頭移除元素。

img

二、同步任務和異步任務

javascript是單線程。單線程就意味着,所有任務需要排隊,前一個任務結束,纔會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等着。
於是js所有任務分爲兩種:同步任務,異步任務
同步任務是調用立即得到結果的任務,同步任務在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;

異步任務是調用無法立即得到結果,需要額外的操作才能預期結果的任務,異步任務不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務可以執行了,該任務纔會進入主線程執行。
JS引擎遇到異步任務(DOM事件監聽、網絡請求、setTimeout計時器等),會交給相應的線程單獨去維護異步任務,等待某個時機(計時器結束、網絡請求成功、用戶點擊DOM),然後由 事件觸發線程 將異步對應的 回調函數 加入到消息隊列中,消息隊列中的回調函數等待被執行。

具體來說,異步運行機制如下:

  • (1)所有同步任務都在主線程上執行,形成一個[執行棧]
  • (2)主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
  • (3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,於是結束等待狀態,進入執行棧,開始執行。
  • (4)主線程不斷重複上面的第三步。

主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,所以整個的這種運行機制又稱爲Event Loop(事件循環)

img

舉個例子

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 over0毫秒後添加到任務隊列隊尾,timer 1 over1秒添加到任務隊列隊尾,等待主線程任務執行完,從隊頭依次執行任務隊列中的任務

三、宏任務和微任務

同步和異步的執行機制在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全部代碼、setTimeoutsetIntervalsetImmediate(瀏覽器暫時不支持,只有IE10支持,具體可見MDN)、I/OUI Rendering

MicroTask(微任務):* Process.nextTick(Node獨有)PromiseObject.observe(廢棄)MutationObserver(具體使用方式查看這裏

在掛起任務時,JS 引擎會將所有任務按照類別分到這兩個隊列中,首先在 宏任務 的隊列中取出第一個任務,執行完畢後取出 微任務 隊列中的所有任務順序執行;之後再取 宏任務,周而復始,直至兩個隊列的任務都取完。

img

給個圖加深理解

img

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對象添加回調函數,也會立即得到這個結果

img

優點:將異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數。此外,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語句,一直運行到結束,返回對象的valueundefined

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)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章