Lesson 4: Working with Browser Events
Lesson Overview
To recap, we'll be looking at :
Events - what they are
Responding to an event - how to listen for an event and respond when one happens
Event Data - harness the data that is included with an event
Stopping an event - preventing an event from triggering multiple responses
Event Lifecycle - the lifecycle stages of an event
DOM Readiness - events to know when the DOM itself is ready to be interacted with
Seeing An Event(Debug only)
如何開眼?
Chrome browser has a special monitorEvents()
function that will let us see different events as they are occurring.
Check out the documentation on the Chrome DevTools site: monitorEvents documentation
可以看到當你在browser中做這些事情的時候,都會有event產生:
click
dblclick
scroll
resize
Respond to Events
EventTarget
Interface
The EventTarget page says that EventTarget:
is an interface implemented by objects that can receive events and may have listeners for them.
and
Element, document, and window are the most common event targets, but other objects can be event targets too…
If you take a look at the EventTarget Interface, you'll notice that it doesn't have any properties and only three methods! These methods are:
.addEventListener()
.removeEventListener()
.dispatchEvent()
1. Adding An Event Listener
Let's use some pseudo-code to explain how to set an event listener:
So an event listener needs three things:
an event target - this is called the target
the type of event to listen for - this is called the type
a function to run when the event occurs - this is called the listener
The <event-target>
(i.e. the target) goes right back to what we just looked at: everything on the web is an event target (e.g. the document
object, a <p>
element, etc.).
The <event-to-listen-for>
(i.e. the type) is the event we want to respond to. It could be a click, a double click, the pressing of a key on the keyboard, the scrolling of the mouse wheel, the submitting of a form...the list goes on!
The <function-to-run-when-an-event-happens>
(i.e. the listener) is a function to run when the event actually occurs.
Let's transform the pseudo-code to a real example of an event listener:
Check out the documentation for more info: addEventListener docs
2. Add Event Listener to the Project
在index file closing body tag前加入你的js script:
js file的內容就是之前console裡面寫的code。
To see a full list of all of the possible events you can listen for, check out the Events documentation: list of events
Remove an Event Listener
Let's say you only want to listen for just the first click event, respond to it, and ignore all other click events. The .addEventListener()
event will listen for and respond to all click events.
To remove an event listener, we use the .removeEventListener()
method. It sounds straightforward enough, right? However, before we look at .removeEventListener()
, we need to take a brief review of object equality.
Are Objects Equal in JavaScript
Equality is a common task in most programming languages, but in JavaScript, it can be a little bit tricky because JavaScript does this thing called type coercion where it will try to convert the items being compared into the same type. (e.g. string, number,). JavaScript has the double equality (==
) operator that will allow type coercion. It also has the triple equality (===
) symbol that will prevent type coercion when comparing.
忽然發現js定義object field的方式不太一樣:
Ok, so why do we care about any of this object/function equality? The reason is that the .removeEventListener()
method requires you to pass the same exact listener function to it as the one you passed to .addEventListener()
.
Remember, the listener function must be the exact same function as the one used in the .addEventListener()
call...not just an identical looking function.
This code will successfully add and then remove an event listener:
Code that does not work
Chrome上的Event listener可以用來查看目前選擇的element上有哪些listener:
找到function所在的js位置:
Phases of an Event
1. Event Phases
There are three different phases during the lifecycle of an event. They are:
the capturing phase
the at target phase
and the bubbling phase
And they actually follow the order above; first, it's capturing, then at target, and then the bubbling phase.
Most event handlers run during the at target phase, such as when you attach a click event handler to the button. The event arrives at the button (its target), and there's only a handler for it right there, so the event handler gets run.
But sometimes you have a collection of items -- such as a list -- and want to have one handler cover every item (and still have the option of individual handlers for some items.) By default, if you click on a child item and a handler doesn't intercept the click, the event will "bubble" upward to the parent, and keep bubbling until something handles it or it hits the document.
Capturing, on the other hand, lets the parent intercept an event before it reaches a child.
為什麼要有phases? 為了讓parent element可以在event到child之前或之後listen same event而後做動作。
要如何在listener中指定phase?
Up until this point, we've only seen the .addEventListener()
method called with two arguments, the:
event type
and the listener
There's actually a third argument to the .addEventListener()
method; the useCapture argument. From it's name, you'd think that if this argument were left out, .addEventListener()
would default to using the capturing phase. This is an incorrect assumption! By default, when .addEventListener()
is called with only two arguments, the method defaults to using the bubbling phase.
However, in this code, .addEventListener()
is called with three arguments with the third argument being true
(meaning it should invoke the listener earlier, during the capturing phase!).
2. The Event Object
Now that you know that event listeners fire in a specific order and how to interpret and control that order, it's time to shift focus to the details of the event itself.
取得event object資訊!
When an event occurs, the browser includes an event object. This is just a regular JavaScript object that includes a ton of information about the event itself. According to MDN, the .addEventListener()
's listener function receives:
a notification (an object that implements the Event interface) when an event of the specified type occurs
Up until this point, I've been writing all of the listener functions without any parameter to store this event object. Let's add a parameter so we can store this important information:
Notice the new event
parameter that's been added to the listener function. Now when the listener function is called, it is able to store the event data that's passed to it!
3. The Default Action
As we just looked at, the event object stores a lot of information, and we can use this data to do all sorts of things. However, one incredibly common reason that professionals use the event object for, is to prevent the default action from happening.
Think about an anchor link on a webpage. There are probably a couple dozen links on this page! What if you wanted to run some code and display some output when you click on one of these links. If you click on the link, it will automatically navigate you to the location listed in its href
attribute: that's what it does by default.
What about a form element? When you submit a form, by default, it will send the data to the location in its action
attribute. What if we wanted to validate the data before sending it, though?
Without the event object, we're stuck with the default actions. However, the event object has a .preventDefault()
method on it that a handler can call to prevent the default action from occurring!
Avoid Too Many Events
以下的code created 200 event listener and 200 functions
Refactoring The Number of Event Listeners
如何優化?
第一個優化:大家共用同一個function(少了199個function)
第二個優化:大家共用同一個event listener(少了199個event listener)
Now the browser doesn't have to store in memory two hundred different event listeners and two hundred different listener functions. That's great for performance!
但是第二個優化有缺點:
However, if you test the code above, you'll notice that we've lost access to the individual paragraphs. There's no way for us to target a specific paragraph element. So how do we combine this efficient code with the access to the individual paragraph items that we did before?
We use a process called event delegation.
Event Delegation
Remember the event object that we looked at in the previous section? That's our ticket to getting back the original functionality!
The event object has a .target
property. This property references the target of the event. Remember the capturing, at target, and bubbling phases?...these are coming back into play here, too!
Let's say that you click on a paragraph element. Here's roughly the process that happens:
a paragraph element is clicked
the event goes through the capturing phase
it reaches the target
it switches to the bubbling phase and starts going up the DOM tree
when it hits the
<div>
element, it runs the listener functioninside the listener function,
event.target
is the element that was clicked
So event.target
gives us direct access to the paragraph element that was clicked. Because we have access to the element directly, we can access its .textContent
, modify its styles, update the classes it has - we can do anything we want to it!
所以為了要access其中一個element,就是利用event object在bubbling phase中取得event target的資訊來做操作。
1. Checking the Node Type in Event Delegation
回傳的event要記得加上type checking
In the code snippet we used above, we added the event listener directly to the <div>
element. The listener function logs a message saying that a paragraph element was clicked (and then the text of the target element). This works perfectly! However, there is nothing to ensure that it was actually a <p>
tag that was clicked before running that message. In this snippet, the <p>
tags were direct children of the <div>
element, but what happens if we had the following HTML:
In this filler text, notice that there are some <span>
tags. If we want to listen to the <article>
for a click on a <span>
, you might think that this would work:
This will work, but there's a major flaw. The listener function will still fire when either one of the paragraph elements is clicked, too! In other words, this listener function is not verifying that the target of the event is actually a <span>
element. Let's add in this check:
Remember that every element inherits properties from the Node Interface. One of the properties of the Node Interface that is inherited is .nodeName
. We can use this property to verify that the target element is actually the element we're looking for. When a <span>
element is clicked, it will have a .nodeName
property of "SPAN"
, so the check will pass and the message will be logged. However, if a <p>
element is clicked, it will have a .nodeName
property of "P"
, so the check will fail and the message will not be logged.
The .nodeName property will return a capital string, not a lowercase one. So when you perform your check make sure to either:
(1) check for capital letters
(2) convert the .nodeName to lowercase
Recap
In this section, we looked at Event Delegation. Event Delegation is the process of delegating to a parent element the ability to manage events for child elements. We were able to do this by making use of:
the event object and its
.target
propertythe different phases of an event
Further Research
Know When The DOM Is Ready
1. The DOM Is Built Incrementally
Do you remember the video we watched of Illya from Google explaining how the DOM is parsed? A key thing to point out is that when the HTML is received and converted into tokens and built into the document object model, is that this is a sequential process. When the parser gets to a <script>
tag, it must wait to download the script file and execute that JavaScript code. This is the important part and the key to why the placement of the JavaScript file matters!
Let's look at some code to show (more or less) what's happening. Take a look at this initial part of an HTML file:
This isn't the full HTML file...BUT, it's all that's been parsed so far. Notice at the bottom of the code that we have so far is a <script>
file. This is using inline JavaScript rather than pointing to an external file. The inline file will execute faster because the browser doesn't have to make another network request to fetch the JavaScript file. But the outcome will be exactly the same for both this inline version and if the HTML had linked to an external JavaScript file.
The problem is with the .querySelector()
method. When it runs...there's no <footer>
element to select from the constructed document object model yet! So instead of returning a DOM element, it will return null
. This causes an error because it would be like running the following code:
Now, we've already used one solution to this issue. Remember that we moved the JavaScript file down to the bottom of the page. Think about why this would make things work. Well, if the DOM is built sequentially, _if_ the JavaScript code is moved to the very bottom of the page, then by the time the JavaScript code is run, all DOM elements will already exist!
However, an alternative solution would be to use browser events!
所以第一種方法是把js file放在最後面。
第二種方法是用event listener。
2. The Content Is Loaded Event
When the document object model has been fully loaded, the browser will fire an event. This event is called the DOMContentLoaded
event, and we can listen for it the same way we listen to any other events:
3. Using the DOMContentLoaded
Event
DOMContentLoaded
EventBecause we now know about the DOMContentLoaded
event, we can use it to keep our JS code in the <head>
.
Let's update the previous HTML code to include this event:
Pretty cool, right?!? We have the JavaScript code in the <head>
element, but it is now wrapped in an event listener for the DOMContentLoaded
event. This will prevent the DOM-styling code from running when the browser gets to it. Then, when the DOM has been constructed, the event will fire and this code will run.
If you're looking at somebody else's code, you may see that their code listens for the load
event being used instead (e.g. document.onload(...)
). load
fires later than DOMContentLoaded
-- load
waits until all of the images, stylesheets, etc. have been loaded (everything referenced by the HTML.) Many older developers use load
in place of DOMContentLoaded
as the latter wasn't supported by the very earliest browsers. But if you need to detect when your code can run, DOMContentLoaded
is generally the better choice.
However, just because you can use the DOMContentLoaded
event to write JavaScript code in the <head>
that doesn't mean you should do this. Doing it this way, we have to write more code (all of the event listening stuff) and more code is usually not always the best way to do something. Instead, it would be better to move the code to the bottom of the HTML file just before the closing </body>
tag.
So when would you want to use this technique? Well, JavaScript code in the <head>
will run before JavaScript code in the <body>
, so if you do have JavaScript code that needs to run as soon as possible, then you could put that code in the <head>
and wrap it in a DOMContentLoaded
event listener. This way it will run as early as possible, but not too early that the DOM isn't ready for it.
Last updated