This article was first published on my personal blog: cherryblog.site/#/. Welcome to my blog for more articles ~
The original intention of writing this article is to meet a problem: write pop-up layer in the web page of mobile terminal. After the pop-up layer pops up, scroll the page, and the underlying elements will scroll together. This should be a very common problem, starting with a search for a solution, and then figuring out how little you know about the browser’s event mechanism.
In this article, you can learn:
- Content related to event flow:
- What is event capture
- What is event bubbling
- How does the DOM event flow work
- Event handler related:
- What is an HTML event handler
- What is DOM0 event handler
- What is DOM2 event handler
addEventListener()
Detailed explanation of the third parameter
- IE event handler
- What’s the difference between the four
- Event objects:
-
DOM event object
preventDefault()
stopPropagation()
- Not all events are bubbling: 🌰
scroll
- Not all events are bubbling: 🌰
-
IE Event Object
-
- How to solve the roll penetration problem
- ❎
addEventListener()
The third parameter is set to true - ❎
stopPropagation()
To prevent a bubble - ✅ preventDefault Prevents the default event
- ✅ Set overflow: hidden for outer elements
- ❎
The browser’s event mechanism
Event capture, event bubbling, DOM Level 0 events, DOM Level 2 events
In my daily development, although I often add events to elements, I always use event methods encapsulated in the framework, such as onClick of React. In rare cases, I use addEventListener. Before, I always thought that the two are the same, but the writing method is different.
I’ve read a few articles about browser events before, but I didn’t feel very clear about them. I know some concepts but I’m not sure.
Finally suddenly remembered, this kind of conceptual thing, or to turn over a dictionary! So I went to the Little Red Book (JavaScript Advanced Programming) and learned a lot about these things that we use all the time. Well, I guess you can learn from the past. HHHHH
Flow of events
The interaction between JS and HTML is implemented using events. The event flow describes the order in which the page receives events.
The event bubbling
Event bubbling is an event flow solution proposed by the IE team. As the name suggests, event bubbling starts with the most specific elements and propagates up to less specific elements (or documents).
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Document</title>
</head>
<body>
<div id="div"> Click me</div>
</body>
<script>
var div = document.querySelector('#div');
div.addEventListener('click'.() = > {
console.log('div click');
})
var body = document.querySelector('body');
body.addEventListener('click'.() = > {
console.log('body click');
})
var html = document.querySelector('html');
html.addEventListener('click'.() = > {
console.log('html click');
})
var document = document.querySelector('document');
document.addEventListener('click'.() = > {
console.log('document click');
})
</script>
</html>
Copy the code
Here is a basic HTML code. When we click div, the output log is:
div click
body click
html click
document click
Copy the code
Translation: We clickeddiv
After the element, the Click event moves up the DOM tree, firing on each node it passes. untildocument
Elements. (even ifdiv
Element is not boundclick
The event,body
The element is boundclick
The event,click
Events can also bubble up tobody
)All modern browsers support event bubbling.
Event capture
Event capture is an event flow solution developed by the Netscape development team. In contrast to event bubbling, event capture receives events first from the least concrete node and propagates down to the most concrete node. Event capture is really about intercepting events before they reach their final destination.Older browsers do not support event capture.
The DOM event flow
The DOM2 Events specification specifies that the event flow is divided into three phases: event capture, arrival to target, and event bubbling. Event capture occurs first, making it possible to intercept events ahead of time. The actual target element then receives the event. The last stage is bubbling, and events must be responded to at the latest.All current browsers support DOM event streaming, except IE8 and older browsers.
I had a misconception that listening events can only be triggered once in the whole event stream, that is, if they are responded to once in the event stream, either in the event bubble or event capture phase, they will not be transmitted again. But it is not, as long as there is no stopPropagation shown, it will be delivered as event stream.
Event handler
In response to an action performed by the user or browser (click, load, mouseover…) Functions that start with ON are called event handlers (event listeners).
HTML event handler
Each event supported by a particular element can use an event handler in the form of an HTML attribute. In fact, this is the most common way to use code like this, the onclick property value is JS code or some other call method.
With event listeners, the browser first creates a function to encapsulate the value of the property. This function has a special local variable: event to hold the event object, and this in the event handler refers to the target element of the event.
<input type="button" value="click me" onclick="console.log('click')" />
Copy the code
DOM0 event handler
The traditional way to create event listeners in JavaScript is to assign a function to a DOM element. Compatibility is best, and all browsers support this method.
Each element (including window and document) has an event handler attribute (usually onXXXX) that takes the value of a function.
const btn = document.getElementById("myBtn");
btn.onclick = function(){
console.log('Clicked')}Copy the code
This use of DOM0 event handling occurs when program assignment is registered in the bubbling phase of the event flow.
The assigned function is treated as the element’s method and runs in the scope of the element, with this referring to the element itself. You can access any attribute and method of an element in an event handler through this.
Set the event handler property to NULL to remove event handlers added by DOM0.
btn.onclick = null;
Copy the code
If there are more than one DOM0 event handler, the latter will overwrite the previous one. Only the result of the last call.
DOM2 event handler
We can also add and remove event handlers on all DOM nodes via addEventListener() and removeEventLinstener().
AddEventListener () and removeEventLinstener() receive three arguments: Event name, event handler, and an option object or a Boolean useCapture (true means event handler is called in capture phase, false (default) means event handler is called in bubble phase, because of cross-browser compatibility, So event handlers are added to the bubbling phase of the event stream by default (that is, the last parameter is false by default). addEventListener(type, listener, useCapture | options)
The option parameter has the following options
Capture: Boolean, indicating that the listener will be triggered when an event capture phase of this type is propagated to the EventTarget. Once: Boolean: the listener is called at most once after being added. If true, the listener is automatically removed after it is called. Passive: Boolean. If true, the listener will never call preventDefault(). If the listener still calls this function, the client ignores it and throws a console warning.Copy the code
The useCapture parameters are as follows
UseCapture optionalBooleanIn the DOM tree, the element that registered the Listener should be called before the EventTarget below it. When useCapture (set totrue), events that bubble up the DOM tree do not trigger the listener. Event bubbling and event capture are two different ways of propagating events when one element has nested another, and both elements have registered a handler for the same event. The event propagation pattern determines the order in which elements receive events. See the Event flow and JavaScript Event Order documentation for further explanation. If not specified, useCapture defaults tofalse 。
Copy the code
In short, my personal understanding is that the useCapture parameter specifies the “timing” when the event handler fires: during the capture or bubbling phase of the event flow. However, whatever the last parameter is set, it does not impede the flow of events.
🌰 : We have a button on the page, add a click event to the button, add a click event to the body, set useCapture to true and false, and watch what happens
btn.addEventListener("click".(e) = > {
console.log('btn click capture ')},true);
btn.addEventListener("click".(e) = > {
console.log('btn click bubble ')}); body.addEventListener("click".(e) = > {
console.log('body click capture')},true);
body.addEventListener("click".(e) = > {
console.log('body click bubble')});// body click capture
// btn click capture
// btn click bubble
// body click bubble
Copy the code
As you can see from the log results, useCapture (capture in Option) only controls whether the event handler is added in the capture or bubble phase of the event stream. It has no effect on event propagation!
The **preventDefault()** method of the Event interface tells the User Agent that if this Event is not explicitly handled, its default action should not take place either. This event will continue to propagate until the event listener calls stopPropagation() or stopImmediatePropagation().
The above code is an event handler that fires Click during the specified phase of the event flow. Like DOM0, this event handler is also run attached to the scope of the element, so this in the event handler refers to that element.
One advantage of DOM2 event handlers is that multiple event handlers can be added to an element and fired in the order they were added.
Event handlers added with addEventListener() can only be removed with removeEventLinstener() (all three parameters are consistent); Therefore, event handlers added using anonymous functions cannot be removed.
IE event handler
IE implements event handlers by attachEvent() and detachEvent(). These methods take the same two arguments: the name of the event handler (eg: onclick) and the event handler function. Because IE8 and earlier only support event bubbling, event handlers added using attachEvent() are added in the bubbling phase.
const btn = document.getElementById("myBtn");
btn.attachEvent("onclick".function(){
console.log("Clicked");
})
Copy the code
There are two differences between IE event handlers and DOM2 event handlers
- Scope:
attachEvent()
It’s running in global scope, soattachEvent()
In the function ofthis
是window
; - Order of execution: IE event handlers are executed in reverse order of addition.
The differences between the four event handlers
Method of use | Remove method | note | |
---|---|---|---|
HTML event handler | Use event handlers in the form of HTML attributesonclick="console.log('click')" |
Same as DOM0 event handler | event Local variables are used to saveevent Object, functionthis Points to the target element of the event |
DOM0 event handler | Assign a function to a DOM elementbtn.onclick = function(){} |
btn.onclick = null |
1. It occurs in the bubbling stage |
2. This refers to the element itself | |||
DOM2 event handler | addEventListener() |
removeEventLinstener() |
1. Accept three parameters: event name, event handler, and options or a Boolean value (true Means that the event handler is invoked during the capture phase,false (Default) calls event handlers during the bubble phase.) |
2. Multiple event handlers can be added to an element and fired in the order in which they were added. | |||
Use 3.addEventListener() The added event handler can only be usedremoveEventLinstener() Remove (only if the three parameters are consistent) |
|||
IE event handler | attachEvent() |
detachEvent() |
1. IE8 and earlier versions only support event bubbling |
2. AttachEvent () is run in the global scope,this 是 window |
|||
3. IE event handlers are executed in reverse order of addition |
The event object
When an event occurs in the DOM, all relevant information is collected in an object called Event. This object contains some basic information: the element that triggered the event, the type of event, and some other data related to the particular event (such as the location of the mouse associated with the mouse event). All browsers support this event object.
btn.onclick = function(event){
console.log(event.type) // click
}
btn.addEventListener("click".() = > {
console.log(event.type); // click
}, false);
Copy the code
DOM event object event
Inside the event handler, the this object is always equal to currentTarget (because this refers to the calling object).
Target is the actual target that the event fires. (It is possible for target and currentTarget to be unequal during the event bubble phase.
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Document</title>
</head>
<body>
<div id="div"> Click me</div>
</body>
<script>
var div = document.querySelector('#div');
div.addEventListener('click'.function(e){
console.log('div click', e.currentTarget === this); // true
console.log('div click', e.target === this); // true
})
var body = document.querySelector('body');
body.addEventListener('click'.function(e){
console.log('body click', e.currentTarget === this); // true
console.log('body click', e.target === this); // false
})
</script>
</html>
Copy the code
preventDefault()
The preventDefault() method is used to prevent the default behavior of the event (e.g., the A tag has default behavior to jump to the href link, preventDefault() prevents this navigation behavior)
PreventDefault () must be cancelable element **
Event preventDefault Normal operation is priority check whether cancelable (MDN check) can be cancelled. If cancelable=false, the default behavior will not be blocked and the code preventDefault will not work. (See MDN: Most native browser events generated by user interaction with the page can be cancelled. [click cancel] (https://developer.mozilla.org/zh-CN/docs/Web/Reference/Events/click), [scroll] (https://developer.mozilla.org/zh-CN/docs/Web/Reference/Events/scroll) or [beforeunload] (https://developer.mozilla.org/zh-CN/docs/Web/Reference/Events/beforeunload) event will prevent user clicks on certain elements respectively, scroll or jump from the page.
const link = document.getElementById("myLink");
link.onclick = function(event){
event.preventDefault();
}
Copy the code
stopPropagation()
The stopPropagation() method is used to immediately stop the propagation of the event stream in the DOM, canceling subsequent event capture or bubbling. Such as
var div = document.querySelector('#div');
div.addEventListener("click".(e) = > {
console.log("clicked");
e.stopPropagation()
}, false);
document.body.addEventListener("click".() = > {
console.log("body clicked");
}, false);
// clicked
Copy the code
If you don’t call stopPropagation(), clicking div will give you two logs. If added, the click event is not propagated to the body, just a log.
But it’s important to note that not all events bubble up! For example, some Scroll events don’t bubble.
The Scroll event is non-bubbling for ordinary Element elements
We can see the description of scroll Event on MDN
Bubbles Not on elements, but Bubbles to the default view when fired on the document bubble: The Scroll event for element does not bubble, but the Scroll event for the Document defaultView does
That is, if the element that triggers scroll is an element, there will only be event capture from Document to Element and Element bubbling. Let’s write dome to verify that
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Document</title>
<style>
body{
position: relative;
width: 100vw;
height: 10000px;
margin: 0;
background-image: linear-gradient(#e66465.#9198e5);
}
#mask{
position: fixed;
width: 100vw;
height: 100vh;
background-color: rgba(0.0.0.0.6);
top: 0;
left: 0;
}
.modal{
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 50vh;
background: #fff;
overflow: scroll;
}
</style>
</head>
<body>
<div id="mask">
<div class="modal">
<h1>A</h1>
<h1>B</h1>
<h1>C</h1>
<h1>D</h1>
<h1>E</h1>
<h1>F</h1>
<h1>G</h1>
<h1>H</h1>
<h1>I</h1>
<h1>G</h1>
<h1>K</h1>
<h1>L</h1>
<h1>M</h1>
</div>
</div>
</body>
<script>
var modal = document.querySelector('.modal');
function log(element, phase) {
console.log(`scroll handler is trigged in ${element} during ${phase}`)}function bindEvent(element, elemName) {
element.addEventListener('scroll', log.bind(null, elemName, 'capture'), true)
element.addEventListener('scroll', log.bind(null, elemName, 'bubble'))
}
bindEvent(window.'window')
bindEvent(modal, 'modal')
</script>
</html>
Copy the code
“Butdocument
thedefaultView
thescroll
The word “bubbling” means if it isdefaultView
The default is that the Scroll event on the window is bubbling. However, since it is the top-level object in the DOM tree, it can only monitor the Capture and bubble phases of Scroll in the Window. All you need to do is put a div inside the body that goes beyond one screen, and then listen for the Scroll event of the window and that div.
IE Event Object
IE event objects are different depending on which event handler is used.
- With the DOM0 event handler, the Event object is a property of the global object Window
- use
attachEvent()
The/HTML attribute method handles the event handler, and the event object is passed to the handler as its only argument. (The event is still an attribute of the window object, but you can just call it in as an argument.)
var div = document.querySelector('#div');
div.onclick = function(){
let event = window.event;
console.log(evennt.type); // click
}
div.attachEvent('onclick'.function(event){
console.log(event.type); // click
})
Copy the code
Prevents the bottom of the popover from scrolling
Having said that, remember the problem we were trying to solve in the first place? After the pop-up layer pops up, scroll the page. Because the event bubbles, the scroll event will bubble to the underlying element (usually body) and the underlying element will scroll with it (the event triggered by the mobile side is Touchmove, not Scroll). ~~ Can we use stopPropagation() to stop event bubbles to solve the scrolling penetration problem? You can write your own dome. The answer is no, using stopPropagation() does prevent bubbles, but viewpoint causes the background layer to scroll. So while it prevents events from bubbling, it still causes the background layer to scroll along.
All scrolling is done by forming a pending queue on the Document, which is then triggered according to certain rules as described in the W3C specification:
When asked to run the scroll steps for a Document doc, run these steps: For each item target in doc’s pending scroll event targets, in the order they were added to the list, run these substeps: If target is a Document, fire an event named scroll that bubbles at target. Otherwise, fire an event named scroll at target. Empty doc’s pending scroll event targets.
When we scroll the mouse wheel, or swipe the phone screen, there are two types of trigger objects (see W3C specification) :
- The viewPort is triggered to scroll, and the eventTarget is the associated Document
- The Element is triggered to scroll, which is usually the element to which we added overflow scroll, with the corresponding Node Element as eventTarget
Notice that there are only two types here, so when we fire scroll or slide, if the current element doesn’t have overflow Settings and the preventDefault event is the native scroll/slide event, then it’s the viewport scroll, The position:fixed element is no exception.
It can be seen that the scrolling penetration problem is not a browser bug (although fixed positioning in ios does cause a lot of bugs), it is fully compliant with the specification, the principle of scrolling should be scrollforWhat can scroll, The CSS positioning of an element should not cause the wheel to fail or the slide to fail.
So if events don’t bubble up, why does the problem of roll penetration occur? If the currently scrolling element can be scrolled, the Scroll event is raised on the current element. If the current element cannot be scrolled (or scrolled to the edge and no longer scrolled), the outer element is scrolled.
Another solution is to use e.preventDefault() to prevent the popup layer from rolling by default.
PreventDefault prevents pop-up layer default scroll events.
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Document</title>
<style>
body{
position: relative;
width: 100vw;
height: 10000px;
margin: 0;
background-image: linear-gradient(#e66465.#9198e5);
}
#mask{
position: fixed;
width: 100vw;
height: 100vh;
background-color: rgba(0.0.0.0.6);
top: 0;
left: 0;
}
</style>
</head>
<body>
<div id="mask"> mask </div>
</body>
<script>
var mask = document.querySelector('#mask');
mask.addEventListener("touchmove".(e) = > {
e.preventDefault()
}, {
passive: false
});
</script>
</html>
Copy the code
One small detail is that we set the last parameter of addEventListener to {passive: false}. Here is a small optimization: the site uses passive event listeners to improve scrolling.
But there are two problems with using this method:
- Scrolling within the pop-up layer will also be disabled because touch events at the pop-up layer are blocked
- The body scroll can still be triggered when the pop-up layer scrolls to the bottom.
So we also need to optimize to determine when to disable scrolling: scroll to boundaries (up, down, left, right) and disable 🚫 if you continue scrolling.
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Document</title>
<style>
body{
position: relative;
width: 100vw;
height: 10000px;
margin: 0;
background-image: linear-gradient(#e66465.#9198e5);
}
#mask{
position: fixed;
width: 100vw;
height: 100vh;
background-color: rgba(0.0.0.0.6);
top: 0;
left: 0;
}
.modal{
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 50vh;
background: #fff;
overflow: scroll;
}
</style>
</head>
<body>
<div id="mask">
<div class="modal">
<h1>A</h1>
<h1>B</h1>
<h1>C</h1>
<h1>D</h1>
<h1>E</h1>
<h1>F</h1>
<h1>G</h1>
<h1>H</h1>
<h1>I</h1>
<h1>G</h1>
<h1>K</h1>
<h1>L</h1>
<h1>M</h1>
</div>
</div>
</body>
<script>
var mask = document.querySelector('#mask');
var modal = document.querySelector('.modal');
// Record the first touch ordinate
let startY = 0;
const modalHeight = modal.clientHeight;
const modalScrollHeight = modal.scrollHeight;
modal.addEventListener("touchstart".(e) = > {
startY = e.touches[0].pageY;
})
mask.addEventListener("touchmove".(e) = > {
let endY = e.touches[0].pageY;
let delta = endY - startY;
if(
(modal.scrollTop === 0 && delta > 0) ||
(modal.scrollTop + modalHeight === modalScrollHeight &&
delta < 0)
){
e.preventDefault()
}
}, true);
</script>
</html>
Copy the code
Set overflow: Hidden for the body
This is actually a bit violent, change the body style manually when the popover pops up, and then remember to change the body style back when the popover closes.
Ps: I’m lazy and js controls the line style, but the standard way to do this is to add the class name to the body
// The bottom does not slide
const bodyEl = document.querySelector('body');
let top = 0;
const stopBodyScroll = (isFixed: boolean) = > {
if (isFixed) {
top = window.scrollY
bodyEl && (bodyEl.style.position = 'fixed')
bodyEl && (bodyEl.style.top = -top + 'px')}else {
bodyEl && (bodyEl.style.position = ' ')
bodyEl && (bodyEl.style.top = ' ')
window.scrollTo(0, top) // return to top}}Copy the code
This method can be improved, because we can scroll every time not only the body but also other elements cause the scroll. Second, to record the position before the scrolling element, do not simply set it to empty.