分享

3.自定义事件

 行者花雕 2021-09-12

上一篇提到了模块化的思想,并且引入了按需加载模块的技术。然而对于实现细节却没有讲解,因为按需加载有点复杂,如果不引入观察者模式的设计思想,就会比较难实现。

使用观察者模式,我们实现dom事件类似的注册触发,这种方式可以很好的解耦,让模块间没有依赖。我们先讨论一下观察者模式是怎么回事,我们先引入几个场景:

  • 如果两个模块之间交互,通常我们用a变量和b变量引用,如果a更改了b的属性,可以通过代码b.property = someValue; 如果a要更改b的行为,那么可以通过b.doSomething(); 更改属性还好,因为b是个对象,更改属性不会报错。如果触发方法,b没有dosomething的方法,那么代码就会报错。这个场景只有在a和b完全知道对方的时候可以使用,比如上一个例子,名为static的Page对象更改显示页面的方法(currentPage),因为无法知道currentPage是什么,这种场景不适合直接使用;

  • 如果一个模块发生了变动,它要让另一个模块做一系列的事情,这种场景比上一个场景更加严苛,更加不适合直接使用;

  • 如果一个模块发生了变动,让另一个模块更改事情,同时另一个模块要求当前模块发生交互。可能双方需要交互一系列操作,这种场景很明显也是无法做到的。

我们联想一下dom事件,当dom被点击的时候,如果监听了点击事件,那就触发了点击的回调方法。如果没有注册,啥事情也不会发生。这时我们假设两个模块注册事件和触发事件,试一下来解决上述的场景问题:

  • 如果模块a需要模块b发生变化,那么在a中调用b的触发事件,b.dispatchEvent(eventname, data); data是传递的数据如果b监听了b.addEventListener(eventName, fn); 那么会调用fn的回调方法,因为fn是在b的作用域定义的,它可以访问在b模块下的私有变量,如果b没有监听eventName事件,那么什么都不会发生;

  • 如果b监听了若干个b.addEventListener(eventName, fn1); b.addEventListener(eventName, fn2); ...
    那么当a中调用b.dispatchEvent的时候,就会触发b的一系列方法;

  • 在监听的回调函数中,可以调用a.dispatchEvent方法。同理,a中的也可以调用b中的回调方法。

通过以上谈论,如果使用事件的方式是可以完美的解决之前的几个场景问题。

需求

开发一个观察者模式的对象,该对象有以下特点:

  1. 可以注册事件,将名称和回调方法们管理起来,有时候回调方法只触发一次也很常用;

  2. 可以触发事件,针对名称以及一个对象,让回调方法们依次触发;

  3. 可以移除某个回调方法,也可以针对名称移除该名称对应的所有回调方法;

  4. 可以销毁对象的destroy方法。

实现

代码如下

function EventDispatcher() {
    this.listeners = {};
}
EventDispatcher.prototype = {
    constructor: EventDispatcher,
    // 订阅自定义事件
    addEventListener: function (type, listener) {
        this._addEventListener(type, listener);
    },
    // 订阅某个类型的事件
    _addEventListener: function (type, listener, isOnce) {
        var listeners = this.listeners;
        if (typeof listeners[type] == "undefined") {
            listeners[type] = [];
        }
        if (isOnce) listener.once = true;
        listeners[type].push(listener);
    },
    // 注册一次绑定事件,触发后自动清除
    addOnce: function (type, listener) {
        this._addEventListener(type, listener, true);
    },
    // 移除某个事件类型的订阅事件
    removeEventListener: function (type, listener) {
        var listeners = this.listeners;
        var handlers = listeners[type];
        if (Array.isArray(handlers)) {
            var i = handlers.indexOf(listener);
            handlers.splice(i, 1);
        }
    },
    //  根据名称移除所有的回调函数
    clearListenerByType: function (type) {
        var handlers = this.listeners[type];
        if (Array.isArray(handlers)) handlers.length = 0;
    },
    // 触发某个事件类型
    dispatchEvent: function (event) {
        var listeners = this.listeners;
        if (!event.target) {
            event.target = this;
        }
        var handlers = listeners[event.type];
        // 如果有事件注册
        if (Array.isArray(handlers)) {
            var onceIndexs = [];
            for (var i = 0, len = handlers.length; i < len; i++) {
                handlers[i](event);
                if (handlers[i].once) {
                    onceIndexs.push(i);
                }
            }
            // 移除一次触发就销毁的回调方法
            if (onceIndexs.length > 0) {
                for (var i = onceIndexs.length - 1; i >= 0; i--) {
                    handlers.splice(onceIndexs[i], 1);
                }
            }
        }
    },
    // 销毁自定义事件
    destroy: function () {
        for (var str in this.listeners) {
            this.listeners[str].length = 0;
            delete this.listeners[str];
        }
    }
};

可以通过继承或者组合方式来使用这个对象

var a = {
    eventDispatcher: new EventDispatcher(),
    attachDiyEvent: function (type, fn) { 
        this.eventDispatcher.addEventListener(type, fn);
    },
    dispatchEvent: function (data) {
        this.eventDispatcher.dispatchEvent(data)
    }
}

如果每个模块都拥有EventDispatcher对象,就可以使用自定义事件进行跨模块交互了,上一篇说的App对象,Page对象,以及后面的Component和PopUp对象,都是间接继承了EventDispatcher对象。

实现按需加载

实现方法 App.require(list, bk),App.define(name, obj), 思路如下:

  1. require方法第一个参数中的name必须要在该name配置项的js中,有对应的App.define(name, obj),
    就是说 App.require(["strTool"], function (strTool) {});
    存在配置项 { strTool: { js: "/public/services/strTool.js "}}
    在/public/services/strTool.js中必须要有定义 App.define("strTool", {});
    App.require代表数组全部被require后,才会执行回调方法, App.define代表某一个模块已经被加载了;

  2. 如果数组中加载的模块都没有引用别的模块,等模块全部加载完,执行回调是没有问题的。如果引入的模块还引用了其它模块,比如App.require(["m1", "m2"], function (m1, m2) {}); m2中还引用了其它模块。 因为m2的定义是在回调函数中定义的,所以当m2的其它模块全部引入后,才会触发m2的引用完成,因此这个场景也是成立的。同理,只要不存在循环引用,或自引用, 就不会出现加载不出来的问题;

  3. 因为引用是异步的,存在一个文件同时被多个文件同时引用的情况,为了避免js被多次加载,需要做一下判断,可以用上面介绍的观察者模式。

实现App.require方法

App.require = function (list, bk) {
    if (typeof list === "function") return list.call(this);
    var len = list.length, mods = [], that = this;
    if (len == 0) bk.call(this);
    var func = function (obj, index) {
        mods[index] = obj;
        if (--len === 0) bk.apply(that, mods);
    }

    list.forEach(function (item, index) {
        // 获取对应的name的js文件,内由App.define(name, obj)方法
        var config = that._contains(item); 
        // 这个方法要定义自定义事件,在App.define触发回调,所有触发后才会全部加载完毕
        loadUnit._getUnit({ name: item, js: config.js }, function (obj) {
            func(obj, index);
        })
    })
};

实现App.define方法

App.define = function (name, bk) {
    if (typeof bk === "function") loadUnit.units[name] = bk();
    else loadUnit.units[name] = bk;
    // 告诉模块我已经加载完了。
    loadUnit.dispatchEvent({ type: name, detail: { component: loadUnit.units[name] }})
}

通过继承的方式,拥有自定义事件的所有方法,实现LoadUnit对象

function LoadUnit() {
    EventDispatcher.call(this);
    // 存已经加载的模块,如果模块已加载,就不再加载js,直接返回结果
    this.units = {};
    // 存放某个模块的加载状态,如果正在加载中,某个模块名为true,否则为false或undefined
    this.loadObj = {};
}
LoadUnit.prototype = create(EventDispatcher.prototype, {
    constructor: LoadUnit,
    _getUnit: function (config, next) {
        var that = this;
        var name = config.name, url = config.js;
        // 如果已经加载锅,直接返回加载完毕
        if (this.units[name]) next(this.units[name], config);
        else {
            // 监听触发一次的事件,再第二点里面触发,触发后才算加载完毕。
            this.addOnce(name, function (obj) {
                next(obj.detail.component, obj.detail.config || config);
            });
            // 如果不是加载状态,那就加载js,反之不加载
            if (!this.loadObj[name]) {
                this.loadObj[name] = true;
                var script = document.createElement("script"),
                    head = document.head;
                script.src = url;
                script.onload = function () {
                    that.loadObj[name] = false;
                }
                head.appendChild(script);
            }
        }
    }
};
// 创建一个loadUnit,来管理模块化
var loadUnit = new LoadUnit();

后面的pageLoadUnit, componentLoadUnit, popUpLoadUnit都是LoadUnit的实例对象。

简单案例

为了更好的理解将上一章节的内容代码拷贝下来,并作以下修改

  1. 修改static.js文件

     getDomObj: function () {
         this.attachDom(".page-container", "pageContainer")
             .attachDom("header", "header")
             .attachDiyEvent("changelayout", this.changeLayoutHandler)
             .attachDom("footer", "footer");
     },
     changeLayoutHandler: function (ev) {
         this.domList.header.textContent = ev.header;
         this.domList.footer.textContent = ev.footer;
     }
  2. 修改home.js, second.js文件

    与app.staticPage交互改为自定义事件交互

     app.staticPage.dispatchEvent("changelayout", {
         header: "次页",
         footer: "从首页跳转到次页"
     });
  3. 查看效果(主要针对移动端,可以通过手机端查看或者用浏览器模拟移动端)
    将代码放在可以访问的服务器或本地服务上,启动服务,通过浏览器访问,访问地址

结语

模块开发是当前web开发中多人合作开发的主流方式,如何去管理模块,以及有序的准确的加载是非常重要的。自定义事件是框架的核心之一,熟悉原理并熟练使用会让项目更容易维护。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多