-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathJavaScript.txt
More file actions
669 lines (473 loc) · 34.4 KB
/
Copy pathJavaScript.txt
File metadata and controls
669 lines (473 loc) · 34.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
To declare variables use keywords var, let or const.
var variableName = "anyting" -> general variable when other two cant be used - don't use it if you can avoid it.
let variableName = "anyting" -> can be reassigned (give it a new values) - but cant be redeclared in the same scope (creating new variable with same name to overwrite it)
const variableName = "anyting" -> must be initialised, cant be redeclared in the same scope, cant be reassigned. basically its like a java final variable.
use const as a default and let when const can't be used.
Hoisting: before any js code is executed, all variables declared with a 'var' are hoisted - meaning the declaration of
the variables is raised to the top of the scope of that function (declaration not initialisation as that would mess with the code
itself).
JS doesn't require you to pass arguments to a function, even if it is set up to assign them to a variable. Any you don't define get the value undefined.
In javascript, functions are objects, you can use them exactly how you would use a regular object. They are also callable (can be invoked).
For example, foo(sum(1, 2)) is passing 3 to foo, not the function sum itself. Whereas foo(sum) is passing the function sum as a argument to foo.
Since functions are objects, the following is valid code:
function sayHi() {
console.log("Hi");
}
var func = sayHi; // 'func' and 'sayHi' will point to the same object, the object being a function that prints "Hi" to the console.
func(); // although there was never a function defined called 'func', this will still work because of the above assignment
let and const variables are scoped to the block of code ( { } ) rather than the full function.
This prevents variables from being accessed until after they've been declared - as it should be. prevents hard to find bugs.
you can concatenate strings with '+' or concat() method - but its best practice to do that shit with template literals.
template literals use back ticks instead of any type of quote marks and ${variableName} inside a template literal will summon the value
of the variable stated. No need for further comma's or plus'.
e.g. : const variableNameHere = `Hello, my name is ${myNameVariableDeclaredEarlier}`;
In normal strings you use \n to start a new line. not so in template literals - instead just start a new line like your
in fucking word - with an enter - and the literal will read the new line literally!
furthermore, the variable name in the curly braces of a template literal could be replaced with a maths operation or an entire fucking loop. shits crazy powerful.
create an object then use its values:
const gemstone = {
type: 'quartz',
color: 'rose',
carat: 21.29
};
const type = gemstone.type;
const color = gemstone.color;
const carat = gemstone.carat;
Destructuring: When extracting specific values from an array or object, and saving them into separate variables you can use
destructuring to put the elements you want to extract on the *left* side of an assignment - instead of the right.
e.g.
const point = [10, 25, -34];
const [x, y, z] = point;
The above will save 10 into a const named x, 25 in y and -34 in z. You can also ignore values when destructuring arrays.
For example, const [x, , z] = point; ignores the y coordinate and discards it.
e.g.
const gemstone = {
type: 'quartz',
color: 'rose',
carat: 21.29
};
const {type, color, carat} = gemstone;
In this example, the curly braces { } represent the object being destructured and type, color, and carat represent the
variables where you want to store the properties from the object. Notice how you don’t have to specify the property from
where to extract the values. Because gemstone has a property named type, the value is automatically stored in the type
variable. Similarly, gemstone has a color property, so the value of color automatically gets stored in the color variable.
And it's the same with carat.
Objects can be shortened when writing them out. For example;
let type = 'quartz';
let color = 'rose';
let carat = 21.29;
const gemstone = {
type: type,
color: color,
carat: carat
};
can also be written as :
let type = 'quartz';
let color = 'rose';
let carat = 21.29;
let gemstone = {
type,
color,
carat,
calculateWorth() { ... }
};
LOOPS:
A for-loop:
for (let i = 0; i < array.length; i++) {
console.log(array[i]);
}
The counter(i) and the exit condition(array.length) can be confusing. You still have to deal with the issue of index
meddling with the array.
An alternative is for-in loops which will iterate over the index of an array. To be clear, it wont loop over the data
at a particular index but the index itself. But the index can be used to access the data so some thing:
const digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
for (const index in digits) {
console.log(digits[index]);
}
An additional issue the for-in loop is that it iterates over all values in the array. Including any methods or
properties you may have given the array. This may be undesired.
And then there is for-of loop. This is a loop that iterates over the content stored within each array unit:
for (const digit of digits) {
console.log(digit);
}
forEach loop also exists - it can only be used with arrays and there's no way to stop or break it. For-of is better.
if(digit % 2 === 0) <--- this is a true if statement for 2, 4, 6 etc.
Writing better code:
const upperDay = day.charAt(0).toUpperCase() + day.slice(1);
Spread operator
const fruits = ["apples", "bananas", "pears"];
const vegetables = ["corn", potatoes, carrots];
const produce = [...fruits, ...vegetables];
This operator, among other things, combines arrays
Rest parameter
const order = [20.17, 18.67, 1.50, "cheese", "eggs", "milk", "bread"];
const [total, subtotal, tax, ...items] = order;
This code takes the values of the 'order' array and assigns the first 3 to individual variables, then the rest to a
separate array called 'items' using the rest parameter.
You can also write a function that takes as argument any number of variables by including a Rest parameter.
Arguments object
The arguments object is an array-like local variable inside all functions (called 'arguments'). It contains an
indexed value for each argument passed to the function. e.g.
function sum() {
let total = 0;
for(const argument of arguments) {
total += argument;
}
return total;
}
The issue with the above function is that sum doesn't have parameters (confusing...) and arguments seems to appear
out of nowhere (confusing...) .
You can fix both of those issues with a simple refactoring:
function sum(...nums) {
let total = 0;
for(const num of nums) {
total += num;
}
return total;
}
THE DOM:
When a html page is requested, the browser does the following:
HTML is received
HTML tags are converted to tokens
Tokens are converted to Nodes
Nodes are converted to the DOM
When you request a website, no matter what backend language is powering that website, it will respond with HTML. The
browser receives a stream of HTML. The bytes are run through a complicated (but fully documented) parsing process that
determines the different characters (e.g. the start tag character <, an attribute like href, a closing angle bracket
like >). After parsing has occurred, a process called tokenization begins. Tokenization takes one character at a time
and builds up tokens.
At this stage, the browser has received the bytes that have been sent by a server. The browser has converted the bytes to
tags and has read through the tags to create a list of tokens. This list of tokens then goes through the tree
construction stage. The output of this stage is a tree-like structure - this is the DOM!
The DOM is accessible in javascript via the global 'document' object.
The 'document' object is an object, just like a JavaScript object. This means it has key/value pairs. Some of the values
are just pieces of data, while others are functions (AKA methods!) that provide some type of functionality.
The first DOM method that we'll be looking at is the .getElementById() method: document.getElementById('navbar');
Running this code caused the document object to search through its entire tree-like structure for the element that has
an ID of "navBar". If navBar doesn't exist then it'll return null.
You can store an element returned via getElementById() inside a const or let variable and access it later. i.e.
const navBar = document.getElementById('navbar');
print(navBar); //this like is for illustration purposes only...
ID's are unique. Theres only one element with a given ID on a page. So, when you need to return multiple elements, use
the following methods:
.getElementsByClassName() //notice element'S' instead of element previously
.getElementsByTagName() //notice element'S' instead of element previously
//also note that its TagName and ClassName - not just Tag and Class
Putting the class name method to use we get:
document.getElementsByClassName('story_box');
The above will return an HTMLCollection list of elements with the 'story_box' class.
And, finally, using the tag name method...
document.getElementsByTagName('p');
This will return a HTMLCollection of all elements composed of the 'p' tag.
A Node in the DOM is javascripts version of an objects class, and a node (lowercase n) is an object created from that class.
The Node is like a superclass for lesser objects. It has some default variables and methods that all objects have, and other
objects, like 'Element' inherit from it. Meaning any element object also inherits from 'Node' and in the way of polymorphism,
*is* a node.
Elements objects also have a .getElementsByClassName() method, so you can select an element from the document object and then
use the same method on the returned element to get a list of elements of a particular class that are descendants of it.
There are a fuck-ton of 'interfaces' like Node and Element in JS - use to WebAPI page to view them.
The querySelector Method
We can use the .querySelector() method to select elements just like we do with CSS by passing to it a string that's just like a CSS selector:
find and return the element with an ID of "header"
document.querySelector('#header');
find and return the first element with the class "header"
document.querySelector('.header');
find and return the first <header> element
document.querySelector('header');
The issue with the above is the .querySelector only returns one argument. Give at a class or tag and it will return the first argument it
finds that matches your provided class or tag.
To find the first paragraph element with a class of callout:
document.querySelector('p.callout');
To get all elements of a certain tag or class, and use CSS rules, use the querySelectorAll() method
find and return a list of elements with the class "header"
document.querySelectorAll('.header');
find and return a list of <header> elements
document.querySelectorAll('header');
The returned list of elements from querySelectorAll is of type 'NodeList' - which has a built in forEach method (among others)
A NodeList behaves just like an array, has the same methods(.length) and uses the same index system to get individual elements[].
Since every element inherits from the Element 'interface', they also inherit the .innerHTML property(variable). This property
contains a string with the elements HTML in it (as well as any elements contained inside it). It can be used to set the HTML of an element.
There exists also outerHTML which includes the tags of the element it is called on - inner html contains just its content within those tags.
And there is textContent property - which returns only the displayed to screen text of an element. You can set any of these properties to a new values like below:
const card = document.querySelector('.card')
card.textContent = "I am UPDATED";
Passing any text that looks like HTML to the .textContent property will still be displayed as text. It will not be displayed as HTML when the element is rendered.
To change an elements html content you need to set the innerHTML property.
There is also an innerText property, this one returns the text of an element as it would be seen visually (text content returns it as is).
This means innerText will not show text the has css property 'display none' because that text isn't displayed.
CREATING AND ADDING NEW ELEMENTS TO PAGE
You create an element useing the createElement() method like so:
// create a brand new <span> element
const newSpan = document.createElement('span');
Then, you need to get another element that you want it to be a child of:
// select the first (main) heading of the page
const mainHeading = document.querySelector('h1');
Then, you use the appendChild() method to add your new element to the end of another elements descendants.
// add the <span> element as the last child element of the main heading
mainHeading.appendChild(newSpan);
The createTextNode method creates..you guessed it, textNodes! Now you're wondering what the fuck a textNode is. I don't know
either but lets read on and find out... Basically, a text node is plain text that you append to an element like paragraph
before you append the paragraph to another element.
// create a paragraph element
const myPara = document.createElement('p');
// create a text node
const textOfParagraph = document.createTextNode('I am the text for the paragraph!');
// append the text node to the paragraph
myPara.appendChild(textOfParagraph);
// append the paragraph to the body tag
document.body.appendChild(myPara);
However, this is useless because its much faster to just edit the textContent property of the paragraph element you created.
const myPara = document.createElement('p');
myPara.textContent = 'I am the text for the paragraph!';
document.body.appendChild(myPara);
If you try and append the same element to two other elements; append child will move the element from one to the other; basically
only the final element to be appended by your element will keep the element.
.appendchild() only adds the element to the end of the tag its appended to. To add an element anywhere else you need to use the
.insertAdjacentHTML() method.
The .insertAdjacentHTML() method has to be called with two arguments:
the location of the HTML
the HTML text that is going to be inserted
The first argument to this method will let us insert the new HTML in one of four different locations
beforebegin – inserts the HTML text as a previous sibling
afterbegin – inserts the HTML text as the first child
beforeend – inserts the HTML text as the last child
afterend – inserts the HTML text as a following sibling
An example using this would be:
const mainHeading = document.querySelector('#main-heading');
const htmlTextToAdd = '<h2>Skydiving is fun!</h2>';
mainHeading.insertAdjacentHTML('afterend', htmlTextToAdd);
If an element already exists in the DOM and this element is passed to .appendChild(), the .appendChild() method will
move it rather than duplicating it
The .insertAdjacentHTML() method's second argument has to be text, you can't pass an element
REMOVING ELEMENTS:
The removeChild() property can be used to remove a child
<parent-element>.removeChild(<child-to-remove>);
You do not need to know what the parent element is of the child you want to remove. Just use the parentElement property!
const mainHeading = document.querySelector('h1');
mainHeading.parentElement.removeChild(mainHeading);
Or you can use the elements own remove method to remove it directly. Much quicker!
const mainHeading = document.querySelector('h1');
mainHeading.remove();
To get the first child of an element use the firstElementChild property.
The difference between .firstChild and .firstElementChild, is that .firstElementChild will always return the first element,
while .firstChild might return whitespace (if there is any) to preserve the formatting of the underlying HTML source code.
DYNAMIC STYLING
To change the look of an element at run time, you can do it one of two ways. The first is add a class to an element when an event
occurs. The new class can have different styles rules that apply to that class in CSS:
const element_name_in_js = document.querySelector('h1');
element_name_in_js.className = 'current_class new_class';
The other way is to add styles directly in JavaScript using the Style attribute.
const mainHeading = document.querySelector('h1');
mainHeading.style.color = 'red';
mainHeading.style.fontSize = '2em';
using the above method, each styling change must be done one line at a time. No shortcuts.
But we can use the '.style.cssText' property to set multiple CSS styles at once; like so:
const mainHeading = document.querySelector('h1');
mainHeading.style.cssText = 'color: blue; background-color: orange; font-size: 3.5em';
Notice that when using the .style.cssText property, you write the CSS styles just as you would in a stylesheet; so you
write font-size rather than fontSize. This is different than using the individual .style.<property> way.
The .style.cssText will overwrite anything that's already in an elements .style attribute (won't change css sheet
rules but attribute css override stylesheet rules for identical properties).
ATTRIBUTES
Another way to set styles for an element is to bypass the .style.<property> and .style.cssText properties altogether and
use the .setAttribute() method:
const mainHeading = document.querySelector('h1');
mainHeading.setAttribute('style', 'color: blue; background-color: orange; font-size: 3.5em;');
The setAttribute() method is not just for styling page elements. You can use this method to set any attribute for an
element. If you want to give an element an ID, you can do that!:
const mainHeading = document.querySelector('h1');
// add an ID to the heading's sibling element
mainHeading.nextElementSibling.setAttribute('id', 'heading-sibling');
// use the newly added ID to access that element
document.querySelector('#heading-sibling').style.backgroundColor = 'red';
The above described methods however are not recommended. CSS should handle styling, HTML should handle page content, and JavaScript
should handle the links between them. Adding CSS attributes via JS is not a best practice as it muddies the code and makes maintenance
difficult (things are easy to find in a large code base).
Therefore, best practice is to add css styling rules to various classes, then add those classes to elements you want the styling to apply to.
.className property returns a space separated string of the elements classes. So, element.className = 'new-class' will change the class's of that
element. Though - this will erase any classes already applied to the element.
A better way is to use the classList property to return a DOMTokenList. The .classList property has a number of properties of its own. Some of the
most popularly used ones are:
.add() - to add a class to the list
.remove() - to remove a class from the list
.toggle() - to add the class if it doesn't exists or remove it from the list if it does already exist
.contains() - returns a boolean based on if the class exists in the list or not
Example using those properties:
const mainHeading = document.querySelector('h1');
mainHeading.classList.add('new_class_name'); //this will add the new class without erasing any existing classes.
EVENTS
// start displaying all events on the document object
monitorEvents(document);
// turn off the displaying of all events on the document object.
unmonitorEvents(document);
monitorEvents is for development/testing purposes only. It's not supposed to be used for production code.
In the inheritance hierarchy of javascript, all 'Elements' inherit from 'Nodes', and all 'Nodes' inherit from the EventTarget interface.
In fact, the EventTarget is the top of the food chain...The document object itself inherits from it - as does every element - while it inherits from nothing.
The EventTarget interface has no properties and 3 methods:
- .addEventListener()
- .removeEventListener()
- .dispatchEvent()
Adding an event listener, with the appropriate inherited method, allows JS to listen for and respond to an event.
Let's use some pseudo-code to explain how to set an event listener:
event-target.addEventListener(event-to-listen-for, function-to-run-when-an-event-happens);
So, an event listener needs 3 things, the target, the type (click, double click, given key on the keyboard etc) and the listener (function executed when event happens).
Example:
const mainHeading = document.querySelector('h1');
mainHeading.addEventListener('click', function () {
console.log('The heading was clicked!');
});
Lots of types of events, see list here: https://developer.mozilla.org/en-US/docs/Web/Events
To stop listening for an event, use the .removeEventListener() method.
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 etc. 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.
The triple equality also only returns true when referring to the same object in memory, two separate but identical objects would return false.
The reason this matters is that the .removeEventListener() method requires you to pass the same exact listener function to it as the one you
passed to .addEventListener().
event-target.removeEventListener(event-to-listen-for, function-to-remove);
So a .removeEventListener() needs three things:
an event target
the type of event to listen for
the function to remove - this is called the listener
Remember, the listener function must be the exact same function as the one used in the .addEventListener() call...not just
an identical looking function. Let's look at a couple of examples.
function myEventListeningFunction() {
console.log('howdy');
}
// adds a listener for clicks, to run the `myEventListeningFunction` function
document.addEventListener('click', myEventListeningFunction);
// immediately removes the click listener that should run the `myEventListeningFunction` function
document.removeEventListener('click', myEventListeningFunction);
As you can see above, the add and remove event listener methods require the same target, type and listener (function).
Now lets look at code that fails:
// adds a listener for clicks, to run the `myEventListeningFunction` function
document.addEventListener('click', function myEventListeningFunction() {
console.log('howdy');
});
// immediately removes the click listener that should run the `myEventListeningFunction` function
document.removeEventListener('click', function myEventListeningFunction() {
console.log('howdy');
});
This code does not successfully remove the event listener. Again, why does this not work?
.addEventListener() and .removeEventListener have their own distinct listener functions...they do not refer to the exact
same function in memory (this is the reason the event listener removal fails!)
PHASES OF AN EVENT
Events have 3 phases: Capturing, At-Target and Bubbling - in that order.
When an event occurs, first - line by line - the event listener checks each element encompassing the clicked target for a EventListener
made to run during the capturing phase.
When the target is reached, it switches to At-Target phase.
Then it switches to Bubbling phase, the same as capturing phase but in the opposite direction.
Most EventListeners are executed during 'At-Target'.
The default of any EventListener is to be executed during Bubbling phase (first step of which is At-Target, so it encompasses that phase). If you want
An event listener to be executed during the capturing phase of an element, add a third argument to your event listener, a boolean value of true.
The phases of an event exist to allow an order of events firing if there are multiple eventListeners attached to a single event. In addition, if an element
is clicked, a click event occurs, but that element may not have an action listener itself, it may be a part of a larger element with a listener. In this
case the clicked element is the target, and the event occurs during the bubbling phase when the event phase reaches the larget element with a listener attached.
When an event occurs, the browser creates an event object with all the events data, and returns this to the listener method.
That is to say, if instead of:
document.addEventListener('click', function () {
console.log('The document was clicked');
});
we instead wrote an event listener like this:
document.addEventListener('click', function (event) { // ← the `event` parameter is new!
console.log('The document was clicked');
});
we would have the event object in our function to use.
event.target gives the target element of an event - even if the target was not specifically where the eventListener was applied. Because we have access
to the target element directly, we can access its .textContent, modify its styles, update the classes it has - we can do anything we want to it! This means
we can have one event listener for many elements thereby improving performance of our webpage. You could even use one listener for each type of required
event on a page - by applying the listener to a large encompassing div container.
The event object for each type of event contains a lot of information. Read documentation on MDN for say a mouse event etc for actual variable names.
The event object also has a .preventDefault() method...errr..function. This, prevents the default behavior of a given element. Say, you didn't wan't a submit
button to immediately send a forms data to where-ever, but wanted to validate the form data first - you'd need to prevent default. Or, if you didn't want
an <a> link to open a webpage, but just run some code that did something else instead, you'd prevent default.
Say you wanted an eventListener that only fired when an event (click) occurred on a specific type of element, you could do something like:
document.querySelector('#elementID').addEventListener('click', function (evt) {
if (evt.target.nodeName === 'SPAN') { // ← verifies target is desired element
console.log('A span was clicked with text ' + evt.target.textContent);
}
});
The if statement in the above code could easily be replaced with a check of elements class or ID before event is responded to.
DOM NEEDS TO BE READY FOR JS
The Dom is parsed in order, html -> tokens -> nodes -> DOM. Each token is parsed in sequence and when the parser gets to the <script> file it is downloaded
and the code executed. Thus, if the script file is at the top of the html page, it will reference html elements that haven't yet been parsed and therefore
don't exist. So, the Javascript file must be linked as the last thing in the html page.
Unless...there is another solution to this problem!
The Dom fires an event when it is finished loading everything for the document-object.
An eventListener listening for 'DOMContentLoaded' event, with JS code in its listener function would bypass the need for the javascript file to be required
at the bottom of the page. You could add such a file to the header:
document.addEventListener('DOMContentLoaded', function () {
document.querySelector('footer').style.backgroundColor = 'purple';
});
But - the above requires more code and unnecessarily complicates things. Just put the <script> tag at the bottom of your html page.
MEASURING AND IMPROVING PERFORMANCE OF CODE:
First, how to measure:
The performance.now() function starts counting in milliseconds from the moment the page loads. So, to get a count of how long it took some code to run, use the
function twice - before and after the code to be measured - and subtract the first value from the second. You'll get an accurate measure of how long it took the code to run:
const startingTime = performance.now();
//code that takes ages
const endingTime = performance.now();
console.log('This code took ' + (endingTime - startingTime) + ' milliseconds.');
The browser is constantly working to make the screen match the DOM. When we add a new element, the browser has to run through a reflow calculation (to determine the
new screen layout) and then repaint the screen. This takes time. So - when possible, add many extra elements all at once instead of adding many individual elements
to a page one at a time - that will make your page load slower.
To do the above, use document fragments to combine the required elements, then add them to the page all at once, like so:
const fragment = document.createDocumentFragment(); // ← uses a 'DocumentFragment' instead of a 'div'
for (let i = 0; i < 100; i++) {
const newElement = document.createElement('p');
newElement.innerText = 'This is paragraph number ' + i;
fragment.appendChild(newElement);
}
document.body.appendChild(fragment); // reflow and repaint here -- once!
Reflow is the process of the browser laying out the page. It happens when you first display the DOM (generally after the DOM and CSS have been loaded),
and happens again every time something could change the layout. This is a fairly expensive (slow) process.
Repaint happens after reflow as the browser draws the new layout to the screen. This is fairly quick, but you still want to limit how often it happens.
If you have to make a group of changes, hide/change all/show is a great pattern to use if the changes are relatively contained.
// hide section
document.getElementById("section").style.display = "none";
// delete or add elements
// show section
document.getElementById("section").style.display = "block";
This is surprisingly fast, to the cost of one reflow and two repaints (and little else). It's fast because hiding doesn't change the layout, it just erases that section of the
screen (1 repaint). When you make the changed section visible again, that's a reflow and a repaint.
VIRTUAL DOM
This is why React and other "virtual DOM" libraries are so popular. You don't make changes to the DOM, but make changes to another structure (a "virtual DOM")
and the library calculates the best way to update the screen to match. The catch is you then have to rework your code to use whatever library you're adopting, and sometimes
you can do a better job updating the screen yourself (because you understand your own unique situation).
JavaScript is a single-threaded programming language, which means it can only execute one thing at a time. JavaScript keeps track of what functions are running by using the Call Stack.
EventListeners are Web API's. Meaning, when the call stack reaches an eventListener it passes the contained function to the browser - as the browser knows when the event occurs.
Then, when the event does occur - the browser moves the listener function to the 'Queue'. JS has a run-til-completion rule so when the call stack has code running it is run til
its completed. Then - any pending listener functions that are waiting to be run in the Queue are run. Note - that the call stack must first be empty before the Queue is even checked.
1) current synchronous code runs to completion, and
2) events are processed when the browser isn't busy. Asynchronous code (such as loading an image) runs outside of this loop and sends an event when it is done.
Similarly to .addEventListener() code being run at some later point, there is the setTimeout() function that will run code at a point later in time. The setTimeout() function takes:
- a function to run at some later time
- the number of milliseconds the code should wait before running the function
e.g: setTimeout(function sayHi() {
console.log('Howdy');
}, 1000);
//the function will be passed to a browser, that will start a timer, and when it expires (1 second later) the function will move to Queue to be executed once call stack is empty.
//this means you can use setTimeout with a delay of 0ms to pass a function to queue, so it runs after the call stack.
Because JS is run to completion - you may have a massive task that needs to be done that will prevent user interaction until the code has run - since user interaction depends on eventListeners
in the Queue which have to wait for the call stack to clear. To fix this - you can divide up your task and run it via setTimeout (0 delay) incrementally - thereby each iteration of your
task will be added to the queue and this will give Queue tasks a change to run. Example:
let count = 1
function generateParagraphs() {
const fragment = document.createDocumentFragment();
for (let i = 1; i <= 500; i++) {
const newElement = document.createElement('p');
newElement.textContent = 'This is paragraph number ' + count;
count = count + 1;
fragment.appendChild(newElement);
}
document.body.appendChild(fragment);
if (count < 20000) {
setTimeout(generateParagraphs, 0);
}
}
generateParagraphs();
This code starts off by setting a count variable to 1. This will keep track of the number of paragraphs that have been added. The generateParagraphs() function will add 500 paragraphs to the
page each time it's invoked. The interesting thing is that there's a setTimeout() call at the end of the generateParagraphs() function. If there are less than twenty thousand elements,
then the setTimeout() will be used to call the generateParagraphs() function.
If you try running this code on a page, you can still interact with the page while the code is running. It doesn't lock up or freeze the page. And it doesn't lock up or freeze because
of the setTimeout() calls.