Detailed JavaScript event delegation

basic concepts

Event delegation, in layman's terms, is to delegate the function of one element to respond to events (click, keydown...) to another element;

Generally speaking, the event of an element or a group of elements is delegated to its parent or outer element. The outer element is actually bound to the event. When the event responds to the element that needs to be bound, it will Through the event bubbling mechanism to trigger the binding event of its outer element, and then execute the function on the outer element.

For example, if a classmate in a dormitory arrives at the same time, one way is to get them one by one in a foolish manner. Another way is to entrust this matter to the dormitory head and let one person go out and take all the express delivery. Then distribute to each dormitory student one by one according to the recipient;

Here, express delivery is an event, each student refers to the DOM element that needs to respond to the event, and the dormitory head who goes out to receive the express delivery is the agent element, so it is this element that really binds the event, which is distributed according to the recipient The process of express delivery is during the execution of the event, it is necessary to determine which one or several of the delegated elements should be matched by the current response event.

Event bubbling

As mentioned earlier, the implementation of event delegation in the DOM uses the mechanism of event bubbling, so what is event bubbling?

In document.addEventListener, we can set the event model: event bubbling and event capture. Generally speaking, the model of event bubbling is used;

https://pic1.zhimg.com/80/v2-bf3b8dbab027713a2b21b9e8a5b7a6c4_1440w.jpg

As shown in the figure above, the event model is divided into three stages:

  • Capture phase: In the event bubbling model, the capture phase will not respond to any events;
  • Target stage: The target stage refers to the event response to the lowest element that triggers the event;
  • Bubbling stage: The bubbling stage is when the trigger response of the event will go from the lowest target to the outermost layer (root node) layer by layer, and the event proxy is to use the event bubbling mechanism to respond to the events that need to be responded to by the inner layer. Bind to the outer layer; ### event

Advantages of delegation

  1. Reduce memory consumption

Imagine if we have a list with a large number of list items in the list, we need to respond to an event when the list item is clicked;

<ul id="list"> <li>item 1</li> <li>item 2</li> <li>item 3</li> ...... <li>item n</li> </ul> // ...... 代表中间还有未知数个 li

If you bind a function to each list item one by one, it will consume a lot of memory and consume a lot of performance in terms of efficiency;

Therefore, a better approach is to bind the click event to his parent layer, that is ul, and then in the implementation of the event go to matches to determine the target element;

Therefore, event delegation can reduce a lot of memory consumption and save efficiency.

  1. Dynamically bound events

For example, there are only a few list items in the above example, and we bind events to each list item;

In many cases, we need to dynamically add or remove list item elements through AJAX or user operations. Then every time we change, we need to re-bind events to the newly added elements and unbind events to the elements that are about to be deleted;

If you use event delegation, there is no such trouble, because the event is bound to the parent layer, and has nothing to do with the increase or decrease of the target element. The execution to the target element is matched in the process of actually responding to the execution of the event function. ;

Therefore, the use of events can reduce a lot of repetitive work in the case of dynamic binding events.

Event delegation in jQuery

I believe many people have used the event delegation in jQuery, and it is mainly implemented in these ways:

  • $.on : Basic usage: $('.parent').on('click','a', function () {console.log('click event on tag a'); }), it is the .parent element The event of the a element below is delegated to $('.parent'). As long as there is a click event on this element, it will automatically find the a element under the .parent element, and then respond to the event;
  • $.delegate : Basic usage: $('.parent').delegate('a','click', function () {console.log('click event on tag a'); }), same as above, and more Corresponding $.delegate to delete the event of the agent;
  • $.live : Basic usage: $('a', $('.parent')).live('click', function () {console.log('click event on tag a'); }), same as above , But if the parent element $(.parent) is not passed in, the event will be delegated to $(document) by default; (abolished)

Realize the function

Basic realization

For example, we have an HTML fragment like this:

<ul id**=**"list"> <li>item 1</li> <li>item 2</li> <li>item 3</li> ...... <li>item n</li> </ul> // ...... 代表中间还有未知数个 li

Let's realize that the event proxy of the li element under #list is delegated to its parent element, which is #list:

*// 给父层元素绑定事件* document.getElementById('list').addEventListener('click', **function** (e) { *// 兼容性处理* **var** event **=** e **||** window.event; **var** target **=** event.target **||** event.srcElement; *// 判断是否匹配目标元素* **if** (target.nodeName.toLocaleLowerCase **===** 'li') { console.log('the content is: ', target.innerHTML); } });

In the above code, the target element is the element that is specifically clicked under the #list element, and then by judging some attributes of the target (such as: nodeName, id, etc.), a certain type of #list li element can be more accurately matched Above

Use Element.matches to match exactly

If you change the HTML to:

<ul id**=**"list"> <li className**=**"class-1">item 1</li> <li>item 2</li> <li className**=**"class-1">item 3</li> ...... <li>item n</li> </ul> // ...... 代表中间还有未知数个 li

Here, we want to delegate the click event of the li element (and its class is class-1) under the #list element to #list;

If we need by the above methods if (target.nodeName.toLocaleLowerCase === 'li')in determining the addition of a determination target.nodeName.className === 'class-1';

But if you imagine that CSS chooses to do more flexible matching, the above judgments are too much, and it is difficult to achieve flexibility. Here you can use Element.matches API to match;

The basic usage method of Element.matches API: Element.matches(selectorString), selectorString is a selector rule like CSS. For example, in this example, you can use target.matches('li.class-1'), which will return a Boolean value , If the target element is the label li and its class is class-1, then it will return true, otherwise it will return false;

Of course, there are still some problems with its compatibility, and it needs a modern browser version of IE9 and above;

We can use  Polyfill  to solve compatibility problems:

**if** (**!**Element.prototype.matches) { Element.prototype.matches **=**Element.prototype.matchesSelector **||**Element.prototype.mozMatchesSelector **||**Element.prototype.msMatchesSelector **||**Element.prototype.oMatchesSelector **||**Element.prototype.webkitMatchesSelector **||function**(s) { **var** matches **=** (**this**.document **||** **this**.ownerDocument).querySelectorAll(s), i **=** matches.length; **while** (**--**i **>=** 0 **&&** matches.item(i) **!==** **this**) {} **return** i **>** **-**1; }; }

After adding Element.matches, we can achieve our needs:

**if** (**!**Element.prototype.matches) { Element.prototype.matches **=**Element.prototype.matchesSelector **||**Element.prototype.mozMatchesSelector **||**Element.prototype.msMatchesSelector **||**Element.prototype.oMatchesSelector **||**Element.prototype.webkitMatchesSelector **||function**(s) { **var** matches **=** (**this**.document **||** **this**.ownerDocument).querySelectorAll(s), i **=** matches.length; **while** (**--**i **>=** 0 **&&** matches.item(i) **!==** **this**) {} **return** i **>** **-**1; }; } document.getElementById('list').addEventListener('click', **function** (e) { *// 兼容性处理* **var** event **=** e **||** window.event; **var** target **=** event.target **||** event.srcElement; **if** (target.matches('li.class-1')) { console.log('the content is: ', target.innerHTML); } });

Function wrapper

In dealing with more scenarios, we can encapsulate the function of the event agent into a common function, so that it can be reused.

Combine the above example to implement a function eventDelegate, which accepts four parameters:

  • [String] A selector string is used to filter the parent element that needs to implement the proxy, that is, the event needs to be bound to it;
  • [String] A selector string is used to filter the descendants of the selector element that triggered the event, which is the element that we need to be delegated to the event;
  • [String] One or more event types separated by spaces and optional namespaces, such as click or keydown.click;
  • [Function] The function that needs agent event response;

There are several key points here: • There may be multiple elements of the parent agent, and events need to be bound one by one; • There may be multiple types of bound events, and events need to be bound one by one; • Matching is being processed Compatibility issues need to be considered among the elements being proxied; • When executing the bound function, you need to pass in the correct parameters and consider this issue;

**function** eventDelegate (parentSelector, targetSelector, events, foo) { *// 触发执行的函数* **function** triFunction (e) { *// 兼容性处理* **var** event **=** e **||** window.event; **var** target **=** event.target **||** event.srcElement; *// 处理 matches 的兼容性* **if** (**!**Element.prototype.matches) { Element.prototype.matches **=**Element.prototype.matchesSelector **||**Element.prototype.mozMatchesSelector **||**Element.prototype.msMatchesSelector **||**Element.prototype.oMatchesSelector **||**Element.prototype.webkitMatchesSelector **||function**(s) { **var** matches **=** (**this**.document **||** **this**.ownerDocument).querySelectorAll(s), i **=** matches.length; **while** (**--**i **>=** 0 **&&** matches.item(i) **!==** **this**) {} **return** i **>** **-**1; }; } *// 判断是否匹配到我们所需要的元素上* **if** (target.matches(targetSelector)) { *// 执行绑定的函数,注意 this* foo.call(target, Array.prototype.slice.call(arguments)); } } *// 如果有多个事件的话需要全部一一绑定事件* events.split('.').forEach(**function** (evt) { *// 多个父层元素的话也需要一一绑定* Array.prototype.slice.call(document.querySelectorAll(parentSelector)).forEach(**function** ($p) { $p.addEventListener(evt, triFunction); }); }); }

? Optimization

When the element being proxied is not the target element, and the element pointed to by the selector targetSelector is not event.target (the element pointed to by the event target phase), then it is necessary to traverse the parentNode of event.target layer by layer to match the targetSelector. Until the parentSelector.

such as:

<ul id**=**"list"> <li><span>item 1</span></li> <li><span>item 2</span></li> </ul>

Still delegate the event of li to #list, but this time we will find that event.target points to li span, so we need to traverse the outer elements layer by layer to match, until we reach the function of the proxy event, we can use event.currentTarget To get the function of the agent event;

Full function:

`function eventDelegate (parentSelector, targetSelector, events, foo) {// The function that triggers execution function triFunction (e) {// Compatibility handling var event = e || window.event;

// 获取到目标阶段指向的元素
var target = event.target || event.srcElement;

// 获取到代理事件的函数
var currentTarget = event.currentTarget;

// 处理 matches 的兼容性
if (!Element.prototype.matches) {
  Element.prototype.matches =
    Element.prototype.matchesSelector ||
    Element.prototype.mozMatchesSelector ||
    Element.prototype.msMatchesSelector ||
    Element.prototype.oMatchesSelector ||
    Element.prototype.webkitMatchesSelector ||
    function(s) {
      var matches = (this.document || this.ownerDocument).querySelectorAll(s),
        i = matches.length;
      while (--i >= 0 && matches.item(i) !== this) {}
      return i > -1;            
    };
}

// 遍历外层并且匹配
while (target !== currentTarget) {
  // 判断是否匹配到我们所需要的元素上
  if (target.matches(targetSelector)) {
    var sTarget = target;
    // 执行绑定的函数,注意 this
    foo.call(sTarget, Array.prototype.slice.call(arguments))
  }

  target = target.parentNode;
}

}

// If there are multiple events, you need to bind all events one by one events.split('.').forEach(function (evt) {// If there are multiple parent elements, you also need to bind one by one Array.prototype.slice .call(document.querySelectorAll(parentSelector)).forEach(function ($p) {$p.addEventListener(evt, triFunction); }); }); }`

Use function:

eventDelegate('#list', 'li', 'click', **function** () { console.log(**this**); });

After clicking you can see the console #list lielement objects;

?limitation

Of course, event delegation also has certain limitations;

For example, events such as focus and blur have no event bubbling mechanism, so they cannot be delegated;

For events such as mousemove and mouseout, although there are events bubbling, they can only be calculated and positioned continuously by location, which consumes high performance and is therefore not suitable for event delegation;

Original link: https://zhuanlan.zhihu.com/p/26536815

Guess you like

Origin blog.csdn.net/terrychinaz/article/details/114461554