在現代JavaScript開發中,非同步程式設計已成為不可或缺的一部分,它使我們能夠處理耗時操作,如網路請求、檔案讀寫,而不阻塞主執行緒。在最近的學習中瞭解到了Promise和Event Loop,本文將深入探討他們的工作原理、如何優雅地使用Promise控制非同步流程,以及其在JavaScript事件迴圈機制中的角色。
非同步程式設計的需求
在Web開發或其他涉及I/O操作的場景中,經常遇到需要等待某個操作完成才能繼續執行的情況。傳統的解決方案是使用回撥函式,但隨著業務複雜度上升,回撥函式層層巢狀,形成了所謂的“回撥地獄”,這不僅使得程式碼難以閱讀和維護,還可能引發除錯難題。
如下面程式碼,要求需要執行完a再執行b再執行c執行d(假設每個方法中都存在setTimeout),當巢狀無限多時,簡直就是地獄。
function a(cbB,cbC,abD){ cbB(cbC,cbD) } function b(cb,cbD){ cb(cbD) } function c(cb){ cb() } function d(){ } a(b,c,d)
Promise的誕生
Promise,正如其名,代表了一個未來的值,它或者成功(resolved)並攜帶結果資料,或者失敗(rejected)並攜帶錯誤資訊。Promise的設計初衷就是爲了解決非同步程式設計中的複雜性問題,提供一種更優雅、鏈式呼叫的方式來組織非同步操作。
Promise的基本使用
透過new Promise((resolve, reject) => {...})
建構函式建立,其中resolve
和reject
是兩個函式,分別用於改變Promise的狀態。
我以一個案例來給大家講解一下。當getup()
狀態為resolve()
時.then()
中的computer()
開始執行。
function getup() { //pending resloved rejected return new Promise((resolve, reject) => { setTimeout(() => { console.log('小帥起床了'); resolve() }, 2000) }) } function computer() { return new Promise((resolve, reject) => { setTimeout(() => { console.log('小帥開啟了電腦'); resolve() }, 1000) }) } function study(params) { console.log('小帥開始敲程式碼了'); } getup().then(() => { return computer() }) .then(() => { study() })
執行結果
.then方法
是promise原型上的一個函式,x.then函式會在x這個promise例項物件狀態變更為resolved之後才執行內部邏輯,由此藉助這個機制可以將非同步捋成同步。then方法支援鏈式呼叫,因為then預設也會返回一個promise物件,但狀態預設是pending,這就會導致後面的then用不上前面then的狀態,從而繼續往前查詢。我們在then中返回一個promise物件,會覆蓋掉then自帶的返回。
還是以上面程式碼為例,刪除computer前的return。為什麼執行結果是先敲程式碼再開啟電腦呢?
因為第一個then沒有返回值,但是它會預設返回一個Promise
,但這個Promise
的狀態預設是pending
(進行中),第二個then無法使用到第一個then的狀態,那麼第二個then就會繼續往前查詢,發現getup()
狀態為resolve
,此時它就會立即執行,而第一個then則在2s後執行。
不刪除return時,第一個then返回的是computer()
的Promise
,此時第二個then可以識別到第一個then中的狀態為resolve
,所以會等待computer()
執行完後再執行
getup().then(() => { computer() }) .then(() => { study() })
執行結果
.catch方法
用於捕獲Promise鏈中任何環節丟擲的錯誤,保證異常處理的一致性。
如一下程式碼,如果Promise的狀態為reject時,會導致程式報錯,增加.catach()方法捕捉錯誤,就會將錯誤處理不會再導致程式報錯。
function a(){ return new Promise(function(resloved,reject){ setTimeout(function(){ console.log('a is ok'); // resloved('請求到的資料') reject('錯誤') },1000) }) } function b() { console.log(1); } a().then((res)=>{ console.log(res); b() }) .catch((err)=>{ console.log(err,'xxxx'); })
執行結果
有catch時
無catch
瀏覽器引擎
JavaScript引擎和渲染引擎都是現代瀏覽器的重要組成部分。
JavaScript引擎負責解析和執行JavaScript程式碼。它讓網頁具有互動性,處理使用者事件,執行非同步操作,以及操縱網頁文件物件模型(DOM)等。知名的JavaScript引擎例如Google Chrome的V8、Mozilla Firefox的SpiderMonkey、Apple Safari的JavaScriptCore(也被稱作Nitro或SquirrelFish)等。
渲染引擎(又稱為佈局引擎或呈現引擎)則負責解析HTML和CSS,構建網頁的視覺化表示,並對其進行佈局和繪製。它確保網頁內容按照CSS樣式和網頁標準進行正確顯示。常見的渲染引擎有Blink(用於Chrome和Opera)、Gecko(用於Firefox)、WebKit(用於Safari)以及歷史上Internet Explorer的Trident(也稱MSHTML)和Microsoft Edge早期版本的EdgeHTML。
事件迴圈(Event Loop)
事件迴圈是JavaScript處理非同步操作的核心機制,確保了即使在單執行緒環境下也能高效地管理任務執行順序。
任務型別
宏任務(Macro Task) :包括
setTimeout
、setInterval
、setImmediate
(Node.js環境)、I/O操作、UI渲染等,它們在當前執行棧完成後執行。微任務(Micro Task) :如
Promise
的回撥、process.nextTick
(Node.js)、MutationObserver
等,它們在當前執行棧的末尾執行,優先順序高於宏任務。
執行流程
初始化階段:程式開始執行時,呼叫棧是空的,但底部隱含了全域性執行上下文。同時,微任務佇列和宏任務佇列也是初始狀態,宏任務佇列中通常有一個代表整個指令碼的宏任務。
執行指令碼:全域性執行上下文被推入呼叫棧,開始同步執行指令碼中的程式碼。執行過程中,任何產生的宏任務或微任務會被分別放入對應的佇列中。
指令碼執行完畢:噹噹前宏任務(通常是整個指令碼)執行結束,呼叫棧清空至全域性上下文,此時檢查微任務佇列。
執行微任務:如果微任務佇列不為空,則從隊首開始執行微任務,直至佇列為空,期間新產生的微任務也會立即執行。此過程會持續進行,直到沒有更多的微任務加入佇列。
宏任務執行:微任務全部執行完畢後,從宏任務佇列中取出下一個任務(如果有),將其推入呼叫棧執行。執行過程中,可能繼續產生新的宏任務和微任務。
渲染操作:在每次宏任務執行後,如果事件迴圈檢測到渲染時機(即呼叫棧為空且沒有待執行的微任務),渲染引擎會嘗試更新介面。
迴圈繼續:事件迴圈不斷重複上述過程,檢查佇列,執行任務,直到所有任務(包括宏任務和微任務)都被處理完。
透過這樣的機制,JavaScript能夠高效地管理同步和非同步程式碼,確保在單執行緒環境中既能夠執行復雜的非同步操作,又不阻塞UI渲染或其他任務的執行。
結語
Event Loop機制支撐起了JavaScript的非同步執行模型,而Promise作為一種高階的非同步程式設計抽象,利用事件迴圈的特性,提供了清晰、靈活的非同步解決方案,二者共同構成了現代JavaScript非同步程式設計的核心基礎。