Video:
JavaScript Enjoys Your Tears
This is a talk I've given a few times on the Synchronous and Asynchronous behavior of JavaScript. The actual talk is a semi-live-coded talk. It has made me nervous leaving the actual code out there for people to play with after the talk, so this post was created.
Single-Threaded and Asynchronous JavaScript?
Spoiler ...
At it's core, JavaScript is a synchronous, blocking, single-threaded language. This means that only one thing can happen at a time.
When people say that JavaScript is an asynchronous language, what they mean is that you can manipulate JavaScript to behave asynchronously.
Boundaries of Asynchronicity
- User Interactions
- Network IO
- Disk IO
- Inter-Process Communications
- Timers
Definitions
Parallelism: multi-thread processing and scheduling (same time).
Asynchronous: single-thread and event loop.
Managed by ...
Concurrency: higher-level tasks that can occur within the same time frame.
Contention: multiple things need to occur at the same instant.
JavaScript Engine Details
These are not a part of the JS Engine; they are included in Browser or NodeJS runtime environment:
- setTimeout
- Event Loop
- Web APIs
- Message Queue
- Task Queue
Here's a great visualization of "JavaScript's call stack/event loop/callback queue (and how they) interact with each other": Loupe.
setTimeout (4ms delay); see this article on MDN setTimeout.
In modern browsers, setTimeout()/setInterval() calls are throttled to a minimum of once every 4ms when successive calls are triggered due to callback nesting (where the nesting level is at least a certain depth), or after certain number of successive intervals. - MDN
Callback Hell
"Pyramid of Doom":
- Symptom of the problems, not the real issue.
Inversion of Control:
- TRUST POINT when callback is passed ... does it behave as expected.
REASONable:
- Temporal Dependency requires nesting; non-linear thinking.
Considerations ...
Coordination of Data:
- At higher level to track data across callbacks.
Split Callbacks:
- Separating out success and errors.
Error First:
- Same trust issues as Split Callback.
Promise Hell ...
- Flow control with bad style.
- Nested promises rather than vertical chaining.
What will the following code do?
export class ThoughtExercise {
_time = 10000;
_wrapper = null
constructor() {};
changeColor = () => {
this._wrapper = document.getElementById('thought-wrapper');
this._wrapper.style.backgroundColor = 'red';
};
changeLayout = () => {
let p = document.createElement('p');
p.setAttribute('id', 'thought-run');
p.innerText = 'Thought Exercise ...';
this._wrapper.appendChild(p);
};
wait = () => {
const start = Date.now();
while(Date.now() < start + this._time) {};
};
event = () => {
this.changeColor();
this.changeLayout();
this.wait();
};
start = () => {
const button = document.getElementById('thought-button');
button.classList.remove('hide');
button.addEventListener('click', this.event);
};
}
Answer
This code essentially wires up a button, that when click fires the changeColor, changeLayout, and wait functions.
When the button is clicked, this code will be thread-locked until this._time has passed. The background color will not be changed until 10,000ms has passed.
Non-Asynchronous Code
Given the following code ...
export class NonAsynchronous {
_numbers = [1, 2, 3];
constructor() {};
forEachSync = (items, callback) => {
for (const item of items) {
callback(item);
}
};
forEachAsync = (items, callback) => {
for (const item of items) {
setTimeout(() => {
callback(item);
}, 0, item);
}
};
runSync = () => {
console.log('The Start');
this.forEachSync(this._numbers, (number) => {
console.log(number * 2);
});
console.log('The End');
};
runAsync = () => {
console.log('The Start');
this.forEachAsync(this._numbers, (number) => {
console.log(number * 2);
});
console.log('The End');
};
start = (async = false) => {
if (!async) {
this.runSync();
} else {
this.runAsync();
}
}
}
Basically, there are two different **starts that can occur here: Non-Asynchronous and Asynchronous; each has a run option (runSync* and runAsync respectively). Each run as an associated forEach functionality.
When this.runSync fires, we should see the following in the console ...
When ***this.runAsync fires, we should see the following ...
Note the differences here. When running synchronously, everything occurs in the order we expect. When running asynchronously, the numbers consoled show up outside the flow of normal JavaScript execution.
Simple Network (simulation)
Callbacks
- Seams that rip across the application; some bootstrapping may not be complete when needed.
Issues dealing with errors in logic.
- Difficult to understand: Nesting, never called, called repeatedly, called synchronously (blocking)
Simple network, simulated by 10 second timeout.
export class SimpleNetwork {
_time = 10000;
constructor() {};
networkRequest = () => {
setTimeout(() => {
console.log(`Async Code after ${this._time}ms.`);
}, this._time);
};
start = () => {
console.log('The Start');
this.networkRequest();
console.log('The End');
};
};
Within this code, we are basically simulating a network request; using setTimeout to provide a ten-second delay. We should see that the network request completes outside the flow of normal JavaScript execution ...
What is seen when this code is run is that the last line actually displays after a ten-second delay.
Complex Network (simulation)
Complex network, simulated by nested timeouts.
export class ComplexNetwork {
_time = 0;
constructor() {};
first = () => {
setTimeout(() => {
console.log('2');
this.second();
console.log('4');
}, this._time);
};
second = () => {
setTimeout(() => {
console.log('3');
}, this._time);
};
start = () => {
console.log('1');
this.first();
console.log('5');
};
}
What we should expect when examining this simulation is that the order should be: 1, 5, 2, 4, 3 (because of the order of the setTimeout / asynchronous operations) ...
ES2015 Simple Promise
Completion and Error Events handle inversion of control issue.
Promise Trust
- Only resolve once
- Either success or error
- Messages passed/kept
- Exceptions become errors
- Immutable once resolved
Simple async with timeout and promise. Issues:
- Passing values
- Nested syntax
- Handling failure ...
Also called: Promises, future, deferred.
export class SimplePromise {
_time = 0;
constructor() {}
timeout = () => {
setTimeout(() => {
console.log('setTimeout Fired');
}, this._time);
};
promise = () => {
new Promise((resolve, reject) => {
resolve('Resolved');
})
.then(res => console.log(res))
.catch(err => console.log(err));
};
start = () => {
console.log('The Start');
this.timeout();
this.promise();
console.log('The End');
};
}
The code here should fire 'The Start', then trigger the timeout and promise functions and finally 'The End'. The order of operations in this case should be 'The Start' and 'The End' should be displayed. Since the promise is immediately resolved and has nothing takes it outside the flow of normal JavaScript execution, it should display next. And finally, the timeout function will display.
ES2015 Complex Promise
Complex async with timeout and chained promises.
- Modular and readable, but slightly wonky.
export class ComplexPromise {
_time = 0;
constructor() {}
timeout = () => {
setTimeout(() => {
console.log('setTimeout Fired');
}, this._time);
};
promise1 = () => {
return new Promise((resolve, reject) => {
resolve('Resolved 1');
})
.then(res => console.log(res))
.catch(err => console.log(err));
};
promise2 = () => {
return new Promise((resolve, reject) => {
resolve('Resolved 2');
})
.then(res => {
console.log(res);
this.promise3();
})
.catch(err => console.log(err));
};
promise3 = () => {
new Promise((resolve, reject) => {
resolve('Resolved 3');
})
.then(res => console.log(res))
.catch(err => console.log(err));
};
start = () => {
console.log('The Start');
this.timeout();
this.promise1();
this.promise2();
console.log('The End');
};
};
Here, we see something similar to the simple promise. The biggest difference is the chained promise 2 and 3. Here, we should see the same as the simple promise example with all the promises completing before the timeout runs ...
Generator Throttling
Cooperative Concurrency versus Preemptive Concurrency.
- Syntactic form of a state-machine.
- About solving the "reasoning about" issue.
- Allow non run-to-completion behavior. Localized blocking only.
- Generators return an iterator.
export function * throttle(func, time) {
let timerID = null;
function throttled(arg) {
clearTimeout(timerID);
timerID = setTimeout(func.bind(window, arg), time);
}
while(true) throttled(yield);
}
export class GeneratorThrottle {
constructor() {};
start = () => {
thr = throttle(console.log, 3000);
thr.next('');
};
};
Here, when the generator is started, thr is initialized to run console.log after three-seconds.
Now, we can see that after initialization, the next function was called three times ... but only one console was fired at the end of the three-second window.
User Interaction
export class UserInteraction {
constructor() {};
dragStart = (event) => {
event.dataTransfer.setData('text/plain', event.target.id);
console.log('drag start', event);
};
dragOver = (event) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
console.log({ x: event.pageX, y: event.pageY });
};
drop = (event) => {
const id = event.dataTransfer.getData('text');
console.log('drop', id);
const element = document.getElementById('drag');
event.target.appendChild(element);
};
}
Basically, this code allows us to see that the drag and drop events do not thread lock JavaScript.
Event Listeners
Event Listeners are Synchronous (not async)
export class EventListeners {
_btn = null;
_time = 100;
constructor() {};
output = (content) => {
console.log(content);
};
setupListeners = () => {
this._btn.addEventListener('click', this.output.bind(null, 'Click Handler 1'));
this._btn.addEventListener('click', this.output.bind(null,'Click Handler 2'));
};
triggerListeners = () => {
setTimeout(() => {
console.log('The Start');
this._btn.click();
console.log('The End');
}, this._time);
};
start = () => {
this._btn = document.getElementById('event-listener-link');
this.setupListeners();
this.triggerListeners();
};
}
We should see that the click events both fire, in order ...
Web Workers
Number of Workers
- The number varies from browser to browser. Optimal seems to be around 20. See (StackOverflow on Number of Web Workers Limit)[https://stackoverflow.com/questions/13574158/number-of-web-workers-limit].
Here is crunch-numbers.js, used as a web worker ...
onmessage = function() {
for (let step = 0, len = 10; step <= len; step++) {
postMessage(step * 10);
const start = Date.now();
while (Date.now() < start + 1000) {};
}
}
This is the code that uses (or not) the web worker code ...
export class WebWorkers {
_worker = new Worker('scripts/presentation/crunch-numbers.js');
_inlineProgress = null;
_workerProgress = null;
contructor() {};
crunchNumbersInline = (callback) => {
for (let step = 0, len = 10; step <= len; step++) {
callback(step * 10);
const start = Date.now();
while (Date.now() < start + 1000) {};
}
};
displayPercentInline = (percent) => {
console.log(`inline percent: ${percent}`);
this._inlineProgress.value = percent;
};
displayPercent = (message) => {
console.log(`web-worker percent: ${message.data}`);
this._workerProgress.value = message.data;
}
runSync = () => {
this._inlineProgress = document.getElementById('inline-worker');
this.crunchNumbersInline(this.displayPercentInline);
};
runAsync = () => {
this._workerProgress = document.getElementById('web-worker');
this._worker.postMessage('start');
this._worker.onmessage = this.displayPercent;
};
start = (async = false) => {
if (!async) {
this.runSync();
} else {
this.runAsync();
}
};
}
What happens here is difficult to see without the associated HTML page running. What this shows is that the inline process gets thread-locked and the percent display does nothing until the time expires, then it shows 100% in a single "jump."
In the case of the web-worker, each 10% increment is displayed properly without JavaScript getting thread-locked.
Load Timing
Original Content (expect this to change via code)
Changed Content Correctly (via code)
The display above it the result of ...
class LoadTiming {
_time = 10000;
constructor() {};
loadSync = () => {
const element = document.getElementById('first-timing');
if (element) {
element.innerHTML = 'Changed Content Correctly (via code)';
}
};
loadAsync = () => {
setTimeout(() => {
const element = document.getElementById('second-timing');
if (element) {
element.innerHTML = 'Changed Content Correctly (via code)';
}
}, this._time);
};
start = () => {
this.loadSync();
this.loadAsync();
};
}
const code11 = new LoadTiming();
code11.start();
As you can see, the code above loads the Synchronous and Asynchronous code immediately. Since the JavaScript here is loaded in the HEAD content, it runs before the BODY content (DOM) is in place and the Synchronous functionality fails silently on the getElementById. The Asynchronous version has enough of a delay in place to ensure the DOM is ready and it can update the content as seen in the code.
Set Timeout Timer
In this code, we want to look at how long the setTimeout delay actually is.
How long is a setTimeout delay?
- (Careful with delay tested ... 1000 iterations)
export class SetTimeoutTimer {
_repetitions = 0;
_totalRepetitions = 1000;
_delay = 0;
_totalActualDelay = 0;
constructor() {};
getActualDelay = () => {
return this._totalActualDelay / this._totalRepetitions;
};
iterate = () => {
let start = new Date();
setTimeout(() => {
this._totalActualDelay += new Date() - start;
this.testDelay();
}, this._delay);
};
testDelay = () => {
if (this._repetitions++ > this._totalRepetitions) {
console.log(`Requested Delay: ${this._delay}, Acual Average Delay: ${this.getActualDelay()}`);
return;
}
this.iterate();
};
start = (delay = 0) => {
this._delay = delay;
this._repetitions = 0;
this._totalActualDelay = 0;
this.testDelay();
};
}
The answer here is not 42. It is generally 4ms as a default for setTimeout. I've seen variation on different machines and browsers from 4ms to around 8ms ... also, as you can see here it is actually not a round number (does not fire AT 4ms, just some time after that when JavaScript can handle it).
ES2017 Async/Await
- Expands on use of Promises.
- Writing asynchronous code that looks and feels synchronous.
- Cleans up the syntax, making it more readable.
export class AsyncAwait {
_time = 2000;
_resolve = true;
_success = `Doing something here ... after ${this._time}ms.`;
_fail = `Failed here ... after ${this._time}ms.`;
constructor() {};
asyncProcess = () => {
return new Promise((resolve, reject) => {
setTimeout(() => { (this._resolve === true) ? resolve(this._success) : reject(this._fail); }, this._time);
});
};
asyncAwait = async () => {
try {
console.log(await this.asyncProcess());
} catch (error) {
console.log(error);
}
};
start = (resolveState = true) => {
this._resolve = resolveState;
console.log('The Start');
this.asyncAwait();
console.log('The End');
};
}
Basically, when this code starts it runs an async/await version of the promise. I was actually asked in a talk how it handled REJECT from the promise and I had to look it up (try/catch block).
Here is the async/await that resolves correctly ...
... and the same code with reject ...
Summary
We've examined:
- Sync and Async code using callbacks. ... debugging
- ES2015 Promise(s) Chains.
- Generators (throttling)
- User Interaction.
- Event Listeners (synchronous).
- Web Workers.
- Load Timing.
- ES2017 Async/Await.
Conclusion
All of this comes from a talk I've given a few times on the Synchronous and Asynchronous behavior of JavaScript. The actual talk is a semi-live-coded talk. It has made me nervous leaving the actual code out there for people to play with after the talk, so this post was created.
Top comments (0)