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

tanka發表於2018-08-07

心路歷程

以前編寫c/c++的時候,真心不知道啥是回撥地獄 , 為啥呢? 因為以前程式設計的時候 , 程式碼的編寫順序就是執行順序。 比如我去讀取一個檔案 (程式碼簡寫)

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

在這裡 , 程式碼執行到read的時候,會阻塞 , 直到檔案讀完,不管失敗還是成功都會有一個結果 , 這時候程式碼才會繼續執行.
現在寫js的時候 , 讀取一個檔案是這樣的:

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中,多個非同步呼叫的情景有很多.比如遍歷一個目錄

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

這是非同步程式設計的典型問題 , 巢狀過深 , 最難看的程式碼誕生了... 對於一個對程式碼有潔癖的人 , 這個真心不能忍!!

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

Promise

協程

eventEmitter(事件釋出訂閱模式)

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..
})
複製程式碼

這裡可以看出 , 呼叫非同步程式碼之後 , 已經獲取了返回的資料 。
為什麼執行了resolve('data'), 或者reject('err')後, then的回撥函式, 或者catch的回撥函式就知道 , '該到我執行的時候到了呢' , 說白了就是有人“通知”我了唄!
resolve , reject 本就是promise自己定義的方法 , 內部實現大概是這樣

當呼叫resolve('data')的時候 , 去通知.then裡繫結的回撥函式 , 通知你一下 , 你該執行了 , 這是引數 this.emit(‘resolve’ , 'data')

當呼叫reject('err')的時候 , 去通知.catch裡繫結的回撥函式 , 通知你一下 , 你該執行了 , 這是引數 this.emit('reject' , 'err')

在呼叫.then(callback)的時候 , callback , 你監聽下‘resolve’ , 有人通知(emit)你的時候 , 你就執行

在呼叫.catch(callback)的時候 , callback , 你監聽下‘reject’ , 有人通知(emit)你的時候 , 你就執行

簡單說 , 就是把回撥函式拿到了外面執行 , 讓程式碼看著'同步' 自己簡單實現了下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的時候都會遍歷佇列 , 把相應的事件拿出來執行。eventEmit的簡單實現

總結

這裡可以看出,不管是哪一種處理回撥地獄的方式 , 都是要處理回撥函式的, 只不過是真正呼叫的位置不同而已~ ,上面三種方式做的都是如何組織回撥函式鏈的執行位置 , 如何讓程式碼看著更好看 ~~

相關文章