Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Understanding ES6 Modules, Asynchronous Programming with Promises, and the Event Loop

Tech 2

Learning Objectives

  1. Understand how to use ES6 module syntax, including default and named exports/imports.
  2. Learn to use Promises to solve callback hell issues.
  3. Simplify Promise calls using async/await.
  4. Explain what the EventLoop is and how it processes asynchronous tasks.
  5. Describe the execution order of macro-tasks and micro-tasks.

Table of Contents

  • ES6 Modules
  • Promises
  • async/await
  • EventLoop
  • Macro-tasks and Micro-tasks

1. ES6 Modules

1.1 Review: Module Implementation in Node.js

Node.js follows the CommonJS module specification:

  • Use require() to import modules.
  • Use module.exports to export members.

Benefits of modularization: Standardization reduces communication costs and facilitates module interaction.

1.2 Frontend Module Categories

Before ES6, the JavaScript community proposed standards like AMD, CMD, and CommonJS, but these had limitations:

  • AMD and CMD were for browser-side JavaScript.
  • CommonJS was for server-side JavaScript.

The variety of standards increased learning and development costs, leading to the unified ES6 module specification.

1.3 ES6 Module Specification

ES6 modules are universal for both browser and server environments, reducing the need to learn multiple standards. Key points:

  • Each JS file is an independent module.
  • Use import to import members.
  • Use export to export members.

1.4 Using ES6 Modules in Node.js

Node.js defaults to CommonJS; to use ES6 modules:

  1. Ensure Node.js version 14.15.1 or higher is installed.
  2. Add "type": "module" to the root of package.json.

1.5 Basic ES6 Module Syntax

ES6 modules support three main usages:

  1. Default export and import.
  2. Named export and import.
  3. Direct import and execution of module code.

(1) Default Export and Import

  • Default export syntax: export default memberToExport
  • Default import syntax: import alias from 'module-identifier'

Notes:

  • Each module can have only one export default.
  • The import alias can be any valid name.

Code example:

// moduleA.js
let privateVar = 10
let hiddenVar = 20
function privateFunc() {}

export default {
    privateVar,
    privateFunc
}

// moduleB.js
import importedModule from './moduleA.js'
console.log(importedModule) // Output: { privateVar: 10, privateFunc: [Function: privateFunc] }

(2) Named Export and Import

  • Named export syntax: export memberToExport
  • Named import syntax: import { member } from 'module-identifier'

Notes:

  • Multiple named exports per module are allowed.
  • Import names must match export names.
  • Use as for renaming during import.
  • Named imports can be combined with default imports.

Code example:

// moduleA.js
export let data1 = 'value1'
export let data2 = 'value2'
export function utility() {}

// moduleB.js
import { data1, utility } from './moduleA.js'
console.log(data1) // Output: value1
console.log(utility) // Output: [Function: utility]

import { data1 as renamedData } from './moduleA.js'

(3) Direct Import for Code Execution

If you only need to execute module code without importing members, use direct import.

Code example:

// moduleA.js
for (let i = 1; i < 3; i++) {
    console.log(i)
}
// No exports

// moduleB.js
import './moduleA.js' // Executes the code in moduleA

2. Promises

2.1 Callback Hell

Callback hell occurs with nested callbacks, leading to:

  • Tight coupling and maintenance difficulties.
  • Reduced code readability due to nesting.

Example:

setTimeout(() => {
    console.log('Output after 1 second')
    setTimeout(() => {
        console.log('Output after 2 more seconds')
        setTimeout(() => {
            console.log('Output after 3 more seconds')
        }, 3000)
    }, 2000)
}, 1000)

2.2 Using Promises to Solve Callback Hell

Promises, introduced in ES6, provide a better way to handle asynchronous operations.

2.2.1 Basic Concepts of Promises

  • A Promise is a constructor: const promiseInstance = new Promise().
  • The .then() method is on Promise.prototype and is used to specify success and failure callbacks: promiseInstance.then(successCallback, failureCallback).
  • Success callback is required; failure callback is optional.

2.2.2 Example: Solving Callback Hell with .then()

Use a third-party package like then-fs for Promise-based file reading in Node.js.

Code example:

import thenFs from 'then-fs'

thenFs.readFile('./files/file1.txt', 'utf8')
    .catch((error) => {
        console.log(error.message)
    })
    .then((content1) => {
        console.log(content1)
        return thenFs.readFile('./files/file2.txt', 'utf8')
    })
    .then((content2) => {
        console.log(content2)
        return thenFs.readFile('./files/file3.txt', 'utf8')
    })
    .then((content3) => {
        console.log(content3)
    })

2.2.3 Parallel Asynchronous Operations with Promise.all() and Promise.race()

  • Promise.all(): Waits for all promises to resolve before proceeding (waiting mechanism).
  • Promise.race(): Proceeds as soon as any promise resolves (racing mechanism).

Code example:

import thenFs from 'then-fs'

const filePromises = [
    thenFs.readFile('./files/file3.txt', 'utf8'),
    thenFs.readFile('./files/file2.txt', 'utf8'),
    thenFs.readFile('./files/file1.txt', 'utf8'),
]

Promise.all(filePromises)
    .then(([result1, result2, result3]) => {
        console.log(result1, result2, result3)
    })
    .catch((error) => {
        console.log(error.message)
    })

Promise.race(filePromises)
    .then(([result1, result2, result3]) => {
        console.log(result1, result2, result3)
    })
    .catch((error) => {
        console.log(error.message)
    })

3. async and await

3.1 Introduction to async and await

Introduced in ES8, async/await simplifies Promise handling, replacing chain .then() calls.

3.2 Basic Usage of async and await

Use await for operations returning a Promise, and mark the containing function with async.

Code example:

import thenFs from 'then-fs'

async function fetchFiles() {
    const data = await thenFs.readFile('./files/file1.txt', 'utf8')
    console.log(data)
}

Notes:

  • Functions using await must be async.
  • In async functions, code before the first await runs synchronously; code after runs asynchronously.

Example illustrating execution order:

import thenFs from 'then-fs'

console.log('A')
async function fetchFiles() {
    console.log('B')
    const data1 = await thenFs.readFile('./files/file1.txt', 'utf8')
    console.log(data1)
    const data2 = await thenFs.readFile('./files/file2.txt', 'utf8')
    console.log(data2)
    console.log('D')
}

fetchFiles()
console.log('C')
// Output order: A C B D

4. EventLoop

4.1 JavaScript as a Single-Threaded Language

JavaScript executes one task at a time, which can cause program freezes with long-running tasks.

4.2 Synchronous and Asynchronous Tasks

Tasks are divided into:

  • Synchronous tasks: Non-time-consuming, executed in order on the main thread (e.g., file reading, data requests).
  • Asynchronous tasks: Time-consuming, delegated to the host enviroment (browser/Node.js), with callbacks placed in a task queue upon completion.

4.3 Understanding the EventLoop

  1. Synchronous tasks execute first on the main thread.
  2. Asynchronous tasks are handled by the host environment; their callbacks go in to the task queue.
  3. After synchronous tasks finish, callbacks from the task queue are executed.
  4. This cycle repeats, forming the EventLoop.

Example:

import thenFs from 'then-fs'

console.log('A') // Synchronous
thenFs.readFile('./file.txt', 'utf8').then((res) => {
    console.log('B') // Asynchronous callback
})

setTimeout(() => {
    console.log('C') // Asynchronous with zero delay
}, 0)
console.log('D') // Synchronous
// Output order: A D C B

5. Macro-tasks and Micro-tasks

5.1 Overview of Macro-tasks and Micro-tasks

Asynchronous tasks are further split into:

  • Macro-tasks: e.g., setTimeout, setInterval, I/O operations.
  • Micro-tasks: e.g., Promise callbacks, process.nextTick.

5.2 Execution Order of Macro-tasks and Micro-tasks

  1. Execute all synchronous tasks.
  2. Execute all micro-tasks.
  3. Execute macro-tasks.
  4. After each macro-task, check for pending micro-tasks and execute them before the next macro-task.

Summary order: script (main code) → micro-tasks (e.g., Promises) → macro-tasks (e.g., setTimeout).

Example diagram illustrating output order 2, 4, 3, 1:

Macro and Micro Task Execution Order

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.