從JavaScript的事件循環到Promise

2020-12-16 天天手作

JS線程是單線程運行機制,就是自己按順序做自己的事,瀏覽器線程用於交互和控制,JS可以操作DOM元素,

說起JS中的異步時,我們需要注意的是,JS中其實有兩種異步,一種是基於瀏覽器的異步IO,比如Ajax,另外一種是基於計時方法setTimeout和setInterval的異步。於異步IO,比如ajax,寫代碼的時候都是順序執行的,但是在真正處理請求的時候,有一個單獨的瀏覽器線程來處理,並且在處理完成後會觸發回調。這種情況下,參與異步過程的其實有2個線程主體,一個是javascript的主線程,另一個是瀏覽器線程。

熟悉Javascript的人都知道,Javascript內部有一個事件隊列,所有的處理方法都在這個隊列中,Javascript主線程就是輪詢這個隊列處理,這個好處是使得CPU永遠是忙碌狀態。這個機制和Windows中的消息泵是一樣的,都是靠消息(事件)驅動,

對於setTimeout和setInterval來說,當js線程執行到該代碼片段時,js主線程會根據所設定的時間,當設定的時間到期時,將設置的回調函數放到事件隊列中,然後js主線程繼續去執行下邊的代碼,當js線程執行完主線程上的代碼之後,會去循環執行事件隊列中的函數。至於setTimeout或者setInterval設定的執行時間在實際表現時會有一些偏差,普遍的一個解釋為,當定時任務的時間到期時,本應該去執行該回調函數,但是這時js主線程可能還有任務在執行,或者是該回調函數再事件隊列中排的比較靠後,就導致該回調函數執行時間與定時器設定時間不一致。

那麼問題來了,什麼是事件隊列?

eventloop是一個用作隊列的數組,eventloop是一個一直在循環執行的,循環的每一輪成為一個tick,在每一個tick中,如果隊列中有等待事件,那麼就會從隊列中摘取下一個事件進行執行,這些事件就是我們之前的回調函數。現在ES6精確指定了事件循環的工作細節,這意味著在技術上將其納入了JavaScript引擎的勢力範圍,而不只是由宿主環境決定了,主要的一個原因是ES6中promise的引入。var eventloop = []

var event;

while(true){

if(eventloop.length>0){

//拿到隊列中的下一個事件

event = eventloop.shift();

//現在執行下一個事件

try{

event();

}catch(e){

reportError(e);

}

}

}

在瀏覽器端,setTimeout中的最小時間間隔是W3C在HTML標準中規定,規定要求低於4ms的時間間隔算為4ms。

任何時候,只要把一個代碼包裝成一個函數,並指定它在響應某個事件時執行,你就是在代碼中創建了一個將來執行的模塊,也由此在這個程序中引入了異步機制。

js引擎並不是獨立運行的,它運行在宿主環境中,就是我們所看到的Web瀏覽器,當然,隨著js的發展,包括最近的Node,便是給js提供了一個在伺服器端運行的環境。並且現在的js還嵌入到了機器人到電燈泡的各種各樣的設配中。

但是這些所有的環境都有一個共同的「點」,即為都提供了一種機制來處理程序中的多個塊的執行,且執行每個塊時調用JavaScript引擎,這種機制被稱為事件循環。

js引擎本身並沒有時間的概念,只是一個按需要執行JavaScript任意代碼片段的環境。

對於js中的回調,我們最常見的就是鏈式回調和嵌套回調

我們經常再ajax中嵌套ajax調用然後再嵌套ajax調用,這就是回調地獄,回調最大的問題就是控制反轉,它會導致信任鏈的完全斷裂,為了解決回調中的控制反轉問題,有些API提供了分離回調(一個用於成功通知,一個用於失敗通知),例如ajax中的success函數和failure函數,這種情況下,API的出錯處理函數failure()常常是可以省略的,如果沒有提供的話,就是假定這個錯誤可以吞掉。

還有一種回調模式叫做「error-first"風格,其中回調的第一個參數保留用作錯誤對象,如果成功的話,這個參數就會被清空/置假。

回調函數是JavaScript異步的基礎單元,但是隨著JavaScript越來越成熟,對於異步領域的發展,回調已經不夠用了。

Promise

Promise 是異步編程中的一種解決方案,最早由社區提出和實現,ES6將其寫進了語言標準,統一了用法,原生提供了Promise對象。

Promise是一種封裝和組合未來值的易於復用的機制。一種在異步任務中作為兩個或更多步驟的流程控制機制,時序上的this-then-that. 假定調用一個函數foo(),我們並不需要去關心foo中的更多細節,這個函數可能立即完成任務,也可能過一段時間才去完成。對於我們來講,我們只需要知道foo()什麼時候完成任務,這樣我們就可以去繼續執行下一個任務了,在傳統的方法中,我們回去選擇監聽這個函數的完成度,當它完成時,通過回調函數通知我們,這時候通知我們就是執行foo中的回調,但是使用promise時,我們要做的是偵聽來自foo的事件,然後在得到通知的時候,根據情況而定。

其中一個重要的好處就是,我們可以把這個事件中的偵聽對象提供給代碼中多個獨立的部分,在foo()完成的時候,他們都可以獨立的得到通知:

var evt = foo();

//讓bar()偵聽foo()的完成

bar(evt);

//讓baz()偵聽foo()的完成

baz(evt);

上邊的例子中,bar和baz中不需要去知道或者關注foo中的實現細節。而且foo也不需要去關注baz和bar中的實現細節。

同樣的道理,在promise中,前面的代碼片段會讓foo()創建並返回一個Promise實例,而且在這個Promise會被傳遞到bar()和baz()中。所以本質上,promise就是某個函數返回的對象。你可以把回調函數綁定再這個對象上,而不是把回調函數當成參數傳進函數。

const promise = doSomething();

promsie.then(successCallback,failureCallback){

}

當然啦,promise不像舊式函數將回調函數傳遞到兩個處理函數中,而且會有一個優點:

在JavaScript事件隊列的本次tick運行完成之前,回調函數永遠不會執行。

通過.then形式添加的回調函數,甚至都在異步操作完成之後才被添加的函數,都會被調用。

通過多次調用.then,可以添加多個回調函數,他們會按照插入順序並且獨立運行。

但是,Promise最直接的好處就是鏈式調用。

doSomething().then(function(result) {

return doSomethingElse(result);

})

.then(function(newResult) {

return doThirdThing(newResult);

})

.then(function(finalResult) {

console.log('Got the final result: ' + finalResult);

})

.catch(failureCallback);

並且在一個失敗操作之後還可以繼續使用鏈式操作,即使鏈式中的一個動作失敗之後還能有助於新的動作繼續完成。

在調用Promise中的resolve()和reject()函數時如果帶有參數,那麼他們的參數會被傳遞給回調函數。

Promise.resolve()和Promise.reject()是手動創建一個已經resolve或者reject的promise快捷方法。通常,我們可以使用Promise.resolve()去鏈式調用一個由異步函數組成的數組。例如:

Promise.resolve().then(func1).then(func2);

Promise.all()和Promise。race()是並行運行異步操作的兩個組合式工具。

Promise.then()方法用來分別指定resolved狀態和rejected狀態的回調函數。傳遞到then中的函數被置入了一個微任務隊列,而不是立即執行,這意味著它是在JavaScript事件隊列的所有運行結束了,事件隊列被清空之後才開始執行

let promise = new Promise(function(resolve, reject) {

console.log('Promise');

resolve();

});

promise.then(function() {

console.log('resolved.');

});

console.log('Hi!');

// Promise

// Hi!

// resolved

Promise.then()方法返回一個Promise,它最多需要有兩個參數:Promise的成功和失敗情況的回調函數。

p.then(onFulfilled, onRejected);

p.then(function(value) {

// fulfillment

}, function(reason) {

// rejection

});

onFulfilled:當Promise變成接受狀態(fulfillment)時,該參數作為回調函數被調用。該函數有一個參數,即接受的值。

onRejected:當Promise變成拒絕狀態時,該參數作為回調函數被調用。該函數有一個參數,即拒絕的原因。

Promise的狀態一旦改變,就永久保持該狀態,不會再改變了。

Promise中的錯誤處理

一般的情況,我們會在每次的Promise中拋出錯誤,在Promise中的then函數中的rejected處理函數會被調用,這是我們作為錯誤處理的常用方法:

let p = new Promise(function(resolve,reject){

reject('error');

});

p.then(function(value){

success(value);

},function(error){

error(error)

}

)

但是一種更好的方式是使用catch函數,這樣可以處理Promise內部發生的錯誤,catch方法返回的還是一個Promise對象,後邊還可以接著調用then方法。而且catch方法儘量寫在鏈式調用的最後一個,避免後邊的then方法的錯誤無法捕獲。

let p = new Promise(function(resolve,reject){

reject('error');

});

p.then(function(value){

success(value);

}).catch(function(error){

console.log('error');

}}

Promise.finally()函數,該方法是ES2018引入標準的。指定不管Promise對象最後狀態如何,都會執行的操作。finally方法的回調函數不接受任何參數,這意味著沒有辦法知道,前面的Promise狀態到底是fulfilled還是rejected。這標明,finally方法裡面的操作,是與狀態無關的,不依賴於Promise的執行結果。

作者:學習會讓你青春永駐

相關焦點

  • JavaScript之Promise對象
    javascript是單線程語言,所以都是同步執行的,要實現異步就得通過回調函數的方式,但是過多的回調會導致回調地獄,代碼既不美觀,也不易維護,所以就有了promise;Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。
  • JavaScript 異步與 Promise 實現
    >onRejected: 在promise被拒絕後調用且僅調用一次該方法,該方法接受promise拒絕原因作參數;兩個函數都是異步事件的回調,符合JavaScript事件循環處理流程返回值該方法必須返回一個promise:    var promise2 = promise1.then(onFulfilled
  • 從promise讀懂JavaScript異步編程
    如上圖,按照事件循環機制執行後,本來希望是先列印』獲取成功』再列印』before readFile2』,因為都是宏任務,執行是先進先出的隊列執行方式執行的,但是因為在node.js中,事件循環是分不同的執行階段的,而不是按照先進先出的執行機制,這就會導致不知道異步執行的任務是何時結束執行的,從而導致如果有多個事件時,是很難確定回調函數到底是什麼時候執行。
  • 面試官:為什麼 Promise 比setTimeout() 快?
    2.事件循環與異步 JS 相關的問題可以通過研究事件循環來回答。我們回顧一下異步 JS 工作方式的主要組成部分。調用堆棧是一個LIFO(後進先出)結構,它存儲在代碼執行期間創建的執行上下文。最後,事件循環永久監聽調用堆棧是否為空。如果調用堆棧為空,則事件循環查看作業隊列或任務隊列,並將準備執行的任何回調分派到調用堆棧中。3.作業隊列與任務隊列我們從事件循環的角度來看這個實驗,我將對代碼執行進行一步一步的分析。
  • JavaScript異步與Promise實現
    第二種,window.onerror事件處理器,所有未捕獲異常都會自動進入此事件回調onRejected: 在promise被拒絕後調用且僅調用一次該方法,該方法接受promise拒絕原因作參數;兩個函數都是異步事件的回調,符合JavaScript事件循環處理流程返回值該方法必須返回一個promise:var promise2 = promise1.then(onFulfilled, onRejected);
  • JavaScript 執行機制
    事件循環的順序,決定js代碼的執行順序。進入整體代碼(宏任務)後,開始第一次循環。接著執行所有的微任務。然後再次從宏任務開始,找到其中一個任務隊列執行完畢,再執行所有的微任務。ok,第一輪事件循環結束了,我們開始第二輪循環,當然要從宏任務Event Queue開始。我們發現了宏任務Event Queue中setTimeout對應的回調函數,立即執行。結束。事件循環,宏任務,微任務的關係如圖所示:
  • 【JavaScript】Promise函數的用法
    javascript
  • 這一次,徹底弄懂 JavaScript 執行機制
    所以一切javascript版的"多線程"都是用單線程模擬出來的,一切javascript多線程都是紙老虎!2.javascript事件循環既然js是單線程,那就像只有一個窗口的銀行,客戶需要排隊一個一個辦理業務,同理js任務也要一個一個順序執行。如果一個任務耗時過長,那麼後一個任務也必須等著。
  • JavaScript Promise啟示錄
    【編者按】JavaScript是一種基於對象和事件驅動並具有相對安全性的客戶端腳本語言。自推出後就大受開發者的青睞,基於JavaScript的開發工具也不計其數,開發者們可以靈活選擇,輕鬆構建應用。
  • ES6 Promise 的最佳實踐
    在本文中,我將討論這些年來學到的最佳實踐,這些最佳實踐可以幫助我充分利用異步 JavaScript。處理 promise rejections沒有什麼比 unhandled promise rejection(未處理的 promise 錯誤) 更讓人頭疼了。
  • 記兩道關於事件循環的題
    本文轉載自【微信公眾號:前端人,ID:FrontendPeople】經微信公眾號授權轉載,如需轉載與原文作者聯繫其一群裡看到的一道事件循環的題:async function async1() {console.log(
  • 理解異步之美--- Promise與async await(一)
    在javascript中這樣的人就是Promise。Promise的實例有三個狀態,Pending(進行中)、Resolved(已完成)、Rejected(已拒絕)。當你把一件事情交給promise時,它的狀態就是Pending,任務完成了狀態就變成了Resolved、沒有完成失敗了就變成了Rejected。
  • JavaScript函數 - 事件驅動
    什麼是事件驅動函數? 最後給大家補充一個知識,叫做事件驅動函數,它到底是做什麼的呢?在頁面交互的過程中所調用的函數,該函數被稱之為事件驅動函數。現在先來簡單的了解一下,以後會詳細講到什麼是事件?和頁面交互的行為稱之為事件比如:滑鼠點擊某個按鈕時(onclick)、滑鼠浮動,或者滑鼠離開某一個區域(onmouseover、onmouseout)、文本框獲取焦點和失去焦點時(onfocus、onblur)等等如果我們想給一個按鈕綁定一個事件,就要通過事件驅動函數來綁定,並且通過id來找到它<script type = "text/javascript
  • 從setTimeout(fn,0)函數剖析JavaScript的執行機制
    瀏覽器事件觸發線程:當一個事件被觸發時該線程會把事件添加到待處理隊列的隊尾,等待 JS 引擎的處理。這些事件可以是當前執行的代碼塊如定時任務、也可來自瀏覽器內核的其他線程如滑鼠點擊、AJAX 異步請求等,但由於JS的單線程關系所有這些事件都得排隊等待 JS 引擎處理。
  • 初學者應該看的JavaScript Promise 完整指南
    Promise racePromise.race(iterable) 方法返回一個 promise,一旦迭代器中的某個promise解決或拒絕,返回的 promise就會解決或拒絕。稍後,我們讀取文件2並將其再次附加到output文件。如你所見,writeFile promise返回文件的內容,你可以在下一個then子句中使用它。如何連結多個條件承諾?你可能想要跳過 Promise 鏈上的特定步驟。有兩種方法可以做到這一點。
  • 前端Javascript進階-ES6中Promise對象
    >$.ajax({ success: function(){ $.ajax({ success: function(){ } }) } })1.Promise介紹(1)異步編程的一種解決方案,比傳統的回調函數更合理(2)簡單說就是一個容器,裡面保存著某個未來才會結束的事件的結果
  • 遊戲開發之旅-JavaScript事件循環
    本節是第四講的第三十三小節,上一節我們為大家介紹了JavaScript內存管理,本節將為大家介紹JavaScript事件循環的原理。並發模型與事件循環(Concurrency model and the event loop)JavaScript有一個基於事件循環的並發模型,事件循環負責執行代碼、收集和處理事件以及執行隊列中的子任務。
  • 新特性for-of循環,讓javascript程序語言重獲超強生命力
    在ES6中,新增特性for-of循環,javascript程序語言在for循環方面,功能更加豐富強大,重獲超強生命力!在計算機發展過程中,計算機程式語言一直被視為計算機硬體的靈魂。作為前端編程的主流程式語言,javascript語言也不例外。在javascript程序語言,新增特性for-of循環,讓循環更加簡潔直接,功能更加豐富多樣。克服了for-in循環和forEach循環的不足,給javascript語言帶來了新的活力。在本例中,定義了一個for_ofloop函數,在該函數內定義可兩個變量,一個為字符串ForArray,和一個數組forArray。
  • 關於JS事件循環機制,我今天終於整明白了
    而這些隊列由js的事件循環(EventLoop)來搞定。macro-task(宏任務)與micro-task(微任務),在最新標準中,它們被分別稱為task與jobs。由於寫文章時沒有注意到,實際上宏任務與微任務的概念是不準確的,感謝評論中讀者指出來,但由於文章中涉及多處宏任務、微任務的解讀,所以本文暫時還是用宏任務、微任務來分別代指task、jobs。
  • 3.2.4 JavaScript循環語句嵌套的應用
    如果在一個循環語句中包含其他的循環語句,我們稱為循環語句的嵌套。對於JavaScript中的while循環語句、do-while循環語句和for循環語句都是可以互相嵌套的。而且根據嵌套的層次,可以分為2層循環、3層循環等。