【JS 異步】告別回調地獄:Async/Await 和 Promise 的優雅實踐與錯誤處理

【JS 異步】告別回調地獄:Async/Await 和 Promise 的優雅實踐與錯誤處理

所屬專欄: 《前端小技巧集合:讓你的代碼更優雅高效
上一篇: 【JS 數組】數組操作的“瑞士軍刀”:精通 Array.reduce() 的騷操作
作者: 碼力無邊


? 引言:那座名為“回調地獄”的金字塔,我們曾親手搭建

嘿,各位在代碼世界里追求光與熱的道友們,我是碼力無邊

在我們的前端江湖中,如果說 DOM 操作是“外家功夫”,數據處理是“內功心法”,那么異步編程,就是那門決定你能否“御劍飛行”的“輕功”。因為在 Web 世界,幾乎所有有價值的操作都是異步的:

  • 向服務器請求數據(Ajax/Fetch)
  • 讀取本地文件
  • 設置一個定時器 (setTimeout)
  • 等待用戶點擊一個按鈕

這些操作都不會立即返回結果。JavaScript 作為一門單線程語言,為了不被這些耗時的任務阻塞主線程(否則頁面就會卡死),它采用了“稍后處理”的異步模型。而在遠古時代,我們實現這種“稍后處理”的唯一方式,就是回調函數 (Callback)

讓我們一起瞻仰一下那座由我們親手搭建,又讓我們備受折磨的“史前遺跡”——回調地獄 (Callback Hell),又稱“毀滅金字塔 (Pyramid of Doom)”:

// 史前遺跡,請勿模仿
ajax('api/user/1', function(user) {console.log('獲取到用戶:', user);ajax(`api/posts?userId=${user.id}`, function(posts) {console.log('獲取到帖子:', posts);ajax(`api/comments?postId=${posts[0].id}`, function(comments) {console.log('獲取到評論:', comments);// 如果還有下一步...天啊...ajax(`api/replies?commentId=${comments[0].id}`, function(replies) {console.log('獲取到回復:', replies);// 金字塔已經高聳入云...}, function(error) {// 每一層都要處理錯誤...});}, function(error) {// ...});}, function(error) {// ...});
}, function(error) {// ...
});

這種代碼,就像一個向右無限延伸的俄羅斯套娃。它的問題顯而易見:

  • 可讀性極差:代碼邏輯不是從上到下,而是像貪吃蛇一樣扭曲。
  • 難以維護:想在中間加一步?或者修改某一步的邏輯?祝你好運。
  • 錯誤處理復雜:每一層嵌套都需要單獨處理錯誤,很容易遺漏。

為了推翻這座壓迫我們多年的“金字塔”,JavaScript 社區的先賢們進行了不懈的斗爭,最終為我們帶來了兩件劃時代的法寶:Promiseasync/await

今天,碼力無邊就將帶你走過這條從“地獄”到“天堂”的救贖之路,讓你徹底掌握現代 JavaScript 中最優雅、最強大的異步編程范式。

一、Promise:從“回調”到“承諾”的革命

Promise 的出現,是異步編程思想的一次偉大飛躍。它把“傳遞一個函數進去等待執行”的模式,變成了“給你一個承諾對象,你拿著它等結果”的模式。

一個 Promise 對象,就像一張“彩票”。你買下它的時候,它處于 pending (進行中) 狀態。未來,它可能會中獎,變成 fulfilled (已成功) 狀態,并給你獎金(結果值);也可能沒中獎,變成 rejected (已失敗) 狀態,并告訴你原因(錯誤信息)。

1.1 用 .then() 鏈式調用,拆解金字塔

Promise 最核心的變革,就是引入了 .then() 方法。.then() 方法可以接收兩個函數作為參數,一個用于處理成功狀態,一個用于處理失敗狀態。更重要的是,.then() 方法會返回一個新的 Promise 對象,這使得我們可以進行鏈式調用

讓我們用 Promise 來重構上面的“地獄”代碼:

// 假設 ajax 函數現在返回一個 Promise
ajax('api/user/1').then(user => {console.log('獲取到用戶:', user);// 返回一個新的 Promisereturn ajax(`api/posts?userId=${user.id}`); }).then(posts => {console.log('獲取到帖子:', posts);// 返回又一個新的 Promisereturn ajax(`api/comments?postId=${posts[0].id}`);}).then(comments => {console.log('獲取到評論:', comments);return ajax(`api/replies?commentId=${comments[0].id}`);}).then(replies => {console.log('獲取到回復:', replies);}).catch(error => {// 革命性的改變:用一個 .catch() 捕獲鏈條上任何一個環節的錯誤!console.error('發生錯誤:', error);});

看到了嗎?金字塔被夷為平地!代碼變成了從上到下的線性結構,邏輯清晰無比。

  • 線性流程:每一步操作都清晰地寫在一個 .then() 中。
  • 統一錯誤處理:鏈式調用中任何一個 Promise 變成 rejected,都會被最后的 .catch() 捕獲。告別了層層嵌套的錯誤處理。
1.2 Promise 的“靜態方法”:Promise.allPromise.race

Promise 還提供了一些強大的工具函數:

  • Promise.all(iterable): 并行執行,等待所有

    • 場景:你需要同時請求用戶基本信息、用戶的好友列表和用戶的相冊,三者沒有依賴關系,但你希望等它們全部成功后,再渲染頁面。
    • 用法:它接收一個 Promise 數組,返回一個新的 Promise。只有當數組中所有的 Promise 都成功時,它才會成功,并且結果是一個包含所有 Promise 結果的數組。如果其中任何一個失敗了,它就會立刻失敗。
    Promise.all([ajax('api/userInfo'),ajax('api/friendList'),ajax('api/album')
    ]).then(([userInfo, friendList, album]) => {// 在這里,三個請求都已成功完成renderPage(userInfo, friendList, album);
    }).catch(error => {// 只要有一個請求失敗,就會進入這里showErrorPage(error);
    });
    
  • Promise.race(iterable): 并行執行,誰快用誰

    • 場景:你向兩個不同的 CDN 節點請求同一個資源,哪個先返回就用哪個。或者,給一個請求設置超時:讓你的請求和 setTimeout 返回的 Promise 賽跑。
    • 用法:它也接收一個 Promise 數組,但只要其中任何一個 Promise 率先改變狀態(無論是成功還是失敗),它就會立即采用那個 Promise 的狀態和結果。
    function requestWithTimeout(url, timeout) {let timeoutPromise = new Promise((_, reject) => {setTimeout(() => reject(new Error('請求超時!')), timeout);});return Promise.race([fetch(url),timeoutPromise]);
    }requestWithTimeout('api/slow-resource', 3000).then(response => console.log('請求成功:', response)).catch(error => console.error(error.message)); // 可能是網絡錯誤,也可能是“請求超時!”
    

Promise 已經非常強大了,但它仍然需要 .then() 的回調函數語法。人類的大腦,終究還是更習慣同步的、阻塞式的代碼寫法。于是,終極形態的“異步救世主”登場了。

二、async/await:用寫同步代碼的方式,來寫異步

async/await 是 ES2017 (ES8) 引入的,它并不是一個新東西,而是建立在 Promise 之上的語法糖 (Syntactic Sugar)。它的目標只有一個:讓異步代碼看起來、寫起來都像同步代碼

兩個關鍵詞:

  • async: 用來修飾一個函數,表明這個函數是一個異步函數。任何 async 函數的返回值,都會被自動包裝成一個 Promise。
  • await: 只能用在 async 函數內部。它后面通常跟著一個 Promise。它的作用是**“暫停”當前 async 函數的執行,等待后面的 Promise 狀態變為 fulfilled,然后直接返回 Promise 的結果值**。如果 Promise 失敗了,它會拋出 (throw) 錯誤。
2.1 終極進化:最“人類友好”的異步代碼

讓我們用 async/await 來重寫我們最初的那個例子,你將見證代碼的可讀性如何達到巔峰:

async function fetchAllData() {try {// 代碼像同步一樣,從上到下執行const user = await ajax('api/user/1');console.log('獲取到用戶:', user);const posts = await ajax(`api/posts?userId=${user.id}`);console.log('獲取到帖子:', posts);const comments = await ajax(`api/comments?postId=${posts[0].id}`);console.log('獲取到評論:', comments);const replies = await ajax(`api/replies?commentId=${comments[0].id}`);console.log('獲取到回復:', replies);return replies; // async 函數的返回值} catch (error) {// 同樣革命性的改變:用標準的 try...catch 來捕獲所有 await 的錯誤!console.error('發生錯誤:', error);}
}fetchAllData().then(result => {console.log('所有數據獲取完畢:', result);
});

震撼嗎?

  • 完全同步的寫法:沒有 .then,沒有回調,代碼的執行順序和你的閱讀順序完全一致。
  • 標準的錯誤處理try...catch 是我們再熟悉不過的同步代碼錯誤處理機制,現在它完美地適用于異步流程。任何一個 await 的 Promise 失敗,都會被 catch 塊捕獲。
  • 優雅的返回值async 函數的返回值就是一個 Promise,你可以繼續在外部用 .then() 來處理最終的結果。

async/await 同樣能和 Promise.all 等工具完美結合:

async function fetchParallelData() {try {const [userInfo, friendList, album] = await Promise.all([ajax('api/userInfo'),ajax('api/friendList'),ajax('api/album')]);renderPage(userInfo, friendList, album);} catch (error) {showErrorPage(error);}
}

三、現代異步編程最佳實踐

  1. 優先使用 async/await:在任何可以使用它的地方(現代瀏覽器、Node.js、或經過 Babel 等工具編譯的環境),async/await 都應該是你的首選。它的可讀性和可維護性是無與倫比的。

  2. 不要忘記 Promise.all:在使用 async/await 時,要警惕一種反模式——串行執行本可以并行的任務。

    // 反模式:不必要的串行等待
    async function getTwoThings() {const thing1 = await fetchThing1(); // 等待 thing1const thing2 = await fetchThing2(); // thing1 好了才開始請求 thing2return [thing1, thing2];
    }// 正確模式:并行執行
    async function getTwoThingsInParallel() {const [thing1, thing2] = await Promise.all([fetchThing1(),fetchThing2()]);return [thing1, thing2];
    }
    
  3. 頂層 await:最新的 JavaScript (ES2022) 已經支持在模塊的頂層使用 await,無需包裹在 async 函數中。這在一些初始化腳本中非常有用。

寫在最后:從馴服異步,到駕馭異步

從回調地獄的混亂,到 Promise 鏈的秩序,再到 async/await 的優雅,JavaScript 的異步編程演進史,就是一部不斷追求“人性化”和“可讀性”的奮斗史。

掌握 async/await 和 Promise,你就不再是那個被異步任務牽著鼻子走的“回調奴隸”,而是一個能夠從容地編排、組織、駕馭復雜異步流程的“時間管理者”。你的代碼將不再是難以理解的“面條”,而是結構清晰、邏輯順暢的“詩篇”。

所以,道友們,請徹底告別回調地獄吧。在你的下一個項目中,大膽地擁抱 async/await,用最現代、最優雅的方式,去馴服時間,駕馭異步!


專欄預告與互動:

我們已經掌握了現代 JS 的異步核心。但代碼寫得再優雅,也得簡潔。ES6 引入了許多強大的“語法糖”,它們能讓你用更少的代碼,做更多的事。

下一篇,我們將深入 ES6+ 的代碼整潔之道,探索解構賦值和展開語法的 5 個神仙用法,讓你的代碼瞬間“瘦身”,可讀性翻倍!

碼力無邊的異步心法,你 Get 了嗎?點贊、收藏、關注,用你的三連,為我的下一次“瞬移”積蓄能量!

今日思考題: forEach 循環是同步的,如果我在 forEach 的回調里使用 await,會發生什么?它會按順序等待每一個 await 完成嗎?為什么?這是一個經典的 async/await 陷阱,把你的分析寫在評論區,我們一起探討!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/93008.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/93008.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/93008.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

23.Linux : ftp服務及配置詳解

Linux : ftp服務及配置詳解 FTP 基本概念 定義:文件傳輸協議(File Transfer Protocol),采用 C/S 模式工作。端口: 控制端口:21數據端口:20FTP 工作原理模式工作流程連接發起方主動模…

悲觀鎖樂觀鎖與事務注解在項目實戰中的應用場景及詳細解析

在今天做的項目練習部分中真的學到了很多東西,也補充了許多之前遺漏或是忘記的知識點,但時間精力有限,我就先記錄一下今天用到的一個新東西,悲觀鎖和樂觀鎖。首先給出實際應用背景:在加入鎖和事務注解之前,…

Java構造器與工廠模式(靜態工程方法)詳解

1. 構造器1.1 構造器的核心意義1.1.1 對象初始化構造器在創建對象 (new) 時自動調用, 用于初始化對象的狀態 (如設置字段初始值, 分配資源等)無構造器時: 字段為默認值(0/null/false)有構造器:確保對象創建后即處于有效狀態1.1.2 強制初始化…

解決jdk初始化運行,防火墻通信選錯專業網絡問題

問題描述新項目添加不同版本的jdk,運行時提示防火墻通信策略,選成專用網絡。其他人訪問后端接口時,提示連接失敗。 解決方案:1、在搜索欄中輸入 防火墻關鍵字,選擇到防火墻和網絡保護2、選擇允許應用通過防火墻3、先點…

【Linux】常用命令(三)

【Linux】常用命令(三)1. export1.1 原理1.2 常用語法1.3 示例1.4 書中對命令的解釋1.5 生效范圍2. 測試服務地址與其端口能否訪問2.1 nc(Netcat)命令2.2 telnet2.3 nmap2.4 curl命令 (適用于HTTP/HTTPS 服務)1. export export 是 Linux Shell&#xff…

Pytest項目_day15(yaml)

YAMLYAML是一個對所有編程語言都很友好的數據序列化標準,它是一種直觀的能夠被電腦識別的數據序列化格式,是一種可讀性高且容易被人類閱讀的腳本語言YAML語言的本質是一種通用的數據串行化格式適用場景 可以直接序列化為數組、字典解析成本低專門寫配置文…

審批流程系統設計與實現:狀態驅動、靈活擴展的企業級解決方案

審批流程系統設計與實現:狀態驅動、靈活擴展的企業級解決方案 本文基于實際企業級審批系統源碼,深入解析如何設計高擴展性、強一致性的審批流程引擎,涵蓋狀態機設計、多租戶隔離、文件服務集成等核心實現。 1. 系統設計概覽 審批系統的核心架…

汽車免拆診斷案例 | 2010款奧迪A4L車行駛中發動機偶爾自動熄火

故障現象 一輛2010款奧迪A4L車,搭載CDZ發動機 ,累計行駛里程約為18.2萬km。該車行駛中發動機偶爾自動熄火,有時熄火后能夠立即重新起動著機,有時需要等待一會兒才能重新起動著機,故障頻率較低。因該故障在其他維修廠陸…

Liam ERD:自動生成美觀的交互式實體關系圖

Liam ERD 是一個可以快速生成美觀且具有交互性的數據庫實體關系圖(ERD)的工具,可以幫助用戶實現復雜數據庫結構的可視化。 Liam ERD 是一個免費開源的項目,代碼托管在 GitHub: https://github.com/liam-hq/liam 功能…

網絡協議序列化工具Protobuf

目錄前言一、下載注意二、解壓安裝三、Protobuf的使用1、創建.proto文件2、利用protoc編譯.proto文件前言 Protocol Buffers是Google的?種語??關、平臺?關、可擴展的序列化結構數據的?法,它可?于(數據)通信協議、數據存儲等。 Protoco…

從表單校驗到API網關:全鏈路輸入安全防護指南

從表單校驗到 API 網關:全鏈路輸入安全防護指南 在軟件系統的安全防御體系中,輸入安全是第一道防線,而這道防線的堅固程度直接決定了系統抵御外部攻擊的能力。從用戶在瀏覽器中填寫表單的那一刻起,到數據經過 API 網關流轉至后端服務,每一個環節都可能成為輸入攻擊的突破…

Flask vs Django:微框架與一站式對決

Flask 簡介 1、簡介 Flask誕生于2010年,是Armin ronacher用Python語言基于Werkzeug工具箱編寫的輕量級Web開發框架,又稱之為微框架。 "微"的含義:Flask旨在保持核心簡潔,本身相當于內核,其他功能需通過擴展…

真實業務場景:mysql慢查詢優化(從17秒的查詢優化到700毫秒)

慢查詢業務場景:原先在我們系統中要統計一些人員的單位 部門信息的數據情況,比如總的男女人數,每個單位下的男女人數等等,然后原來的sql是這樣寫的 根據一個單位的id 然后對一張表做出多個子查詢進行查詢,這時候統計記錄 由于加載…

遠程影音訪問:通過 cpolar 內網穿透服務使用 LibreTV

文章目錄前言【視頻教程】1.關于LibreTV2.docker部署LibreTV3.簡單使用LibreTV4.安裝cpolar內網穿透5.配置ward公網地址6.配置固定公網地址總結LibreTV 與 cpolar 的協同應用,為用戶打造了一條通往高清觀影自由的便捷之路。通過這一方案,用戶不僅擺脫了商…

Apache ECharts 6 核心技術解密 – Vue3企業級可視化實戰指南

簡介 ECharts 是百度開源的一個使用 JavaScript 實現的開源可視化庫,它能夠生動、可交互地展示數據。在 Vue3 項目中集成 ECharts 可以讓你的項目更加直觀和動態地呈現數據信息。 核心優勢 特性SVG渲染器Canvas渲染器縮放保真度★★★★★★★☆☆☆動態交互性能…

考公VS考研,拼哪個性價比高?

即將到來下半年,將迎來考公和考研是兩個非常重要的考試,也是許多年輕人為之奮斗的目標。無論是獲得一份穩定的“鐵飯碗”,還是提升學歷學位獲得更高的競爭力,都是值得努力的方向。那么,考公vs考研,到底哪個…

python2操作neo4j

環境依賴 jdk、neo4j圖數據庫 操作一條數據完整demo import os,json,sys,io from py2neo import Graph,Nodetry:sys.stdout io.TextIOWrapper(sys.stdout.buffer, encodingutf-8)sys.stderr io.TextIOWrapper(sys.stderr.buffer, encodingutf-8) except Exception:passcla…

AI 編程實踐:用 Trae 快速開發 HTML 貪吃蛇游戲

1. 背景與目標 貪吃蛇是最適合入門的 2D 網頁小游戲之一:規則簡單、反饋清晰、可擴展空間大(穿墻模式、道具、多食物、排行榜……)。 demo地址:https://game.haiyong.site/snake-game.html 本項目的目標是: 純前端、…

FreeRTOS-C語言指針筆記

文章目錄一級指針指針基本概念指針使用示例代碼說明二、二級指針二級指針重點解析一級指針 C語言中的指針是一個非常重要的概念,它存儲了變量的內存地址。指針的使用可以使程序更加高效,尤其在處理數組、字符串和動態內存分配時。 指針基本概念 指針變…

界面布局智能建議生成:從功能需求到專業UI的AI加速之路

內容簡介: 傳統界面設計讓產品經理陷入"不懂設計、等設計師"的困境,效率低下還容易被挑刺。本文深度解析DeepSeek驅動的界面布局智能生成技術,通過DESIGN框架提示詞模板,讓產品經理在30分鐘內生成3種專業級界面方案,實現…