淺淺的談一下回調地獄的問題

心路歷程

之前編寫c/c++的時候,真心不知道啥是回調地獄 , 爲啥呢? 由於之前編程的時候 , 代碼的編寫順序就是執行順序。 好比我去讀取一個文件 (代碼簡寫)node

std::ifstream t;
t.open("file.txt");
buffer = new char(length);
t.read(buffer , length);
複製代碼

在這裏 , 代碼執行到read的時候,會阻塞 , 直到文件讀完,無論失敗仍是成功都會有一個結果 , 這時候代碼纔會繼續執行.
如今寫js的時候 , 讀取一個文件是這樣的:c++

const fs = require('fs');
fs.readFile("file.txt",function(err , result){
    // 獲取結果 , 執行相關的業務代碼
})
code...
複製代碼

能夠看出,在js裏,當執行讀取文件的代碼後,沒有去等文件的執行結果,代碼直接向下執行 , 當讀取文件有結果的時候,在那個回調函數中執行相關的業務代碼。
對於一個一直編寫同步代碼,秉承着面向對象就是上帝的程序員小白,看到這段代碼心裏是崩潰的😢程序員

1:這裏代碼的編寫順序居然不是代碼的執行順序!
2:調用函數 , 還能傳一個函數爲參數(難道是函數指針,可是爲毛線要這樣作,這都是什麼鬼??!)
3:爲何在那個回調函數裏,能獲取到讀取文件結果的信息??
編程

什麼是函數式編程 ??

簡單說,函數式編程是一種「編程範式」,最直觀的感受就是,函數是一種對象類型,能夠做爲參數傳給別的函數,也能夠做爲結果return;
崩潰的我在學習js的路上停滯不前,內心一直鄙視這種js這種解釋性語言,搞什麼函數式編程,有毛線用??
直到有一天碰到了函數式編程的上帝 , 他語重心長的和我說:
咱們函數式編程天生是爲併發編程而生的啊,你看看函數沒有side effect,不共享變量,能夠安全地調度到任何一個CPU core上去運行,沒有煩人的加鎖問題,多好啊。
如今想一想本身真的很小白 , 只知道面向對象編程 , 面向過程編程 , 對函數式編程徹底無感。。設計模式

愛上了函數式編程的我又遇到了新的麻煩 (回調地獄)

在編寫js的過程當中, 事件獲得了普遍的應用,配合異步I/O,將事件點暴露給業務邏輯。
事件的編程方式具備輕量級,鬆耦合,只關注事物點等優點。
可是在多個異步任務的情景下,事件與事件之間如何獨立,如何協做是一個問題。
在node中,多個異步調用的情景有不少.好比遍歷一個目錄promise

fs.readdir(path.join(__dirname,'..'),function(err , files){
    files.forEach(function(filename , index){
        fs.readFle(filename , function(){
            ....
        })
    })
})
複製代碼

這是異步編程的典型問題 , 嵌套過深 , 最難看的代碼誕生了... 對於一個對代碼有潔癖的人 , 這個真心不能忍!!安全

目前我所知道回調地獄的解決方式

Promise併發

協程異步

eventEmitter(事件發佈訂閱模式)async

promise

promise , 感受就是把要執行的回調函數拿到了外面執行 , 使代碼看起來很"同步"~
看下promise如何實現的吧

let promise = new Promise(function(resolve , reject){
    // 執行異步代碼的調用 
    async(function(err , right){
        // 徹底是能夠根據返回的數據 , 直接執行相應的邏輯 , 不過爲了讓代碼看着"好看同步" , 決定把數據看成參數傳遞給外面,</br>
        去外面(then的回調函數裏 , 或者catch的回調函數裏)執行 
        // 根據返回的數據 , 來肯定該調用哪一個接口 
        if(right){
            resolve("data"); 
        }
        if(err){
            reject('err') 
        }
    })
})  
// 若是執行了resolve() , 就走到這裏 
.then(function(data){
    coding..
})
//若是執行了reject , 就走到了這裏 
.catch(function(err){
    coding..
})
複製代碼

這裏能夠看出 , 調用異步代碼以後 , 已經獲取了返回的數據 。以後把獲取的參數傳遞給then或者catch的回調函數,去執行相關的回調函數。

簡單說 , 就是把回調函數拿到了外面執行 , 讓代碼看着'同步'

promise具體細節 Promise源碼解析

協程

首先說下協程的定義 : 協程是一個無優先級的子程序調度組件 , 容許子程序在特定的地方掛起和恢復.
線程包含於進程,協程包含於線程。只要內存足夠,一個線程中能夠有任意多個協程,但某一時刻只能有一個協程在運行,多個協程分享該線程分配到的計算機資源。
協程要作的是啥 , 寫同步的代碼卻作着異步的事兒。

什麼時候掛起?? 什麼時候恢復呢??

掛起 : 在協程發起異步調用的時候掛起
恢復 : 其餘協程退出而且異步操做完成時。

Generator --> 協程在js中的實現

寫個🌰先 :

function* generator(x){
    var a = yield x+2;
    var b = yield a+3;
    var c = yield b+2;
    return;
}
複製代碼

最直觀的感受: 當調用generator(1)時,其實返回了一個鏈表.每個單元裏裝一些函數片斷 , 以yield爲界線 , 向上面的例子
(x+2;) --> (a+3) ---> (b+2) ---> (return;);
每次都經過next()方法來移動指針到下一個函數片斷,執行函數片斷(eval) , 返回結果.

var gen = generator(2);
gen.next(); // 當調用next(),會先走第一個代碼段 , 而後就不執行了 , 交出控制權 .直到啥時候再執行next(),會走下一個代碼段.
複製代碼

這裏能夠看出來 , 咱們徹底能夠在每一個代碼段都封裝一個異步任務 , 反正在異步任務執行的時候 , 我已經交出了控制權 , js主線程的代碼繼續往下走 , 啥也不耽誤 , 等到異步任務完成的時候, 通知我一下 , 我這邊看看等到其餘協程也都退出的時候 , 就調用next() , 繼續往下走.. 這樣下來 , 看看代碼多"同步" , 是否是~~~
繼續看下 , 當調用next("5")時 , 裏面是能夠傳入參數 , 並且傳入的參數是上一個yield的異步任務的返回結果 .
能夠說這個特性很是有用,就像上面說的,當異步任務完成的時候,就再調用next() , 走下面的代碼 , 可是無法獲取到上一個異步任務的結果的 , 因此這個特性就是作這個的 , next('異步任務的結果');

async/awit

說到async/awit , 最直觀的感受 , 不就是對gennerator的封裝 , 改個名麼??

let gen = async function(){      
    let f1 = await readFile("one");
    let f2 = await readFile2(123123);       
}
複製代碼

簡單說 , async/awit 就是對上面gennerator自動化流程的封裝 , 讓每個異步任務都是自動化的執行 , 當第一個異步任務readFile("one")執行完 , async內部本身執行next(),調用第二個任務readFile2(123123),以此類推...

這裏也許有人會困惑 , 爲何wait 後面返回的必須是promise ??

是這樣 , 上面說了當第一個異步完成時通知我一下 , 我在調用next() , 繼續往下執行 , 可是我何時完成的, 怎麼通知你??
promise就是作這件事的 , async內部會在promise.then(callback),回調函數裏調用 next()... (還有用Thunk的, 也是爲了作這個事的);

eventEmitter(事件發佈訂閱模式)

事件發佈訂閱模式普遍應用於異步編程的模式,是回調函數的事件化 , 能夠很好的解耦業務邏輯 , 也算是一種解決回調地獄的方式 , 不過和promise,async不一樣的是 , promise,async就是爲了解決回調地獄而設計出來的 , 而eventEmit是一種設計模式 , 正好能夠解決這個問題~~

// 訂閱 
emitter.on('event1',function(message){
    console.log(message);
})
// 發佈
emitter.emit('event1',data);
複製代碼

事件發佈訂閱模式能夠實現一個事件與多個回調函數的關聯,這些回調函數又稱爲事件監聽器.聽過emit發佈事件後 , 消息會當即傳遞給當前事件的全部監聽器執行.

事件發佈訂閱模式經常用來解耦業務邏輯,事件的發佈者無需關注訂閱的監聽器如何實現業務邏輯 , 數據經過消息的方式靈活傳遞。

fs.readFile("file.txt",function(err , result){
    // 獲取結果
    emmitter.emit('event1' , result)
})
emmiter.on('event' , function(result){
    // 在這裏執行相關業務代碼 
})
複製代碼

事件發佈訂閱模式咋實現的呢 ???

簡單說就是在上層維護一個私有的callback回調函數隊列 , 每次emmit的時候都會遍歷隊列 , 把相應的事件拿出來執行。

總結

這裏能夠看出,不論是哪種處理回調地獄的方式 , 都是要處理回調函數的, 只不過是真正調用的位置不一樣而已~ ,上面三種方式作的都是如何組織回調函數鏈的執行位置 , 如何讓代碼看着更好看 ~~

相關文章
相關標籤/搜索