Using Javascript to implement a custom event mechanism

With the development of web technology, the use of JavaScript custom objects is becoming more and more frequent, so that the objects created by oneself also have an event mechanism, and external communication through events can greatly improve development efficiency. The event mechanism provides great convenience for our web development, enabling us to specify what operation to do and what code to execute at any time.

foreword

The event mechanism provides great convenience for our web development, enabling us to specify what operation to do and what code to execute at any time.

For example, the click event is triggered when the user clicks; the keydown and keyup events are triggered when the keyboard is pressed or the keyboard is popped up; and in the upload control, the event before the file is added, and the event after the upload is completed.

Since there will be corresponding event triggers at the right time, we can specify corresponding processing functions for these events, and insert various personalized operations and processing into the original process, making the whole process richer.

Events such as click, blur, focus, etc. are native events directly provided by the original dom, and the various events used by some other controls we use are not available in the native dom. For example, upload controls usually have upload start and Complete events, how are these events implemented?

I also want to add a similar event mechanism to my own developed controls. How to implement it? Let's find out.

What the event should do

Before implementing, let's first analyze the basic functions that the event mechanism should have.

Simply put, an event must provide the following functions:

  • bind event
  • trigger event
  • unbind event

Preparation

Let's observe a feature of the event, the event must belong to an object. For example: focus and blur events are for dom elements that can get focus, input events are for input boxes, uploading starts and uploading is successful.

In other words, an event does not exist independently, it needs a carrier. So how do we let the event have a carrier? A simple implementation solution is to use the event as a base class and inherit the event class where the event is needed.

We name the binding event, triggering event, and unbinding event as: on, fire, and off, then we can simply write this event class:

function CustomEvent() {
    this._events = {}
}

CustomEvent.prototype = {
    constructor: CustomEvent,
    
    // 绑定事件
    on: function () {
    },
    
    // 触发事件
    fire: function () {
    },
    
    // 取消绑定事件
    off: function () {
    }
}

event binding

First of all, to realize the binding of the event, the event binding must specify the type of event and the processing function of the event.

So what else is needed? We are a custom event, and we don't need to specify whether it is triggered in the bubbling phase or in the capture phase like native events, and we don't need to additionally specify which elements to trigger like in jQuery.

In the event function, this is generally the current instance, which may not be applicable in some cases. We need to re-specify the context in which the event processing function runs.

Therefore, when determining the event binding, the three parameters are: event type, event processing function, and event processing function execution context.

So what does event binding do? In fact, it is very simple. Event binding only needs to record the corresponding event name and event processing function.

The implementation is as follows:

{
    /**
  * 绑定事件
  * 
  * @param {String} type 事件类型
  * @param {Function} fn 事件处理函数
  * @param {Object} scope 要为事件处理函数绑定的执行上下文
  * @returns 当前实例对象
  */
    on: function (type, fn, scope) {
        if (type + '' !== type) {
            console && console.error && console.error('the first argument type is requird as string')
            return this
        }
        
        if (typeof fn != 'function') {
            console && console.error && console.error('the second argument fn is requird as function')
            return this
        }
        
        type = type.toLowerCase()

        if (!this._events[type]) {
            this._events[type] = []
        }
        
        this._events[type].push(scope ? [fn, scope] : [fn])
        
        return this
    }
}

Since an event can be bound multiple times and executed sequentially during execution, the processing functions of all event types are stored using arrays.

event trigger

The basic function of event triggering is to execute the event bound by the user, so it is only used to check whether there is a specified execution function when the event is triggered, and if so, just call it.

In addition, event triggering is actually the process of executing the processing function specified by the user, and many personalized operations can be performed in the event processing function specified by the user, so just executing this function is not enough. Necessary information must also be provided for the current function, such as the currently clicked element in the click event, the key code of the current key in the keyboard event, and the current file information in the upload start and upload completion.

Therefore, when an event is triggered, the actual parameter of the event processing function must contain the basic information of the current event.

In addition, through the user's operation in the event processing function, the adjusted information may be required. For example, in the keydwon event, the user can prohibit the entry of this key. Before the file is uploaded, the user cancels the upload of the file or modifies some file information. Therefore, the event trigger function should return the event object modified by the user.

The implementation is as follows:

{
    /**
  * 触发事件
  * 
  * @param {String} type 触发事件的名称
  * @param {Object} data 要额外传递的数据,事件处理函数参数如下
  * event = {
   // 事件类型
   type: type,
   // 绑定的源,始终为当前实例对象
   origin: this,
   // 事件处理函数中的执行上下文 为 this 或用户指定的上下文对象
   scope :this/scope
   // 其他数据 为fire时传递的数据
  }
  * @returns 事件对象
  */
    fire: function (type, data) {
        type = type.toLowerCase()
        var eventArr = this._events[type]
        var fn,
            // event = {
            //  // 事件类型
            //  type: type,
            //  // 绑定的源
            //  origin: this,
            //  // scope 为 this 或用户指定的上下文,
            //  // 其他数据 
            //  data: data,
            //  // 是否取消
            //  cancel: false
            // };
            // 上面对自定义参数的处理不便于使用 将相关属性直接合并到事件参数中
            event = $.extend({
                // 事件类型
                type: type,
                // 绑定的源
                origin: this,
                // scope 为 this 或用户指定的上下文,
                // 其他数据 
                // data: data,
                // 是否取消
                cancel: false
            }, data)
        
        if (!eventArr) {
            return event
        }
        
        for (var i = 0, l = eventArr.length; i < l; ++i) {
            fn = eventArr[i][0]
            event.scope = eventArr[i][1] || this
            fn.call(event.scope, event)
        }
        
        return event
    }
}

The actual parameters given to the event handler function in the above implementation must contain the following information:

  • type : the type of event currently triggered
  • origin : the object the current event is bound to
  • scope : the execution context of the event handler function

In addition, different information can be added to the event object when different events are triggered.

About  Object.assign(target, ...sources) is a method in ES6 that copies the values ​​of all enumerable properties from one or more source objects to the target object and returns the target object, similar to the well-known method  $.extend(target,..sources) .

event canceled

What needs to be done in event cancellation is to remove the bound event handler.

The implementation is as follows:

{
    /**
  * 取消绑定一个事件
  * 
  * @param {String} type 取消绑定的事件名称
  * @param {Function} fn 要取消绑定的事件处理函数,不指定则移除当前事件类型下的全部处理函数
  * @returns 当前实例对象
  */
    off: function (type, fn) {
        type = type.toLowerCase()
        var eventArr = this._events[type]
        
        if (!eventArr || !eventArr.length) return this
        
        if (!fn) {
            this._events[type] = eventArr = []
        } else {
            for (var i = 0; i < eventArr.length; ++i) {
                if (fn === eventArr[i][0]) {
                    eventArr.splice(i, 1)
                    // 1、找到后不能立即 break 可能存在一个事件一个函数绑定多次的情况
                    // 删除后数组改变,下一个仍然需要遍历处理!
                    --i
                }
            }
        }
        
        return this
    }
}

Here, a similar native event unbinding is implemented. If an event handler is specified, the specified event handler will be removed. If the event handler is omitted, all event handlers under the current event type will be removed.

Events that fire only once

There is a one method in jQuery, and the event it binds will only be executed once. This method is very useful in some specific situations, and does not require the user to manually unbind the event.

The implementation here is also very simple, just unbind when this event is triggered.

The implementation is as follows:

{
    /**
  * 绑定一个只执行一次的事件
  * 
  * @param {String} type 事件类型
  * @param {Function} fn 事件处理函数
  * @param {Object} scope 要为事件处理函数绑定的执行上下文
  * @returns 当前实例对象
  */
    one: function (type, fn, scope) {
        var that = this
        
        function nfn() {
            // 执行时 先取消绑定
            that.off(type, nfn)
            // 再执行函数
            fn.apply(scope || that, arguments)
        }
        
        this.on(type, nfn, scope)
        
        return this
    }
}

The principle is not to directly bind the function specified by the user, but to generate a new function and bind it. When this function is executed, it will first cancel the binding, and then execute the processing function specified by the user.

Basic prototype

At this point, a complete set of event mechanism has been completed, and the complete code is as follows:

function CustomEvent() {
    this._events = {}
}

CustomEvent.prototype = {
    constructor: CustomEvent,
    
    /**
  	* 绑定事件
  	* 
  	* @param {String} type 事件类型
	* @param {Function} fn 事件处理函数
	* @param {Object} scope 要为事件处理函数绑定的执行上下文
	* @returns 当前实例对象
	*/
    on: function (type, fn, scope) {
        if (type + '' !== type) {
            console && console.error && console.error('the first argument type is requird as string')
            return this
        }
        
        if (typeof fn != 'function') {
            console && console.error && console.error('the second argument fn is requird as function')
            return this
        }
        
        type = type.toLowerCase()

        if (!this._events[type]) {
            this._events[type] = []
        }
        
        this._events[type].push(scope ? [fn, scope] : [fn])

        return this
    },
    
    /**
  	* 触发事件
 	* 
 	* @param {String} type 触发事件的名称
    * @param {Anything} data 要额外传递的数据,事件处理函数参数如下
 	* event = {
        // 事件类型
        type: type,
        // 绑定的源,始终为当前实例对象
        origin: this,
        // 事件处理函数中的执行上下文 为 this 或用户指定的上下文对象
        scope :this/scope
        // 其他数据 为fire时传递的数据
    }
    * @returns 事件对象
    */
    fire: function (type, data) {
        type = type.toLowerCase()
        var eventArr = this._events[type]
        var fn, scope,
            event = Object.assign({
                // 事件类型
                type: type,
                // 绑定的源
                origin: this,
                // scope 为 this 或用户指定的上下文,
                // 是否取消
                cancel: false
            }, data)

        if (!eventArr) return event

        for (var i = 0, l = eventArr.length; i < l; ++i) {
            fn = eventArr[i][0]
            scope = eventArr[i][1]
            
            if (scope) {
                event.scope = scope
                fn.call(scope, event)
            } else {
                event.scope = this
                fn(event)
            }
        }
        
        return event
    },
    /**
    * 取消绑定一个事件
    * 
    * @param {String} type 取消绑定的事件名称
    * @param {Function} fn 要取消绑定的事件处理函数,不指定则移除当前事件类型下的全部处理函数
    * @returns 当前实例对象
    */
    off: function (type, fn) {
        type = type.toLowerCase()
        var eventArr = this._events[type]
        
        if (!eventArr || !eventArr.length) return this
        
        if (!fn) {
            this._events[type] = eventArr = []
        } else {
            for (var i = 0; i < eventArr.length; ++i) {
                if (fn === eventArr[i][0]) {
                    eventArr.splice(i, 1)
                    // 1、找到后不能立即 break 可能存在一个事件一个函数绑定多次的情况
                    // 删除后数组改变,下一个仍然需要遍历处理!
                    --i
                }
            }
        }
        return this
    },
    /**
    * 绑定一个只执行一次的事件
    * 
    * @param {String} type 事件类型
    * @param {Function} fn 事件处理函数
    * @param {Object} scope 要为事件处理函数绑定的执行上下文
    * @returns 当前实例对象
    */
    one: function (type, fn, scope) {
        var that = this

        function nfn() {
            // 执行时 先取消绑定
            that.off(type, nfn)
            // 再执行函数
            fn.apply(scope || that, arguments)
        }
        
        this.on(type, nfn, scope)
        
        return this
    }
}

Use it in your own control

A set of event mechanism has been implemented above, how can we use it in our own events.

For example, I wrote a calendar control and need to use the event mechanism.

function Calendar() {
    // 加入事件机制的存储的对象
    this._event = {}
    // 日历的其他实现
}

Calendar.prototype = {
    constructor:Calendar,
    on:function () {},
    off:function () {},
    fire:function () {},
    one:function () {},
    // 日历的其他实现 。。。
}

The above pseudo-code is used as an illustration, and it is only necessary to let the control inherit methods such as on, off, fire, and one. However, it must be ensured that the event storage object _events must be directly loaded on the instance. This needs to be paid attention to when inheriting. There are too many inheritance schemes in JavaScript.

The event mechanism is added to the calendar control Calendar above, and then it can be used in the Calendar.

For example, when developing a calendar, we trigger the cellRender event when the cells of the calendar are rendered.

// 每天渲染时发生 还未插入页面
var renderEvent = this.fire('cellRender', {
    // 当天的完整日期
    date: date.format('YYYY-MM-DD'),
    // 当天的iso星期
    isoWeekday: day,
    // 日历dom
    el: this.el,
    // 当前单元格
    tdEl: td,
    // 日期文本
    dateText: text.innerText,
    // 日期class
    dateCls: text.className,
    // 需要注入的额外的html
    extraHtml: '',
    isHeader: false
})

In the event, we provide the currently rendered date, text class and other information to the user, so that the user can bind this event and perform their own personalized processing in this event.

For example, when rendering, if it is a weekend, insert a "false" logo and make the date display in red.

var calendar = new Calendar()

calendar.on('cellRender', function (e) {
    if(e.isoWeekday > 5 ) {
        e.extraHtml = '<span>假</span>'
        e.dateCls += ' red'
    } 
})

Using the event mechanism in the control can simplify the development, make the process easy to control, and provide very rich personalized operations for actual use. Let's use it quickly.

Summarize

The above is the whole content of this article. I hope that the content of this article has a certain reference learning value for your study or work. If you have any questions, you can leave a message for exchange. Thank you for your support to Scenario 3.

The article is transferred from Script House, author: Yi Yun

Use Javascript to implement a custom event mechanism icon-default.png?t=LA92https://www.jb51.net/article/130564.htm

Guess you like

Origin blog.csdn.net/godread_cn/article/details/122031597