【发布订阅模式解决回调地狱】定义发布订阅、解决回调地狱

已被阅读 961 次 | 文章分类:日常随笔 | 2022-03-29 00:20

如何定义发布订阅模式及能解决回调地狱问题

1 回调地狱

回调地狱已经是老生常谈的话题了;它出现在异步请求的需求中,即Ajax之类的请求

                                            
举例如果有20个ajax请求;每一个都依赖于前一个的请求结果(即后一个接口的请求参数需要从前一个的结果去拿),那么必须等前一个请求结果结束,然后在回调中,开始调用第二个;第三个请求在第二个的回调中调用,依次进行,将有20个嵌套回调,所以这叫回调地狱,比18次地狱更恐怖~
                                            
                                        

下面例子都可直接在浏览器直接运行;替换接口即可

如下例子:用浏览器原生的fetch请求接口,结果需要用固定的response.json()处理成js对象;

                                            
<!doctype HTML>
<html>
    <head>
        <meta charset="utf-8"></meta>
        <style>
            html,body{
                width:100%;
                height:100%;
                margin: 0;
            }
        </style>
    </head>
    <body>
        <script>
            // 连续的回调请求
            fetch('http://localhost:8088/api/getinfo1').then((response)=>{return response.json()}).then((res)=>{
        if(res.code==='0'){
            // 第一层回调
            console.log(res.data.server);      
            fetch('http://localhost:8088/api/getinfo2').then((response)=>{return response.json()}).then((res)=>{
                if(res.code==='0'){
                    // 第一层回调
                    console.log(res.data.server);    
                    fetch('http://localhost:8088/api/getinfo3').then((response)=>{return response.json()}).then((res)=>{
                        if(res.code==='0'){
                            // 第三层回调
                            console.log(res.data.server);      
                            // ....
 
                            // ...第100层回调
 
 
                            // ...地狱
                        }
                    })  
                }
            })
        }
    })
        </script>
    </body>
</html>
                                            
                                        

如果说永远只有10个,20个未尝不可;毕竟像一套流水线工程,方便管理;但是如果成百上千那简直是噩梦

如何解决回调地狱?

目前我们在项目中常用的方式是ES7的async 和await;使用他们可以解决嵌套回调问题,像用同步代码一样按顺序执行即可;如下

                                            
<!doctype HTML>
<html>
    <head>
        <meta charset="utf-8"></meta>
        <style>
            html,body{
                width:100%;
                height:100%;
                margin: 0;
            }
        </style>
    </head>
    <body>
    <script>
        init()
        async function init(){
            await geoInfo1();
            await geoInfo2();
            await geoInfo3();
 
        }
        function  geoInfo1(){
            return new Promise((resolve)=>{
                fetch('http://localhost:8088/api/getinfo1').then((response)=>{return response.json()}).then((res)=>{
                    if(res.code==='0'){
                        console.log(res.data.server);
                        resolve();
                    }
                })
            })
        }
        function  geoInfo2(){
            return new Promise((resolve)=>{
                fetch('http://localhost:8088/api/getinfo2').then((response)=>{return response.json()}).then((res)=>{
                    if(res.code==='0'){
                        console.log(res.data.server);
                        resolve();
                    }
                })
            })
        }
        function  geoInfo3(){
            return new Promise((resolve)=>{
                fetch('http://localhost:8088/api/getinfo3').then((response)=>{return response.json()}).then((res)=>{
                    if(res.code==='0'){
                        console.log(res.data.server);
                        resolve();
                    }
                })
            })
        }          
    </script>
    </body>
</html>
                                            
                                        

/net/upload/image/20220329/a2487cd4872947468ae12e87e137ae45.png

可以看到代码的输出结果按照同步主线程方式,按顺序执行

2 发布订阅模式

本文主要介绍如何用发布订阅模式解决回调地狱,在这之前,咱先实现一个最基本的发布订阅模式;

通俗讲:就是先定义函数存下来;然后根据名称把函数读出来执行,函数用一个对象管理。

订阅:将事件名和对应的函数添加到事件对象,属于定义函数;一个名称对应一个函数数组,里面存储多个函数 发布:从事件对象中获取到事件名对应的所有函数,遍历执行; 取消订阅:把之前添加的某个函数从事件对象中删除即可

代码如下:

                                            
class PubSub {
    constructor() {
        // 一个对象存放所有的消息订阅
        // 每个消息对应一个数组,数组结构如下
        // {
        //   "event1": [cb1, cb2]
        // }
        // this.events={
        //     "event1":[
        //         fn1,
        //         fn2
        //     ],
        //     "event2":[
        //         fn3,
        //         fn4
        //     ]
        // }
        this.events = {}
    }
    // 订阅
    subscribe(event, callback) {
        if(this.events[event]) {
        // 如果有人订阅过了,这个键已经存在,就往里面加就好了
        this.events[event].push(callback);
        } else {
        // 没人订阅过,就建一个数组,回调放进去
        this.events[event] = [callback]
        }
    }
    // 发布
    publish(event, ...args) {
        // 取出所有订阅者的回调执行
        const subscribedEvents = this.events[event];
 
        if(subscribedEvents && subscribedEvents.length) {
        subscribedEvents.forEach(callback => {
            callback.call(this, ...args);
        });
        }
    }
    // 取消订阅
    unsubscribe(event, callback) {
    // 删除某个订阅,保留其他订阅
    const subscribedEvents = this.events[event];
 
    if(subscribedEvents && subscribedEvents.length) {
    this.events[event] = this.events[event].filter(cb => cb !== callback)
    }
}
}
                                            
                                        

调用:

                                            
let pb=new PubSub();
// 调用订阅方法,添加一个event1消息,并给消息添加一个行为  (定义函数)
pb.subscribe('event1',function(){
    console.log('event1的第一个callback')
})
// 调用订阅方法,添加一个event1消息,并给消息添加一个行为  (定义函数)
pb.subscribe('event1',function(){
    console.log('event1的第二个callback')
})
pb.publish('event1')  // 发布(调用函数)
                                            
                                        

/net/upload/image/20220329/b72157c6f160461b858c0ddb3b08a3f6.png

3 发布订阅模式解决回调地狱

上面介绍了发布订阅模式的原理;先定义后调用;所以根据这个原理去解决;

(1)首先订阅全部消息

(2)在上一个请求的回调函数中,调用下一个请求的发布

所以代码如下

                                            
// 异步,info1的发布放到异步队列
fetch('http://localhost:8088/api/getinfo1').then((response)=>{return response.json()}).then((res)=>{
    if(res.code==='0'){
        console.log(res.data.server);       // 第一执行
        pb.publish('info1')       
    }
})
// 订阅info1是主线程;所以先执行订阅info1,但是回调函数是异步,info2的异步放到异步队列
pb.subscribe('info1',()=>{
    fetch('http://localhost:8088/api/getinfo2').then((response)=>{return response.json()}).then((res)=>{
        if(res.code==='0'){
            console.log(res.data.server);   // 第二执行
            pb.publish('info2')
        }
    })
})
// 订阅info2是主线程;所以先执行订阅info2,但是回调函数是异步,info3的异步放到异步队列
pb.subscribe('info2',()=>{
    fetch('http://localhost:8088/api/getinfo3').then((response)=>{return response.json()}).then((res)=>{
        if(res.code==='0'){ 
             console.log(res.data.server);   // 第三执行
            pb.publish('info3')
        }
    })
})
                                            
                                        

/net/upload/image/20220329/a2487cd4872947468ae12e87e137ae45.png

结果显示同步执行,所以实现了同步线程的执行效果;

如果对上面代码的执行流程不清楚,可以了解下同步主线程,宏任务和微任务的相关知识;移步异步操作中的宏任务(macrotasks)与微任务(microtasks)

QQ:3410192267 | 技术支持 微信:popstarqqsmall

Copyright ©2017 xiaobaigis.com . 版权所有 鲁ICP备17027716号