詳解JavaScript如何利用異步解密回調(diào)地獄
JavaScript 是一門單線程的編程語言,意味著同一時(shí)間只能執(zhí)行一個(gè)任務(wù)。然而,現(xiàn)代的 Web 應(yīng)用程序通常需要處理大量的異步操作,如網(wǎng)絡(luò)請求、文件讀寫、用戶輸入等。如果這些所有操作都是同步的,會導(dǎo)致應(yīng)用程序在等待某些操作完成時(shí)被阻塞,用戶體驗(yàn)將變得非常差。
為了更好地處理這些異步操作,JavaScript 引入了異步編程的概念。在同步編程中,代碼會按照順序執(zhí)行,每個(gè)操作都要等待前一個(gè)操作完成才能繼續(xù)。相比之下,異步編程的代碼不是按順序執(zhí)行的,允許程序在執(zhí)行某個(gè)操作時(shí)不必等待該任務(wù)完成,而是可以繼續(xù)執(zhí)行后續(xù)的代碼。異步編程在處理非阻塞 I/O 操作、事件驅(qū)動等方面發(fā)揮了重要作用,為提高性能、避免阻塞和提供更好的用戶體驗(yàn)等方面帶來了優(yōu)勢。
回調(diào)函數(shù)(Callback)
在 JavaScript 中,最早的異步編程方式是通過回調(diào)函數(shù)。當(dāng)需要執(zhí)行一個(gè)耗時(shí)的操作時(shí),比如發(fā)起一個(gè)網(wǎng)絡(luò)請求或讀取文件,可以通過將一個(gè)函數(shù)作為參數(shù)傳遞給異步操作,并在操作完成后調(diào)用該函數(shù),實(shí)現(xiàn)異步回調(diào)。
隨著應(yīng)用程序變得更為復(fù)雜,多個(gè)異步操作的嵌套使用會導(dǎo)致回調(diào)函數(shù)的嵌套,形成所謂的回調(diào)地獄(Callback Hell)。
getData(function(result1) {
processData(result1, function(result2) {
processMoreData(result2, function(result3) {
// ... 還有更多嵌套的回調(diào)
});
});
});
這種嵌套結(jié)構(gòu)使代碼高度耦合,難以理解和維護(hù),容易引發(fā)錯(cuò)誤。在回調(diào)函數(shù)中,不僅難以清晰地捕捉和處理錯(cuò)誤,且無法使用return語句。為了克服回調(diào)地獄的問題,涌現(xiàn)了一系列解決方案。
觀察者模式
觀察者模式定義了對象間一對多的依賴關(guān)系,當(dāng)一個(gè)對象的狀態(tài)發(fā)生改變時(shí),所有依賴于它的對象都會得到通知并自動更新。在 JavaScript 中,事件監(jiān)聽是觀察者模式的一種實(shí)現(xiàn)方式。
事件監(jiān)聽是一種常見的異步編程方式,特別適用于基于事件觸發(fā)的場景,如瀏覽器的 DOM 操作和 NodeJS 的事件驅(qū)動。通過事件監(jiān)聽,程序可以注冊對特定事件的監(jiān)聽器,在事件發(fā)生時(shí)執(zhí)行相應(yīng)的回調(diào)函數(shù)。
// 在瀏覽器中
// 事件監(jiān)聽
document.getElementById('myButton').addEventListener('click', function() {
console.log('Button clicked!');
});
// 觸發(fā)事件
document.getElementById('myButton').click(); // 模擬按鈕點(diǎn)擊事件
// 在 Node.js 中
const EventEmitter = require('events');
// 主題
class Subject extends EventEmitter {
setState(state) {
this.emit('stateChange', state);
}
}
// 觀察者
class Observer {
update(state) {
console.log('Received updated state:', state);
}
}
const subject = new Subject();
const observer = new Observer();
// 添加觀察者到主題
subject.on('stateChange', observer.update.bind(observer));
subject.setState('newState');
RxJS 也是一個(gè)基于觀察者模式的庫,它通過使用可觀察對象(Observables)來處理異步和事件驅(qū)動的編程。具體使用可參考RxJS文檔。
觀察者模式使得代碼更具模塊化和解耦性。然而,需要注意整個(gè)程序變成事件驅(qū)動模型,且事件監(jiān)聽器的注冊和觸發(fā)被分離,可能導(dǎo)致運(yùn)行流程不夠清晰。在使用觀察者模式時(shí),需權(quán)衡使用場景,以確保代碼結(jié)構(gòu)清晰且易于理解。
發(fā)布訂閱模式
發(fā)布-訂閱是一種消息范式,它也是一種處理異步編程的方式,類似于觀察者模式,但有一個(gè)中間的事件通道,通常被稱為事件總線。消息的發(fā)送者不會將消息直接發(fā)送給特定的接收者。而是將發(fā)布的消息分為不同的類別,無需了解哪些訂閱者可能存在。同樣的,訂閱者可以表達(dá)對一個(gè)或多個(gè)類別的興趣,只接收感興趣的消息,無需了解哪些發(fā)布者存在。
相對于事件監(jiān)聽,發(fā)布-訂閱模式耦合度更低,允許發(fā)布者和訂閱者之間進(jìn)行解耦。在 Node.js 中,EventEmitter 可以同時(shí)實(shí)現(xiàn)觀察者模式和發(fā)布訂閱模式的特性,具體取決于使用方式。
以下是發(fā)布-訂閱模式的簡單實(shí)現(xiàn):
const EventEmitter = require('events');
class EventBus extends EventEmitter {}
const eventBus = new EventBus();
// 訂閱者
function subscriber(state) {
console.log('Received updated state:', state);
}
// 添加訂閱者到事件總線
eventBus.on('stateChange', subscriber);
// 發(fā)布者
function publisher(newState) {
eventBus.emit('stateChange', newState);
}
// 觸發(fā)狀態(tài)變化
publisher('newState');
發(fā)布訂閱模式和觀察者模式都是解決回調(diào)地獄問題的有效手段。發(fā)布訂閱模式以其靈活性和松散耦合的特點(diǎn),更適用于復(fù)雜的系統(tǒng),而觀察者模式則更為簡單,適用于規(guī)模較小的場景。
然而,在處理多個(gè)觀察者或訂閱者時(shí),這兩種模式可能會使代碼變得復(fù)雜,增加維護(hù)和理解的成本。因此,在選擇模式時(shí),需要根據(jù)項(xiàng)目規(guī)模和復(fù)雜性權(quán)衡各種因素,以確保代碼的清晰性和可維護(hù)性。
Promise
Promise 是 ES6 中新增的一種異步編程解決方案,它是對回調(diào)函數(shù)的一種補(bǔ)充,可以避免回調(diào)地獄問題。Promise 本質(zhì)上是一個(gè)容器,里面保存著某個(gè)未來才會結(jié)束的事件(通常是一個(gè)異步操作)的結(jié)果。
一個(gè) Promise 對象包含其原型,狀態(tài)值(pending/fulfilled/rejected),值(then 方法返回的值)。Promise 對象創(chuàng)建后將立即執(zhí)行,其中的函數(shù)執(zhí)行會阻塞,而函數(shù)參數(shù) resolve 和 reject 的執(zhí)行時(shí)則會掛到微任務(wù)中執(zhí)行。
狀態(tài)值:
Pending(進(jìn)行中):表示異步操作尚未完成,仍在進(jìn)行中。
Fulfilled(已完成):表示異步操作成功完成,可以通過 then 方法獲取結(jié)果。
Rejected(已失敗):表示異步操作失敗,可以通過 catch 方法或另一個(gè)then中的第二個(gè)參數(shù)捕捉錯(cuò)誤。
核心方法:
resolve:將 Promise 狀態(tài)從 pending 轉(zhuǎn)變?yōu)?fulfilled,并傳遞結(jié)果值。
reject:將 Promise 狀態(tài)從 pending 轉(zhuǎn)變?yōu)?rejected,并傳遞錯(cuò)誤信息。
創(chuàng)建 Promise 對象的基本語法如下:
const promise = new Promise((resolve, reject) => {
// 異步操作,只有執(zhí)行 resolve/reject 才可以決定 Promise 狀態(tài),任何其他操作都無法改變這個(gè)狀態(tài)。一旦狀態(tài)改變,就不會再變。
if (/* 異步操作成功 */) {
resolve(result); // 將結(jié)果傳遞給 then
} else {
reject(error); // 將結(jié)果傳遞給 then 的第二個(gè)參數(shù)或者 catch 捕獲
}
});
Promise 的鏈?zhǔn)秸{(diào)用通過 then 方法實(shí)現(xiàn),可以更清晰地表達(dá)異步操作的順序和邏輯:
promise
.then(result => {
// 處理成功的結(jié)果
return result;
})
.then(result2 => {
// 處理result2
return result2;
})
.then(result3 => {
// 處理result3
return result3
})
.catch(error => {
// 處理錯(cuò)誤
});
在Promise中,then 方法的第一個(gè)參數(shù)回調(diào)函數(shù)用于處理異步操作成功的結(jié)果,而 then 方法的第二個(gè)參數(shù)回調(diào)或 catch 方法則用于處理異步操作失敗的情況。
Promises 廣泛應(yīng)用于處理 JavaScript 中的異步操作,包括 AJAX 請求、定時(shí)任務(wù)、文件操作等。盡管 Promise 解決了回調(diào)地獄等問題,但也有一些缺點(diǎn):
其一,無法取消 Promise 中的 fn,一旦新建它就會立即執(zhí)行,無法中途取消。
其二,如果不設(shè)置回調(diào)函數(shù),Promise 內(nèi)部拋出的錯(cuò)誤,不會反應(yīng)到外部。
其三,當(dāng)處于 pending 狀態(tài)時(shí),無法得知目前進(jìn)展到哪一個(gè)階段(剛剛開始還是即將完成)。
其四,代碼冗余,原來的任務(wù)被 Promise 包裝了一下,不管什么操作,一眼看去都是一堆 then,語義變得不清晰。
Generator 函數(shù)
ES6 引入的 Generator 函數(shù)是一種強(qiáng)大的異步編程解決方案,實(shí)際上是協(xié)程的一種實(shí)現(xiàn)。作為狀態(tài)機(jī),Generator 函數(shù)封裝了多個(gè)內(nèi)部狀態(tài),通過使用 yield 和 return 表達(dá)式實(shí)現(xiàn)了執(zhí)行的暫停和恢復(fù)。執(zhí)行 Generator 函數(shù)返回一個(gè)遍歷器對象,通過該對象的 next、return 和 throw 方法,可以逐步遍歷 Generator 函數(shù)內(nèi)部的每個(gè)狀態(tài)。這一特性使得異步編程更加直觀和可控。
yield 表達(dá)式: 標(biāo)記暫停執(zhí)行的位置,只有當(dāng)調(diào)用 next 方法、內(nèi)部指針指向該語句時(shí)才會執(zhí)行,為 JavaScript 提供了手動的“惰性求值”功能。
function* gen() {
yield 123 + 456;
}
const generatorInstance = gen();
// 惰性求值,只在需要時(shí)觸發(fā)計(jì)算
console.log(generatorInstance.next().value); // 輸出: 579
return 表達(dá)式: 標(biāo)記 Generator 函數(shù)的結(jié)束位置,調(diào)用 return 方法后,Generator 函數(shù)執(zhí)行會終止,并且后續(xù)的邏輯將不再執(zhí)行。
function* myGenerator() {
yield 1;
return 2; // 執(zhí)行到return時(shí),生成器函數(shù)終止
yield 3; // 這個(gè)語句不會執(zhí)行
}
const generator = myGenerator();
console.log(generator.next()); // 輸出: { value: 1, done: false }
console.log(generator.next()); // 輸出: { value: 2, done: false }
console.log(generator.next()); // 輸出: { value: undefined, done: true }
Generator.prototype.next():用于恢復(fù) Generator 函數(shù)的執(zhí)行,可以帶一個(gè)參數(shù),該參數(shù)會被當(dāng)作上一個(gè) yield 表達(dá)式的返回值。Generator 函數(shù)從暫停狀態(tài)到恢復(fù)運(yùn)行,它的上下文狀態(tài)(context)是不變的。通過next方法的參數(shù),就有辦法在 Generator 函數(shù)開始運(yùn)行之后,繼續(xù)向函數(shù)體內(nèi)部注入值。也就是說,可以在 Generator 函數(shù)運(yùn)行的不同階段,從外部向內(nèi)部注入不同的值,從而調(diào)整函數(shù)行為。
function* f() {
for(var i = 0; true; i++) {
var reset = yield i;
if(reset) { i = -1; }
}
}
var g = f();
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }
Generator.prototype.return():返回給定的值,并終結(jié)遍歷 Generator 函數(shù)。
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }
遍歷器對象 g 調(diào)用 return() 方法后,返回值的 value 屬性就是 return() 方法的參數(shù) foo。并且,Generator 函數(shù)的遍歷就終止了。如果 return() 方法調(diào)用時(shí),不提供參數(shù),則返回值的 value 屬性為 undefined。
Generator.prototype.throw():可以在函數(shù)體外拋出錯(cuò)誤,然后在 Generator 函數(shù)體內(nèi)捕獲。
var g = function* () {
try {
yield;
} catch (e) {
console.log('內(nèi)部捕獲', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕獲', e);
}
// 內(nèi)部捕獲 a
// 外部捕獲 b
遍歷器對象 i 連續(xù)拋出兩個(gè)錯(cuò)誤。第一個(gè)錯(cuò)誤被 Generator 函數(shù)體內(nèi)的 catch 語句捕獲。i 第二次拋出錯(cuò)誤,由于 Generator 函數(shù)內(nèi)部的 catch 語句已經(jīng)執(zhí)行過了,不會再捕捉到這個(gè)錯(cuò)誤了,所以這個(gè)錯(cuò)誤就被拋出了 Generator 函數(shù)體,被函數(shù)體外的 catch 語句捕獲。
yield* 表達(dá)式:如果在 Generator 函數(shù)內(nèi)部,調(diào)用另一個(gè) Generator 函數(shù)。需要在前者的函數(shù)體內(nèi)部,自己手動完成遍歷。
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
// 手動遍歷 foo()
for (let i of foo()) {
yield i;
}
yield 'y';
}
// 等價(jià)于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// x a b y
手動遍歷寫法非常麻煩,es6 提供了 yield* 表達(dá)式,用來在一個(gè) Generator 函數(shù)里面執(zhí)行另一個(gè) Generator 函數(shù)。
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// x a b y
Generator 函數(shù)具有廣泛的應(yīng)用場景,包括但不限于:
迭代器(Iterator):生成器函數(shù)可以用于創(chuàng)建自定義迭代器。通過使用 yield 在每次迭代中生成下一個(gè)值,可以方便地實(shí)現(xiàn)自定義迭代邏輯。
異步操作:生成器函數(shù)可以與 yield 結(jié)合使用,實(shí)現(xiàn)更清晰的異步代碼。
** 協(xié)程(Coroutine)**:生成器函數(shù)可以模擬簡單的協(xié)程,允許在函數(shù)執(zhí)行過程中暫停和恢復(fù)。
惰性計(jì)算:生成器函數(shù)可以用于處理大量的數(shù)據(jù)流,逐步生成和處理數(shù)據(jù),而不需要一次性加載所有數(shù)據(jù),優(yōu)化性能。
流式處理:多步操作非常耗時(shí),使用 Generator 按次序自動執(zhí)行所有步驟。
處理文件內(nèi)容:當(dāng)處理大型文件時(shí),Generator 可以逐行讀取文件內(nèi)容,而不必一次性讀取整個(gè)文件到內(nèi)存。
節(jié)省資源:在一些情況下,如果生成的數(shù)據(jù)只在迭代過程中使用,而不需要保存在內(nèi)存中,使用 Generator 可以節(jié)省系統(tǒng)資源。
雖然 Generator 避免了回調(diào)地獄和 Promise 鏈?zhǔn)秸{(diào)用的復(fù)雜性,但 Generator 會使得代碼更加復(fù)雜和難以理解,尤其對于初學(xué)者而言可能需要花費(fèi)更多時(shí)間來適應(yīng)和理解這種編程模型。
Async/Await
ES2017 引入了 Async/Await 語法糖,為異步編程帶來了更直觀、更易讀的方式。Async/Await 是 Generator 和 Promise 的綜合體,Async/Await 中還自帶自動執(zhí)行器,且 await 后跟的是 Promise。其語法糖特性讓異步代碼看起來更像同步代碼,消除了回調(diào)地獄問題,提高了可讀性和可維護(hù)性。
基本結(jié)構(gòu)如下:
async function myAsyncFunction() {
// 異步操作
const result = await someAsyncOperation();
return result;
}
async 函數(shù)使用關(guān)鍵字 async 聲明,內(nèi)部使用 await 關(guān)鍵字等待異步操作完成。在 async 函數(shù)中,await 暫停函數(shù)的執(zhí)行,等待 Promise 解決并返回。
錯(cuò)誤處理方面,async 函數(shù)采用傳統(tǒng)的 try-catch 結(jié)構(gòu),使代碼結(jié)構(gòu)更加清晰:
async function fetchData() {
try {
const result1 = await getData();
const result2 = await processData(result1);
const result3 = await processMoreData(result2);
// ... 后續(xù)操作
return finalResult;
} catch (error) {
// 處理錯(cuò)誤
}
}
需要注意的是,async 函數(shù)返回的 Promise 對象會在內(nèi)部所有 await 命令后面的 Promise 對象執(zhí)行完畢之后才會發(fā)生狀態(tài)改變,除非在此過程中遇到 return 語句或發(fā)生錯(cuò)誤。
總體而言,async 函數(shù)的實(shí)現(xiàn)最簡潔、最符合語義,幾乎沒有語義不相關(guān)的代碼。
總結(jié)
這些不同的異步編程方式各有優(yōu)劣,選擇合適的方式取決于項(xiàng)目的需求、復(fù)雜性和開發(fā)團(tuán)隊(duì)的經(jīng)驗(yàn)。在實(shí)際應(yīng)用中,可以根據(jù)具體情況靈活選擇異步編程的方式,以提高代碼的質(zhì)量和可維護(hù)性。
以上就是詳解JavaScript如何利用異步解密回調(diào)地獄的詳細(xì)內(nèi)容,更多關(guān)于JavaScript異步回調(diào)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
使用js正則表達(dá)式驗(yàn)證文件擴(kuò)展名方法實(shí)例
文件上傳時(shí),為了安全驗(yàn)證,對于手工改動文件后綴名產(chǎn)生的偽造文件進(jìn)行判斷過濾,下面這篇文章主要給大家介紹了關(guān)于如何使用js正則表達(dá)式驗(yàn)證文件擴(kuò)展名的相關(guān)資料,需要的朋友可以參考下2022-11-11
JavaScript和TypeScript中的void的具體使用
這篇文章主要介紹了JavaScript和TypeScript中的void的具體使用,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09
JS函數(shù)修改html的元素內(nèi)容,及修改屬性內(nèi)容的方法
下面小編就為大家?guī)硪黄狫S函數(shù)修改html的元素內(nèi)容,及修改屬性內(nèi)容的方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-10-10
JS實(shí)現(xiàn)面包屑導(dǎo)航功能從零開始示例
這篇文章主要為大家介紹了JS實(shí)現(xiàn)面包屑導(dǎo)航功能從零開始示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12
js 靜態(tài)動態(tài)成員 and 信息的封裝和隱藏
一下用面向?qū)ο蟮南嚓P(guān)概念來解釋js中的仿面向?qū)ο?,因?yàn)閖s中不像其他語言,不存在面向?qū)ο笳Z言的相關(guān)特性2011-05-05
100多個(gè)基礎(chǔ)常用JS函數(shù)和語法集合大全
本文將介紹100多個(gè)基礎(chǔ)常用JS函數(shù)和語法,具有很好的參考價(jià)值,下面跟著小編一起來看下吧2017-02-02

