用我的EventDispatcher 解决“竞态”问题

705 阅读6分钟

现在说一下何为“竞态“吧。

比如有个产品列表,点击产品列表上的产品图片,然后页面上出现一个弹窗查看产品详情。想详情是通过产品图片对应的id去后端接口获取到的。当你点击一个产图片,在数据没有返回之前,你又点击了另外一张图片,或者另外两张图片,你最终可能看到了一个自己不想要的产品详情。这个产品详情可能来自于你的第一次点击,或者你的第二次点击。而你期望看到的是最后一次点击的详情,你最后一次点击的产品详情可能会在你看到的最终画面之前一闪而过。

我们把这样的场景定义为“竞态”现象。

当然如果出现了这样的现象,欣仔认为交互设计肯定是不合格的。。。因为这不光是技术层面的问题,更多的是整个场景设计的问题。

本篇的目的也是从技术的角度去解决可能会出现的这类问题。

以上的场景出现的一个C/S 结构的SPA的应用中,所有操作都不会对浏览器产生刷新动作。

说到SPA,以前有个少年对按摩比较感兴趣,特别是泰式按摩,他有一个想成为世界顶级泰式按摩技师的梦想,但限制于条件,初中没毕业就到社会上闯荡,梦想成为夙愿久久不能触及。一次偶然的机会,他在上网的时候看到一则名为“SPA应用入门到大神,只要588”的广告。由于初中肄业,加上他对SPA的印象,思考片刻后就点开了报名。后来他学成归来,成为了一名VUE 的开发人员,月薪5000,比做服务员的时候轻松不少。后来据说菲律宾马尼拉那边看中了他的才干 ,高薪应聘了他过去开发系统软件,之后就再也没听过他的消息

说完少年的故事,回归这篇文章的吧。

关于竞态的问题,rxjs有着他自己的解决方案,也是大家通用的方案,大家可以百度或者谷歌一下,你会看到关于rxjs在此方面的一些特性。但欣仔写作有个原则:专挑你们没见过的。

PS:阅读本文需要确保你已经拥有了OOP的相关开发经验。

面对同一个问题,往往会有很多解决方案。通用的方案方便大家交流,非通用的可以给到大家一种别样的思路。

我之前在npmjs上发过一个自定义事件的包

shinevent

其中包含了两个个主要模块

ShinEvent

ShinEventDispatcher

ShinEvent负责封装事件的描述并可以传递消息,另外一个则负责发布事件。

回到我们所面对的竞态问题,该如何解决?

当第一次接口调用的时候,以及最后一次接口调用的时候。由于用户操作的时间间隔小于接口请求响应的时间,我们最终只需处理最后一次接口数据的返回而忽略之前接口返回的数据。问题是:之前的所有接口请求已经发起,我们不可能阻断接口的请求,所以方案是我们只能从请求回调里做一些工作:即忽略最后一次请求之前的所有请求的数据回调,只处理最后一次的。

shinevent该怎么用呢?

import {ShinEvent,ShinEventDispatcher} from 'shinEvent'
//define a new class which extends from ShinEventDispatcher
class testTarget extends ShinEventDispatcher{
    init (){
        this.dispatchEvent(new ShinEvent('init'));
    }
}
//create a new testTarget as target
let target=new testTarget();
//define call back function at object 'this'
this.handleInit=function(e){
    console.log('i am shinevent');
}
//add listener  and set the special function
target.addEventListener('init',this.handleInit,this);
target.init();
//you will get print of "i am shinevent"
//remove listener
target.removeEventListener('init',this.handleInit,this);

target.init();
//you will get nothing

以上代码执行逻辑为:

当我第一次执行

target.init()

的时候得到一个

i am shinevent

的输出,因为我在tatget的init方法里面发布了‘init’事件,并且在外部订阅了init事件。当我第二次执行

target.init()

的时候,我什么也没得到。因为我在第二次执行他的时候,用了一个

target.removeEventListener('init',this.handleInit,this);

移除了init的事件订阅。

所以大家大致可以知道,通过一个ShinEventDispatcher的实例,可以发布事件;并知道如何对特定的事件作出响应;以及移除事件。

应用到我们这篇文章里所面对的问题,思路可以是:每当我新建一个类似ajax的接口请求,就撤销之前的事件监听,然后新建一个新的事件监听,这就保证了目前的订阅处理,都是最新的。

STEP1

定义一个处理接口的类:AjaxExecuter

import {ShinEvent,ShinEventDispatcher} from 'shinEvent'
class AjaxExecuter extends ShinEventDispatcher{
    doAjax(url,data){
        //do something ajax
        //such as 
        $.ajax({
            url,
            data,
            success:(msg)=>{
                let event=new ShinEvent('getdata');
                //ShinEvent的data参数用于封装传递的消息。
                //此案例中event.data的数据格式如下
                //{msg:{}, 'pindex': pindex}
                event.data={msg,...data};
                this.dispatchEvent(event);
            }
        })
    }
}

STEP2

定义一个AjaxPool 管理类,包含以及管理以上AjaxExecuter类对象

class AjaxPool extends ShinEventDispatcher{

    getList(pindex){
        if(this.ae)
        {
            this.ae.removeEventListener('getdata',this.handleGetData,this);
            this.ae=null;
        }
        this.ae=new AjaxExecuter();
        this.ae.addEventListener('getdata',this.handleGetData,this);
        this.ae.doAjax('abc.com',{pindex})
        
    }
    handleGetData(e)
    {
        let {data}=e;
        let event=new ShinEvent('getlist');
        event.data=data;
        this.dispatchEvent(event)
    }
}

从上往下阅读:

但我有一个需要根据页码获取一串list的时候。通过AjaxPool对象的getList方法去,这个方法里面用了一个AjaxExecuter 对象请求数据,在请求之前,对当前存在的AjaxExecuter的getdata事件做了一个移除。每次请求都对老的AjaxExecuter对象进行了对应事件以及对应对象的移除,以确保每次请求结果的处理都是唯一的。

当我们在外围处理调用的时候如下:

const AP =new AjaxPool();
AP.addEventListener('getlist',this.handleGetList,this);
this.handleGetList=e=>{
    //e为一个shinevent对象,包含了事件类型,以及消息。
    //e.data为事件夹带的消息。为step1中shinevent对象包含的消息体。
    //
    console.log(e.data);
}
AP.getList(1);
AP.getList(2);
AP.getList(3);
AP.getList(4);
AP.getList(5);

最终我们得到的输出只会有一次,即最后一次

AP.getList(5);

这一次请求得到的数据;类似于:

{msg:{}, 'pindex': 5}

除非我们的接口请求的速度大于代码执行的速度。

介于此,我们做了两次封装,但是都是基于‘发布/订阅’机制得以实现。实际上RXJS的内部原理也是如此。发布/订阅 是现代框架视觉驱动以及事件驱动的内核,数据双向绑定或是单项绑定的实现原理中都能见到他的影子。

如果大家有兴趣可以通过

npm install --save shinevent//或者yarn add shinevent

安装测试,在npmjs搜索shinevent 得到相关的注释。其实是一个很简单的api,也希望对大家有所帮助,能够帮你买解决日后项目中可能遇到的问题。

欢迎大家订阅我的公众号,第一时间看到我的原创技术分享~