JavaScript中異步與回調的基本概念及回調地獄現象
JavaScript異步與回調
一、前言
在學習本文內容之前,我們必須要先瞭解異步的概念,首先要強調的是異步和並行有著本質的區別。
- 並行,一般指並行計算,是說同一時刻有多條指令同時被執行,這些指令可能執行於同一
CPU
的多核上,或者多個CPU
上,或者多個物理主機甚至多個網絡中。 - 同步,一般指按照預定的順序依次執行任務,隻有當上一個任務完成後,才開始執行下一個任務。
- 異步,與同步相對應,異步指的是讓
CPU
暫時擱置當前任務,先處理下一個任務,當收到上個任務的回調通知後,再返回上個任務繼續執行,整個過程無需第二個線程參與。
也許用圖片的方式解釋並行、同步和異步更為直觀,假設現在有A、B兩個任務需要處理,使用並行、同步和異步的處理方式會分別采用如下圖所示的執行方式:
二、異步函數
JavaScript
為我們提供瞭許多異步的函數,這些函數允許我們方便的執行異步任務,也就是說,我們現在開始執行一個任務(函數),但任務會在稍後完成,具體完成時間並不清楚。
例如,setTimeout
函數就是一個非常典型的異步函數,此外,fs.readFile
、fs.writeFile
同樣也是異步函數。
我們可以自己定義一個異步任務的案例,例如自定義一個文件復制函數copyFile(from,to)
:
const fs = require('fs') function copyFile(from, to) { fs.readFile(from, (err, data) => { if (err) { console.log(err.message) return } fs.writeFile(to, data, (err) => { if (err) { console.log(err.message) return } console.log('Copy finished') }) }) }
函數copyFile
首先從參數from
讀取文件數據,隨後將數據寫入參數to
指向的文件。
我們可以像這樣調用copyFile
:
copyFile('./from.txt','./to.txt')//復制文件
如果這個時候,copyFile(...)
後面還有其他代碼,那麼程序不會等待copyFile
執行結束,而是直接向下執行,文件復制任務何時結束,程序並不關心。
copyFile('./from.txt','./to.txt') //下面的代碼不會等待上面的代碼執行結束 ...
執行到這裡,好像一切還都是正常的,但是,如果我們在copyFile(...)
函數後,直接訪問文件./to.txt
中的內容會發生什麼呢?
這將不會讀到復制過來的內容,就行這樣:
copyFile('./from.txt','./to.txt') fs.readFile('./to.txt',(err,data)=>{ ... })
如果在執行程序之前,./to.txt
文件還沒有創建,將得到如下錯誤:
PS E:\Code\Node\demos\03-callback> node .\index.js
finished
Copy finished
PS E:\Code\Node\demos\03-callback> node .\index.js
錯誤:ENOENT: no such file or directory, open 'E:\Code\Node\demos\03-callback\to.txt'
Copy finished
即使./to.txt
存在,也無法讀取其中復制的內容。
造成這種現象的原因是:copyFile(...)
是異步執行的,程序執行到copyFile(...)
函數後,並不會等待其復制完畢,而是直接向下執行,從而導致出現文件./to.txt
不存在的錯誤,或者文件內容為空錯誤(如果提前創建文件)。
三、回調函數
異步函數的具體執行結束的時間是不能確定的,例如readFile(from,to)
函數的執行結束時間大概率取決於文件from
的大小。
那麼,問題在於我們如何才能準確的定位copyFile
執行結束,從而讀取to
文件中的內容呢?
這就需要使用回調函數,我們可以修改copyFile
函數如下:
function copyFile(from, to, callback) { fs.readFile(from, (err, data) => { if (err) { console.log(err.message) return } fs.writeFile(to, data, (err) => { if (err) { console.log(err.message) return } console.log('Copy finished') callback()//當復制操作完成後調用回調函數 }) }) }
這樣,我們如果需要在文件復制完成後,立即執行一些操作,就可以把這些操作寫入回調函數中:
function copyFile(from, to, callback) { fs.readFile(from, (err, data) => { if (err) { console.log(err.message) return } fs.writeFile(to, data, (err) => { if (err) { console.log(err.message) return } console.log('Copy finished') callback()//當復制操作完成後調用回調函數 }) }) } copyFile('./from.txt', './to.txt', function () { //傳入一個回調函數,讀取“to.txt”文件中的內容並輸出 fs.readFile('./to.txt', (err, data) => { if (err) { console.log(err.message) return } console.log(data.toString()) }) })
如果,你已經準備好瞭./from.txt
文件,那麼以上代碼就可以直接運行:
PS E:\Code\Node\demos\03-callback> node .\index.js
Copy finished
加入社區“仙宗”,和我一起修仙吧
社區地址:http://t.csdn.cn/EKf1h
這種編程方式被稱為“基於回調”的異步編程風格,異步執行的函數應當提供一個回調參數用於在任務結束後調用。
這種風格在JavaScript
編程中普遍存在,例如文件讀取函數fs.readFile
、fs.writeFile
都是異步函數。
四、回調的回調
回調函數可以準確的在異步工作完成後處理後繼事宜,如果我們需要依次執行多個異步操作,就需要嵌套回調函數。
案例場景:依次讀取文件A和文件B
代碼實現:
fs.readFile('./A.txt', (err, data) => { if (err) { console.log(err.message) return } console.log('讀取文件A:' + data.toString()) fs.readFile('./B.txt', (err, data) => { if (err) { console.log(err.message) return } console.log("讀取文件B:" + data.toString()) }) })
執行效果:
PS E:\Code\Node\demos\03-callback> node .\index.js
讀取文件A:仙宗無限好,隻是缺瞭佬讀取文件B:要想入仙宗,鏈接不能少
http://t.csdn.cn/H1faI
通過回調的方式,就可以在讀取文件A之後,緊接著讀取文件B。
如果我們還想在文件B之後,繼續讀取文件C呢?這就需要繼續嵌套回調:
fs.readFile('./A.txt', (err, data) => {//第一次回調 if (err) { console.log(err.message) return } console.log('讀取文件A:' + data.toString()) fs.readFile('./B.txt', (err, data) => {//第二次回調 if (err) { console.log(err.message) return } console.log("讀取文件B:" + data.toString()) fs.readFile('./C.txt',(err,data)=>{//第三次回調 ... }) }) })
也就是說,如果我們想要依次執行多個異步操作,需要多層嵌套回調,這在層數較少時是行之有效的,但是當嵌套次數過多時,會出現一些問題。
回調的約定
實際上,fs.readFile
中的回調函數的樣式並非個例,而是JavaScript
中的普遍約定。我們日後會自定義大量的回調函數,也需要遵守這種約定,形成良好的編碼習慣。
約定是:
callback
的第一個參數是為 error 而保留的。一旦出現 error,callback(err)
就會被調用。- 第二個以及後面的參數用於接收異步操作的成功結果。此時
callback(null, result1, result2,...)
就會被調用。
基於以上約定,一個回調函數擁有錯誤處理和結果接收兩個功能,例如fs.readFile('...',(err,data)=>{})
的回調函數就遵循瞭這種約定。
五、回調地獄
如果我們不深究的話,基於回調的異步方法處理似乎是相當完美的處理方式。問題在於,如果我們有一個接一個 的異步行為,那麼代碼就會變成這樣:
fs.readFile('./a.txt',(err,data)=>{ if(err){ console.log(err.message) return } //讀取結果操作 fs.readFile('./b.txt',(err,data)=>{ if(err){ console.log(err.message) return } //讀取結果操作 fs.readFile('./c.txt',(err,data)=>{ if(err){ console.log(err.message) return } //讀取結果操作 fs.readFile('./d.txt',(err,data)=>{ if(err){ console.log(err.message) return } ... }) }) }) })
以上代碼的執行內容是:
- 讀取文件a.txt,如果沒有發生錯誤的話;
- 讀取文件b.txt,如果沒有發生錯誤的話;
- 讀取文件c.txt,如果沒有發生錯誤的話;
- 讀取文件d.txt,…
隨著調用的增加,代碼嵌套層級越來越深,包含越來越多的條件語句,從而形成不斷向右縮進的混亂代碼,難以閱讀和維護。
我們稱這種不斷向右增長(向右縮進)的現象為“回調地獄”或者“末日金字塔”!
fs.readFile('a.txt',(err,data)=>{ fs.readFile('b.txt',(err,data)=>{ fs.readFile('c.txt',(err,data)=>{ fs.readFile('d.txt',(err,data)=>{ fs.readFile('e.txt',(err,data)=>{ fs.readFile('f.txt',(err,data)=>{ fs.readFile('g.txt',(err,data)=>{ fs.readFile('h.txt',(err,data)=>{ ... /* 通往地獄的大門 ===> */ }) }) }) }) }) }) }) })
雖然以上代碼看起來相當規整,但是這隻是用於舉例的理想場面,通常業務邏輯中會有大量的條件語句、數據處理操作等代碼,從而打亂當前美好的秩序,讓代碼變的難以維護。
幸運的是,JavaScript
為我們提供瞭多種解決途徑,Promise
就是其中的最優解。
(原諒我賣瞭一個關子,這篇文章太長瞭,下篇繼續講)
六、總結
本文主要介紹瞭異步和回調的基本概念,二者是JavaScript
的核心內容,需要所有熱愛JS
的小夥伴深入瞭解。
- 異步、並行、同步的基本概念;
- 使用回調函數處理異步任務;
- 回調函數的嵌套和約定;
- 回調地獄的基本概念;
到此這篇關於JavaScript中異步與回調的基本概念,以及回調地獄現象的文章就介紹到這瞭,更多相關js異步回調地獄內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- nodejs 中的讀取文件fs模塊示例詳解
- nodejs中的讀取文件fs與文件路徑path解析
- Node的文件系統你瞭解多少
- 總結Node.js中9種fs模塊文件操作方法(文件夾遞歸刪除知識)
- Node.js原理阻塞和EventEmitter及其繼承的運用實戰