浅浅的谈一下回调地狱的问题

3,210 阅读8分钟

心路历程

以前编写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..
})

这里可以看出 , 调用异步代码之后 , 已经获取了返回的数据 。之后把获取的参数传递给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的时候都会遍历队列 , 把相应的事件拿出来执行。

总结

这里可以看出,不管是哪一种处理回调地狱的方式 , 都是要处理回调函数的, 只不过是真正调用的位置不同而已~ ,上面三种方式做的都是如何组织回调函数链的执行位置 , 如何让代码看着更好看 ~~