不知你是否是也有這樣的疑惑,咱們爲何須要回調函數這個概念呢?直接調用函數不就能夠了?回調函數到底有什麼做用?程序員到底該如何理解回調函數?javascript
這篇文章就來爲你解答這些問題,讀完這篇文章後你的武器庫將新增一件功能強大的利器。java
假設大家公司要開發下一代國民App「明日油條」,一款主打解決國民早餐問題的App,爲了加快開發進度,這款應用由A小組和B小組協同開發。
其中有一個核心模塊由A小組開發而後供B小組調用,這個核心模塊被封裝成了一個函數,這個函數就叫make_youtiao()。
若是make_youtiao()這個函數執行的很快並能夠當即返回,那麼B小組的同窗只須要:
-
-
-
-
-
-
make_youtiao()執行完後,控制轉回到調用函數中
若是世界上全部的函數都像make_youtiao()這麼簡單,那麼程序員大機率就要失業了,還好程序的世界是複雜的,這樣程序員纔有了存在的價值。
現實中make_youtiao()這個函數須要處理的數據很是龐大,假設有10000個,那麼make_youtiao(10000)不會馬上返回,而是可能須要10分鐘才執行完成並返回。
可能有的同窗會問,和剛纔同樣直接調用不能夠嗎,這樣多簡單。
是的,這樣作沒有問題,但就像愛因斯坦說的那樣「一切都應該儘量簡單,可是不能過於簡單」。
顯然直接調用的話,那麼調用線程會被阻塞暫停,在等待10分鐘後才能繼續運行。在這10分鐘內該線程不會被操做系統分配CPU,也就是說該線程得不到任何推動。
沒有一個程序員想死盯着屏幕10分鐘後才能獲得結果。
若是你是老闆的話你會什麼都不幹一直盯着員工寫代碼嗎?所以一種更好的作法是程序員在代碼的時候老闆該幹啥幹啥,程序員寫完後天然會通知老闆,這樣老闆和程序員都不須要相互等待,這種模式被稱爲異步。
回到咱們的主題,這裏一種更好的方式是調用make_youtiao()這個函數後再也不等待這個函數執行完成,而是直接返回繼續後續流程,這樣A小組的程序就能夠和make_youtiao()這個函數同時進行了,就像這樣:
在這種狀況下,回調(callback)就必須出場了。
有的同窗可能尚未明白爲何在這種狀況下須要回調,彆着急,咱們慢慢講。
make_youtiao(10000);sell();
能夠看到這是最簡單的寫法,意思很簡單,製做好油條後賣出去。
咱們已經知道了因爲make_youtiao(10000)這個函數10分鐘才能返回,你不想一直死盯着屏幕10分鐘等待結果,那麼一種更好的方法是讓make_youtiao()這個函數知道製做完油條後該幹什麼,即,更好的調用make_youtiao的方式是這樣的:
「製做10000個油條,
炸好後賣出去
」,所以調用make_youtiao就變出這樣了:
make_youtiao(10000, sell);
看到了吧,如今make_youtiao這個函數多了一個參數,除了指定製做油條的數量外
還能夠指定製做好後該幹什麼
,第二個被make_youtiao這個函數調用的函數就叫回調,callback。
如今你應該看出來了吧,雖然sell函數是你定義的,可是這個函數倒是被其它模塊調用執行的,就像這樣:
make_youtiao這個函數是怎麼實現的呢,很簡單:
void make_youtiao(int num, func call_back) { call_back(); }
這樣你就不用死盯着屏幕了,由於你把make_youtiao這個函數執行完後該作的任務交代給make_youtiao這個函數了,該函數製做完油條後知道該幹些什麼,這樣就解放了你的程序。
有的同窗可能仍是有疑問,爲何編寫make_youtiao這個小組不直接定義sell函數而後調用呢?
不要忘了明日油條這個App是由A小組和B小組同時開發的,A小組在編寫make_youtiao時怎麼知道B小組要怎麼用這個模塊,假設A小組真的本身定義sell函數就會這樣寫:
void make_youtiao(int num) { real_make_youtiao(num); sell(); }
同時A小組設計的模塊很是好用,這時C小組也想用這個模塊,然而C小組的需求是製做完油條後放到倉庫而不是否是直接賣掉,要知足這一需求那麼A小組該怎麼寫呢?
void make_youtiao(int num) { real_make_youtiao(num); if (Team_B) { sell(); } else if (Team_D) { store(); }}
故事還沒完,假設這時D小組又想使用呢,難道還要接着添加if else嗎?
這樣的話A小組的同窗只須要維護make_youtiao這個函數就能作到工做量飽滿了,顯然這是一種很是糟糕的設計。
因此你會看到,製做完油條後接下來該作什麼不是實現make_youtiao的A小組該關心的事情,很明顯只有調用make_youtiao這個函數的使用方纔知道。
所以make_youtiao的A小組徹底能夠經過回調函數將接下來該幹什麼交給調用方實現,A小組的同窗只須要針對回調函數這一抽象概念進行編程就行了,這樣調用方在製做完油條後無論是賣掉、放到庫存仍是本身吃掉等等想作什麼均可以,A小組的make_youtiao函數根本不用作任何改動,由於A小組是針對回調函數這一抽象概念來編程的。
以上就是回調函數的做用,固然這也是針對抽象而不是具體實現進行編程這一思想的威力所在。面向對象中的多態本質上就是讓你用來針對抽象而不是針對實現來編程的。
在上面的示例中,雖然咱們使用了回調這一律念,也就是調用方實現回調函數而後再將該函數當作參數傳遞給其它模塊調用。
可是,這裏依然有一個問題,那就是make_youtiao函數的調用方式依然是同步的,關於同步異步請參考《從小白到高手,你須要理解同步與異步》,也就是說調用方是這樣實現的:
make_youtiao(10000, sell);
咱們能夠看到,調用方必須等待make_youtiao函數返回後才能夠繼續後續流程,咱們再來看下make_youtiao函數的實現:
void make_youtiao(int num, func call_back) { real_make_youtiao(num); call_back(); }
看到了吧,因爲咱們要製做10000個油條,make_youtiao函數執行完須要10分鐘,也就是說即使咱們使用了回調,調用方徹底不須要關心製做完油條後的後續流程,可是調用方依然會被阻塞10分鐘,這就是同步調用的問題所在。
若是你真的理解了上一節的話應該能想到一種更好的方法了。
反正製做完油條後的後續流程並非調用方該關心的,也就是說調用方並不關心make_youtiao這一函數的返回值,那麼一種更好的方式是:把製做油條的這一任務放到另外一個線程(進程)、甚至另外一臺機器上。
若是用線程實現的話,那麼make_youtiao就是這樣實現了:
void make_youtiao(int num, func call_back) { create_thread(real_make_youtiao, num, call_back);}
看到了吧,這時當咱們調用make_youtiao時就會
馬上返回
,即便油條尚未真正開始製做,而調用方也徹底無需等待制做油條的過程,能夠馬上執行後流程:
make_youtiao(10000, sell);
這時調用方的後續流程能夠和製做油條
同時
進行,這就是函數的
異步調用
,固然這也是異步的高效之處。
res = request();handle(res);
這就是函數的同步調用,只有request()函數返回拿到結果後,才能調用handle函數進行處理,request函數返回前咱們必須
等待
,這就是同步調用,其控制流是這樣的:
可是若是咱們想更加高效的話,那麼就須要異步調用了,咱們不去直接調用handle函數,而是做爲參數傳遞給request:
咱們根本就不關心request何時真正的獲取的結果,這是request該關心的事情,咱們只須要把獲取到結果後該怎麼處理告訴request就能夠了,所以request函數能夠馬上返回,真的獲取結果的處理多是在另外一個線程、進程、甚至另外一臺機器上完成。
![](http://static.javashuo.com/static/loading.gif)
從編程思惟上看,異步調用和同步有很大的差異,若是咱們把處理流程當作一個任務來的話,那麼同步下整個任務都是咱們來實現的,可是異步狀況下任務的處理流程被分爲了兩部分:
-
第一部分是咱們來處理的,也就是調用request以前的部分
-
第二部分不是咱們處理的,而是在其它線程、進程、甚至另外一個機器上處理的。
咱們能夠看到因爲任務被分紅了兩部分,第二部分的調用不在咱們的掌控範圍內,同時只有調用方纔知道該作什麼,所以在這種狀況下回調函數就是一種必要的機制了。
也就是說回調函數的本質就是「只有咱們才知道作些什麼,可是咱們並不清楚何時去作這些,只有其它模塊才知道,所以咱們必須把咱們知道的封裝成回調函數告訴其它模塊」。
如今你應該能看出異步回調這種編程思惟模式和同步的差別了吧。
在計算機科學中,回調函數是指一段以參數的形式傳遞給其它代碼的可執行代碼。
注意,回調函數是一種軟件設計上的概念,和某個編程語言沒有關係,幾乎全部的編程語言都能實現回調函數。
對於通常的函數來講,咱們本身編寫的函數會在本身的程序內部調用,也就是說函數的編寫方是咱們本身,調用方也是咱們本身。
但回調函數不是這樣的,雖然函數編寫方是咱們本身,可是函數調用方不是咱們,而是咱們引用的其它模塊,也就是第三方庫,咱們調用第三方庫中的函數,並把回調函數傳遞給第三方庫,第三方庫中的函數調用咱們編寫的回調函數,如圖所示:
而之因此須要給第三方庫指定回調函數,是由於第三方庫的編寫者並不清楚在某些特定節點,好比咱們舉的例子油條製做完成、接收到網絡數據、文件讀取完成等以後該作什麼,這些只有庫的使用方纔知道,所以第三方庫的編寫者沒法針對具體的實現來寫代碼,而只能對外提供一個回調函數,庫的使用方來實現該函數,第三方庫在特定的節點調用該回調函數就能夠了。
另外一點值得注意的是,從圖中咱們能夠看出回調函數和咱們的主程序位於同一層中,咱們只負責編寫該回調函數,但並非咱們來調用的。
最後值得注意的一點就是回調函數被調用的時間節點,回調函數只在某些特定的節點被調用,就像上面說的油條製做完成、接收到網絡數據、文件讀取完成等,這些都是事件,也就是event,本質上咱們編寫的回調函數就是用來處理event的,所以從這個角度看回調函數不過就是event handler,所以回調函數自然適用於事件驅動編程event-driven,咱們將會在後續文章中再次回到這一主題。
咱們已經知道有兩種類型的回調,這兩種類型的回調區別在於回調函數被調用的時機。
這種回調就是一般所說的同步回調synchronous callbacks、也有的將其稱爲阻塞式回調blocking callbacks,或者什麼修飾都沒有,就是回調,callback,這是咱們最爲熟悉的回調方式。
當咱們調用某個函數A並以參數的形式傳入回調函數後,在A返回以前回調函數會被執行,也就是說咱們的主程序會等待回調函數執行完成,這就是所謂的同步回調。
不一樣於同步回調, 當咱們調用某個函數A並以參數的形式傳入回調函數後,A函數會馬上返回,也就是說函數A並不會阻塞咱們的主程序,一段時間後回調函數開始被執行,此時咱們的主程序可能在忙其它任務,回調函數的執行和咱們主程序的運行同時進行。
既然咱們的主程序和回調函數的執行能夠同時發生,所以通常狀況下,主程序和回調函數的執行位於不一樣的線程或者進程中。
這就是所謂的異步回調,asynchronous callbacks,也有的資料將其稱爲deferred callbacks ,名字很形象,延遲迴調。
從上面這兩張圖中咱們也能夠看到,異步回調要比同步回調更能充分的利用機器資源,緣由就在於在同步模式下主程序會「偷懶」,由於調用其它函數被阻塞而暫停運行,可是異步調用不存在這個問題,主程序會一直運行下去。
所以,異步回調更常見於I/O操做,自然適用於Web服務這種高併發場景。
讓咱們用簡單的幾句話來總結一下回調下與常規編程思惟模式的不一樣。
假設咱們想處理某項任務,這項任務須要依賴某項服務S,咱們能夠將任務的處理分爲兩部分,調用服務S前的部分PA,和調用服務S後的部分PB。
在常規模式下,PA和PB都是服務調用方來執行的,也就是咱們本身來執行PA部分,等待服務S返回後再執行PB部分。
在這種狀況下,咱們本身來執行PA部分,而後告訴服務S:「等你完成服務後執行PB部分」。
所以咱們能夠看到,如今一項任務是由不一樣的模塊來協做完成的。
在同步模式下,服務調用方會因服務執行而被阻塞暫停執行,這會致使整個線程被阻塞,所以這種編程方式自然不適用於高並發動輒幾萬幾十萬的併發鏈接場景,
針對高併發這一場景,異步實際上是更加高效的,緣由很簡單,你不須要在原地等待,所以從而更好的利用機器資源,而回調函數又是異步下不可或缺的一種機制。
有的同窗可能認爲有了異步回調這種機制應付起一切高併發場景就能夠高枕無憂了。
實際上在計算機科學中尚未任何一種能夠橫掃一切包治百病的技術,如今沒有,在可預見的未來也不會有,一切都是妥協的結果。
實際上咱們已經看到了,異步回調這種機制和程序員最熟悉的同步模式不同,在可理解性上比不過同步,而若是業務邏輯相對複雜,好比咱們處理某項任務時不止須要調用一項服務,而是幾項甚至十幾項,若是這些服務調用都採用異步回調的方式來處理的話,那麼頗有可能咱們就陷入回調地獄中。
舉個例子,假設處理某項任務咱們須要調用四個服務,每個服務都須要依賴上一個服務的結果,若是用同步方式來實現的話多是這樣的:
a = GetServiceA();b = GetServiceB(a);c = GetServiceC(b);d = GetServiceD(c);
咱們知道異步回調的方式會更加高效,那麼使用異步回調的方式來寫將會是什麼樣的呢?
GetServiceA(function(a){ GetServiceB(a, function(b){ GetServiceC(b, function(c){ GetServiceD(c, function(d) { .... }); }); });});
我想不須要再強調什麼了吧,你以爲這兩種寫法哪一個更容易理解,代碼更容易維護呢?
博主有幸曾經維護過這種類型的代碼,不得不說每次增長新功能的時候巴不得本身化爲兩個分身,一個不得不去重讀一邊代碼;另外一個在一旁罵本身爲何當初選擇維護這個項目。
異步回調代碼稍不留意就會跌到回調陷阱中,那麼有沒有一種更好的辦法既能結合異步回調的高效又能結合同步編碼的簡單易讀呢?
幸運的是,答案是確定的,咱們會在後續文章中詳細講解這一技術。
在這篇文章中,咱們從一個實際的例子出發詳細講解了回調函數這種機制的前因後果,這是應對高併發、高性能場景的一種極其重要的編碼機制,異步加回調能夠充分利用機器資源,實際上異步回調最本質上就是事件驅動編程,這是咱們接下來要重點講解的內容。
本文分享自微信公衆號 - 碼農的荒島求生(escape-it)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。