🔧
JavaScript
Frontend Web DevLanguages
  • JavaScript
Powered by GitBook
On this page
  • Work Distribution and Scheduling
  • Data Structures
  • Arrays
  • Object Literals
  • Classless Objects
  • Classes
  • Maps
  • Sets
  • Special Structures
  • Iterators
  • Generators
  • Iterables
  • Expressions
  • Destructured Assignment
  • Rest Captures
  • Value Spreading
  • Asynchronous Programming
  • Callbacks
  • Promises
  • Async/Await
  • Fetch
  • Web Workers
  • Service Workers
  • Lifecycle
  • Advanced
  • Intercepting Network Requests
  • Cache API

JavaScript

Basics: Hoisting, Scope, Prototype-based scope lookups

Arrow functions - Anonymous functions with shorter syntax that provide implicit 'this' keyword binding to where the function was defined, and not to the default scope of where the function was called.

Class functions - for Object Constructors.

Work Distribution and Scheduling

Though JS engines are single-threaded, concurrency is possible in:

  • Browsers: Web Workers which execute in separate contexts, get OS-level threads, and communicate with an application via message passing.

  • Node: Many core APIs run on separate threads from a shared pool. One can also start additional node instances within a cluster, as separate processes but with no shared memory.

Otherwise work is delegated via a cooperative multitasking system where tasks are run-to-completion, and async tasks are placed aside for continued processing later when ready. This is also known as "the event loop", and is similar to the Python's asyncio library.

The implication for browser performance is that with synchronous scheduling, any long running tasks will block subsequent tasks. This becomes a UX problem when it affects scroll/input events, animations, and anything else important in line.

The main advice is to prioritize/schedule JS tasks and don't block the loop. When UIs are completely created/managed from JS during runtime, one only has control over scheduling individual script bundles in the HTML through source order and tag properties async, preload, and prefetch.

To provide more fine-grained control between developers and the browser JS engine, both browsers vendors and UI frameworks are incorporating cooperative scheduling architectures as intermediaries.

React redesigned their reconciliation algorithm (concierge) to allow better task scheduling, prioritization, and execution. For startup, it once needed to fetch all components in a file and load them before it started building an instance tree for the DOM and triggering an initial render. More components would proportionally increase first render. Now with React's Fiber architecture, it can progressively build the instance tree as components stream in. Through code splitting and new API functions, React developers can:

  • Pre-render content without DOM updates

  • Lazy load components

  • Defer rendering part of one's application tree with conditional logic

  • Provide interim fallback UI for longer loading/rendering components

  • Prioritize updates, such as user-initiated events over network events

  • Interrupt or abort running lower priority tasks with higher ones

  • Low priority component rendering offscreen, then displaying when data is loaded. Example: Tabbed content, modals, list components with view windows

Data Structures

Arrays

TODO

Object Literals

Key-Value structures with the following properties:

  • Keys are Strings or Symbols, and can be given or computed.

  • Keys are unordered.

  • Objects can be indirectly iterated and sized through 'entries' and 'keys' methods.

  • Objects have by default the 'Object' constructor function as their prototype, which can be changed by assigning a value to the '__proto__' key.

  • Keys can be getter or setter functions for other keys.

  • Keys values can be indirectly assigned through variables in the outer scope with the same key name.

  • Keys with function values can be directly assigned as 'name() {}'.

  • Like in classes, 'super' can be used to address the prototype chain scope.

Classless Objects

TODO

Classes

TODO

Maps

Key-Value structures like Objects, but with the following properties:

  • Keys can be any value including objects, functions, and primitives.

  • Keys are ordered by original insertion order.

  • Maps can be directly iterated and sized.

  • Maps only prototype is the Map constructor function.

  • Keys belong only to them.

There are also WeakMaps that weakly hold on to referenced values, and probably best explained by JS documentation.

Sets

TODO

Special Structures

Iterators

Constructed objects around an internal state of sequentially produced values. The series can be finite or infinite. The iterator object has a 'next' method that:

  • Calculates the first/subsequent items in the collection, tracking current position.

  • Returns an object with two keys: value (corresponding value), and done: (boolean).

Iterators follow a protocol according to the above guidelines, though implementation may be customized depending on what 'Iterable' created the iterator. Custom iterators including generators, allow you to pass arguments during initialization and in 'next' calls.

Generators

Special functions that return iterators, but without an internal state tracking iteration position. Here's how it works:

  • Write a function declaration, with an asterisk before the function's name,

  • with or without parameters,

  • that runs from top to bottom, adding 'yield' statements wherever you want to pause execution,

  • immediately followed by a value you want to return whenever the 'next' method is called.

Generators allow flexibility to control the operation's sequential flow and decide what values are returned during each iteration. The yield statement pauses execution, regardless whether it's placed in the middle of an expression or separately on its own line.

One can also pass arguments to 'next', which become return values to the generator when it continues execution. So if there was a variable being assigned to, or a function being called with a yield parameter, the return values get used for the remaining execution of that expression. But if there's nothing to do with values passed to 'next', like a yield statement with nothing else to perform, then those values passed are discarded.

Using 'yield*' followed by an iterable, will then run through and yield every value.

Lastly, Generator objects iteration values can be used with for..of loops.

Iterables

Objects you can ask for an Iterator and which allow iteration using 'for..of' loops. JS has a few types with built-in iterables including String, Array, Map, and Set. Technically, an object is an Iterable if:

  • It (or its prototype) has a property with a '[Symbol.iterator]' key,

  • whose value is the '@@iterator' method,

  • which when called will return an iterator object.

Calling this will return an iterator object: [1,2,3][Symbol.iterator]() Which then if called next(), will return: {value: 1, done: false}

For custom iterables, create an object with a '[Symbol.iterator]' key whose value is either:

  • a function that follows the iterator protocol, or

  • a generator function

Iterables can also be destructured like arrays and objects, with all sequentially yielded values.

Expressions

Destructured Assignment

Probably the closest JS comes to assigning copied values to variables through pattern matching. The approach is similar to as its done in functional languages. Entails declaring the variable types and names on the left side of the assignment, followed by its value on the right. If the data shapes on the left and right correspond, the declared variables are initialized accordingly.

Rest Captures

Another form of destructured assignment that assigns a variable on the left, the remaining copied values of an object or array on the right side of an expression.

Value Spreading

Another form of destructuring that unpacks values inside iterable objects including strings, arrays, and objects. This is the opposite of rest capturing. It can be used within arrays, objects, and as the last parameter to a function.

Asynchronous Programming

TODO: Paraphrase

Callbacks

A function called by node when another piece of work is completed, used to sequentially organize asynchronous operations. Function parameters are results from previous operations, with the first parameter representing any error results, followed by one or more parameters representing success results. Standard callbacks are useful for short operation sequences.

Promises

Means of representing eventual values. They're constructor functions that take an executor function with two function parameters: resolve (success) and reject(failure). Promises Immediately return from the initial callback function and provide a proxy value when the result is not yet known.

"We can listen for the eventual value by passing a callback to the 'then' function." Called a 'Thenable'. 'Then' functions provided for when the previous function completes.

"The then function returns a new promise, that resolves to whatever the callback returns. This allows us to chain multiple then functions together. If the callbacks return a promise, the resolved value is passed down the chain (not the promise itself)."

Errors handled in two ways: 1) Listening using the catch function, for thrown errors and rejections 2) Passing errors to a next 'then' function

Catch functions can be placed anywhere in a promise chain, and will only receive errors previous to it. This can be helpful if alternative plans are needed if errors arise.

To run multiple promises in parallel, and run a function when they're all complete, use Promise.all. 'then' is called if all resolve successfully, and 'catch' is called immediately if one or more promises reject.

The finally function is a final function that would be called in case either successful or error scenarios.

Async/Await

With longer sequences, Promises improve code readability and the logical flow of failure/success conditions. However when we want more conditional logic, assigning async computed values to variables, and more error handling, async functions provide greater flexibility by allowing us to write code that appears synchronous. And to better control errors, we can put our asynchronous calls within try/catch clauses.

Fetch

Way to send or receive data asynchronously using a promise-based API. Can be using async functions, awaiting returned data. TODO "The promises returned by a Fetch request will NOT reject if the response has a 400- or 500-level status code. It only rejects on network failure. (This is actually a good thing for us today). By default, Fetch does not send or receive any cookies."

When browsers load resources normally in HTML, they use the same mechanisms as fetch under the hood.

Fetch supports getting JSON, XML, media, script files, etc. Data (except for JSON/XML) is returned as a stream, and because there's multiple data types possibly received, we need to tell fetch how to handle the data.

(Cross Origin Resource Sharing) "rules apply, but you can request things like images that you don't plan on reading outright. This applies to images, audio, video, scripts. Rule of thumb: anything you might pass into the src attribute of an HTML element. To do so, set an options object as the 2nd parameter to fetch, with a key 'mode: 'no-cors''.

We can also build requests using Request constructor functions, which are passed resource URLs and an options object. This request is then passed into fetch.

Notes: "You can only read the body of a Request or a Response object once. You can however, clone the object if you think you'll need to read it again."

Web Workers

TODO: Paraphrase

"Parallel, not just concurrent. Each is its own independent script. Can send messages back to its creator. Must obey same-origin policy."

Worker constructor function. Dedicated Workers - "can only communicate with the 'script' (frame) that spawned them. Multiple JS files are still one 'script' Shared Workers - "can communicate between multiple frames on the same origin. Multiple tabs/windows/frames"

Limitations and Features Anything synchronous is verboten. Messaging to the main application thread is necessary. No DOM/cookies/localStorage

Limited location and navigator objects

Available setInterval and setTimeout (and respective clears*) fetch and Promise Cache Storage API WebSocket IndexedDB

Terminating Workers "You may have multiple dedicated or shared worker instances, based off of the same script file. They'll each have their own scope, and take up non-trivial resources. You may kill them from the main application using .terminate(). This is a great tools for the 'do something then die' worker pattern. You can't terminate a Service Worker this way"

Service Workers

TODO: Paraphrase

Influenced by the Extensible Web Manifesto in that developers should have low-level access to program properties and behavior based on their app's needs. This is in contrast to the assumption (that browser vendors) decide ALL web apps need particular functionalities, and create all-in-one solutions.

Thankfully we have can inspect and control SWs within their lifecycles.

"Service Workers run on their own thread just like a Web Worker or Shared Worker" "They can intercept network requests' This means you can catch the request if the client is offline and either provide a fallback or use a resource from cache." "SW can run in the background, even when the web application is not open" Exist independent of any tabs.

"Programmable network proxy. JS worker. Only works over secure domains" because of their ability to run in the background, intercept network requests. "Has a predictable lifecycle. Necessary for many app-like capabilities, including 'offline', notifications, background sync and more!"

Implementation: check if 'serviceWorker' in navigator, register it. Installing - "'install' event has fired, but it hasn't completed yet" Installed - "The SW is installed but it's waiting for another SW to be unloaded" Activating - "The 'active' event has fired, but it hasn't completed yet" Active = "The SW is installed and ready to begin handling events" Redundant - "The SW has been replaced by another one"

Event listeners are added to SW listening for install, activate, fetch...

then becomes active and will remain installed until another SW comes to replace it.

Lifecycle

"Make offline-first something that's possible. Allow the new SW to get installed and ready without disturbing the current one. Ensure that there is one and only one active SW.

"Conditions for Replacing a SW" The application won't update until all browser tabs using that SW are closed. You can see waiting service workers in browser dev tools. "If you don't want this in your application for some reason?, you can call 'event.skipWaiting()'. This can be called during the installation event too. "If you only want this in development, then the Chrome Devtools are your friend", using "Update on reload" or use Cmd+Shift+R

Advanced

Event.skipWaiting() should be used with caution though, as this replaces the SW on any other "currently opened instances of your application-- without replacing their client-side code".

When SWs are activated after a previous SW was ousted, there might be active clients now disconnected. They can be claimed by the new SW using 'self.clients.claim()'.

"As part of the registration process, we can inquire about active SW as well as any that are presently installing or waiting in line to supplant the active worker".

We can also add events and listeners to SWs, allowing them to listen/respond appropriately. If older service workers will, or have been replaced (normal or suddenly (skipWaiting)), we can maybe check application conditions first, prompt the user for confirmation, and perform other actions accordingly.

Intercepting Network Requests

"Listening for fetch events will not only give us XHR requests, but all network requests, including images, CSS, HTML, and other JS".

"Working with the FetchEvent" event.request -- the request object event.request.url -- original URL request event.request.method -- HTTP method used event.respondWith -- "allows you to respond with something other than what was being requested"

Cache API

Allows programmatic caching control using a Promise-based API.

So for example if the SW install event fired, we can use event.waitUntil(), passing in the cache function to perform like opening a cache (new/old) and returning the cache with resources added.

Why event.waitUntil() ? Since the "SW can be killed at any time by the browser, event.waitUntil() keeps the 'install' process going until the promise it was handed resolves. This allows us to be assured that by the time the activate event is fired, the preceding install process has completed successfully. If the promise it was given rejects, the SW installation will fail, it will be abandoned, and the currently-active worker --if any-- will remain in charge". This is done if we want to "ensure success, know about failure, and keep the process alive until it's done".

The Cache API is a key-value storage of request and response objects. "Under the hood, it's a Map where Request objects are the keys and Response objects are the values. This means you can store responses and just serve them back if you receive another matching request."

Two major times to setup caches: 1) Install event - new caches only 2) Activate event - remove any caches a retired SW was using

Not just for SWs though. The browser can also access the cache API, allowing users perhaps to "manually add items to the cache". Like "save for offline".

Great use case: Fallback images/resources for assets that might normally be cached and customized to the user, but unavailable at this time, like an avatar.

Last updated 1 year ago