Bỏ qua

JavaScript Event Loop

Khái niệm cơ bản

Event Loop là cơ chế cốt lõi cho phép JavaScript (single-threaded) xử lý các tác vụ bất đồng bộ mà không bị blocking. Đây là "trái tim" của JavaScript runtime, quản lý việc thực thi code, xử lý events, và callbacks.

Tại sao cần Event Loop?

  • JavaScript là single-threaded: Chỉ có một thread chính để thực thi code
  • Tránh blocking UI: Không làm đơ giao diện khi có tác vụ nặng
  • Xử lý bất đồng bộ: Cho phép thực hiện nhiều tác vụ "cùng lúc"
  • Responsive applications: Ứng dụng phản hồi mượt mà
  • Ví dụ trực quan: Event Loop

Kiến trúc JavaScript Runtime

┌─────────────────────────────────────┐
│            Call Stack               │ ← Ngăn xếp thực thi (LIFO)
│         function frames             │   Thực thi code đồng bộ
└─────────────────────────────────────┘
                    │
                    ▼ Event Loop
┌─────────────────────────────────────┐
│            Web APIs                 │ ← Browser/Node.js APIs
│  setTimeout, DOM events, fetch      │   Xử lý bất đồng bộ
└─────────────────────────────────────┘
                    │
                    ▼ Callbacks
┌─────────────────────────────────────┐
│         Microtask Queue             │ ← Độ ưu tiên CAO
│   Promise.then, queueMicrotask      │   Process callbacks
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│        Callback Queue               │ ← Độ ưu tiên THẤP HỐN
│  (Task Queue/Macrotask Queue)       │   setTimeout, DOM events
└─────────────────────────────────────┘

Các thành phần chính

1. Call Stack (Ngăn xếp gọi hàm)

Call Stack là nơi JavaScript thực thi code đồng bộ theo nguyên tắc LIFO.

function first() {
    console.log('First function start');
    second();
    console.log('First function end');
}

function second() {
    console.log('Second function start');
    third();
    console.log('Second function end');
}

function third() {
    console.log('Third function');
}

first();

/* Call Stack execution:
   ┌─────────────┐
   │   third()   │ ← Executing
   ├─────────────┤
   │   second()  │
   ├─────────────┤
   │   first()   │
   ├─────────────┤
   │   global    │
   └─────────────┘

Output:
First function start
Second function start  
Third function
Second function end
First function end
*/

2. Web APIs (Browser APIs)

Web APIs là các API được cung cấp bởi browser hoặc Node.js runtime:

  • Timers: setTimeout, setInterval
  • DOM Events: click, scroll, resize
  • HTTP Requests: fetch, XMLHttpRequest
  • Storage: localStorage, sessionStorage
  • Others: requestAnimationFrame, IntersectionObserver
// Web API được gọi nhưng không block Call Stack
setTimeout(() => {
    console.log('Timer callback');
}, 1000);

fetch('https://api.example.com/data')
    .then(response => response.json())
    .then(data => console.log(data));

document.addEventListener('click', () => {
    console.log('Click handler');
});

console.log('Synchronous code continues...');
// Output ngay lập tức: "Synchronous code continues..."
// Callbacks sẽ chạy sau khi timer/request hoàn thành

3. Callback Queue (Task Queue / Macrotask Queue)

Callback Queue chứa các callbacks từ Web APIs, hoạt động theo FIFO:

console.log('Start');

// Callback sẽ vào Callback Queue
setTimeout(() => console.log('Timeout 1'), 0);
setTimeout(() => console.log('Timeout 2'), 0);
setTimeout(() => console.log('Timeout 3'), 0);

console.log('End');

/* 
Call Stack: Start → End
Callback Queue: [Timeout 1] → [Timeout 2] → [Timeout 3]

Output:
Start
End
Timeout 1
Timeout 2  
Timeout 3
*/

4. Microtask Queue

Microtask Queueđộ ưu tiên cao hơn Callback Queue:

console.log('Start');

// Macrotask
setTimeout(() => console.log('Timeout'), 0);

// Microtasks
Promise.resolve().then(() => console.log('Promise 1'));
Promise.resolve().then(() => console.log('Promise 2'));

console.log('End');

/*
Output:
Start
End
Promise 1    ← Microtasks được ưu tiên
Promise 2    ← Microtasks được ưu tiên  
Timeout      ← Macrotask chạy sau
*/

Quy trình hoạt động của Event Loop

Event Loop hoạt động theo chu kỳ với các bước sau:

Bước 1: Thực thi Call Stack

console.log('1');  // ← Thực thi ngay
console.log('2');  // ← Thực thi ngay

Bước 2: Kiểm tra Microtask Queue

Promise.resolve().then(() => console.log('Microtask')); // ← Ưu tiên cao

Bước 3: Kiểm tra Callback Queue

setTimeout(() => console.log('Macrotask'), 0); // ← Ưu tiên thấp hơn

Bước 4: Render (nếu cần)

Browser có thể render UI updates.

Bước 5: Lặp lại

Event Loop tiếp tục chu kỳ.


Ví dụ chi tiết

Ví dụ 1: Cơ bản

console.log('1');

setTimeout(() => {
    console.log('2');
}, 0);

Promise.resolve().then(() => {
    console.log('3');
});

console.log('4');

/*
Step-by-step execution:

1. console.log('1') → Call Stack → Output: "1"
2. setTimeout() → Web API → Callback vào Callback Queue sau 0ms
3. Promise.resolve().then() → Microtask Queue
4. console.log('4') → Call Stack → Output: "4"
5. Call Stack empty → Check Microtask Queue → Output: "3"
6. Microtask Queue empty → Check Callback Queue → Output: "2"

Final Output: 1, 4, 3, 2
*/

Ví dụ 2: Phức tạp hơn

console.log('Start');

setTimeout(() => {
    console.log('Timeout 1');
    Promise.resolve().then(() => console.log('Promise inside timeout'));
}, 0);

Promise.resolve()
    .then(() => {
        console.log('Promise 1');
        return Promise.resolve();
    })
    .then(() => {
        console.log('Promise 2');
    });

setTimeout(() => {
    console.log('Timeout 2');
}, 0);

console.log('End');

/*
Execution trace:

Call Stack: Start → End
Microtask Queue: [Promise 1] → [Promise 2]
Callback Queue: [Timeout 1] → [Timeout 2]

Output sequence:
Start
End
Promise 1
Promise 2
Timeout 1
Promise inside timeout  ← New microtask created during timeout
Timeout 2
*/

Ví dụ 3: Nested Promises và Timeouts

console.log('Script start');

setTimeout(() => console.log('setTimeout 1'), 0);

Promise.resolve()
    .then(() => {
        console.log('Promise 1');
        setTimeout(() => console.log('setTimeout 2'), 0);
    })
    .then(() => {
        console.log('Promise 2');
    });

setTimeout(() => console.log('setTimeout 3'), 0);

console.log('Script end');

/*
Output:
Script start
Script end
Promise 1
Promise 2
setTimeout 1
setTimeout 3  
setTimeout 2
*/

Microtasks vs Macrotasks

Microtasks (Độ ưu tiên cao)

  • Promise.then/catch/finally
  • queueMicrotask()
  • MutationObserver (trong browser)
  • process.nextTick() (trong Node.js)

Macrotasks (Độ ưu tiên thấp hơn)

  • setTimeout/setInterval
  • setImmediate (Node.js)
  • I/O operations
  • UI events
  • requestAnimationFrame

So sánh:

// Microtask
Promise.resolve().then(() => console.log('Microtask'));
queueMicrotask(() => console.log('queueMicrotask'));

// Macrotask  
setTimeout(() => console.log('Macrotask'), 0);

console.log('Sync');

/*
Output:
Sync
Microtask
queueMicrotask
Macrotask
*/

Ứng dụng thực tế

Vấn đề: Blocking Code

//  SAI - Blocking main thread
function heavyComputation() {
    console.log('Starting heavy computation...');

    // Simulation of heavy work - BLOCKS UI
    for (let i = 0; i < 10000000000; i++) {
        // Heavy computation that blocks the thread
        Math.random();
    }

    console.log('Heavy computation done!');
}

// This will freeze the browser for several seconds
heavyComputation();
console.log('This message will be delayed');

Giải pháp: Non-blocking với Event Loop

//  ĐÚNG - Non-blocking approach
function heavyComputationAsync(callback) {
    console.log('Starting async heavy computation...');

    let i = 0;
    const total = 10000000;
    const chunkSize = 100000;

    function processChunk() {
        let chunkEnd = Math.min(i + chunkSize, total);

        // Process a small chunk
        while (i < chunkEnd) {
            Math.random();
            i++;
        }

        // Update progress
        console.log(`Progress: ${((i / total) * 100).toFixed(1)}%`);

        if (i < total) {
            // Schedule next chunk, giving browser time to breathe
            setTimeout(processChunk, 0);
        } else {
            console.log('Heavy computation done!');
            if (callback) callback();
        }
    }

    // Start processing
    processChunk();
}

// Usage
heavyComputationAsync(() => {
    console.log('Computation finished callback');
});

console.log('This message appears immediately');

Debouncing với Event Loop

function debounce(func, delay) {
    let timeoutId;

    return function (...args) {
        // Clear previous timeout
        clearTimeout(timeoutId);

        // Set new timeout
        timeoutId = setTimeout(() => {
            func.apply(this, args);
        }, delay);
    };
}

// Usage: Search input
const searchInput = document.getElementById('search');
const debouncedSearch = debounce((query) => {
    console.log('Searching for:', query);
    // Actual search API call here
}, 300);

searchInput.addEventListener('input', (e) => {
    debouncedSearch(e.target.value);
});

Throttling với requestAnimationFrame

function throttle(func, limit) {
    let inThrottle;

    return function (...args) {
        if (!inThrottle) {
            func.apply(this, args);
            inThrottle = true;

            setTimeout(() => {
                inThrottle = false;
            }, limit);
        }
    };
}

// Smooth scroll handling
const throttledScrollHandler = throttle(() => {
    console.log('Scroll position:', window.scrollY);
    // Update scroll-based animations
}, 16); // ~60fps

window.addEventListener('scroll', throttledScrollHandler);

Performance Best Practices

1. Tránh Long-running Tasks

//  Bad - Long running task
function badDataProcessing(data) {
    for (let item of data) {
        // Complex processing
        heavyOperation(item);
    }
}

//  Good - Break into chunks
async function goodDataProcessing(data) {
    const chunkSize = 100;

    for (let i = 0; i < data.length; i += chunkSize) {
        const chunk = data.slice(i, i + chunkSize);

        chunk.forEach(item => {
            heavyOperation(item);
        });

        // Yield control back to browser
        await new Promise(resolve => setTimeout(resolve, 0));
    }
}

2. Sử dụng Web Workers cho CPU-intensive tasks

// main.js
const worker = new Worker('worker.js');

worker.postMessage({ command: 'start', data: largeDataSet });

worker.onmessage = function(e) {
    if (e.data.type === 'progress') {
        console.log('Progress:', e.data.progress);
    } else if (e.data.type === 'complete') {
        console.log('Result:', e.data.result);
    }
};

// worker.js
self.onmessage = function(e) {
    if (e.data.command === 'start') {
        const result = processLargeData(e.data.data);
        self.postMessage({ type: 'complete', result: result });
    }
};

function processLargeData(data) {
    // Heavy computation without blocking main thread
    return data.map(item => complexCalculation(item));
}

3. Proper Promise Handling

//  Bad - Creating unnecessary microtasks
async function badAsyncFunction() {
    const result1 = await Promise.resolve(1);
    const result2 = await Promise.resolve(2);
    const result3 = await Promise.resolve(3);

    return result1 + result2 + result3;
}

//  Good - Parallel execution
async function goodAsyncFunction() {
    const [result1, result2, result3] = await Promise.all([
        Promise.resolve(1),
        Promise.resolve(2),
        Promise.resolve(3)
    ]);

    return result1 + result2 + result3;
}

Common Pitfalls và Solutions

1. Mixing Sync và Async Code

//  Problem
function mixedFunction() {
    console.log('Start');

    setTimeout(() => {
        console.log('Async operation');
    }, 0);

    return 'Sync result'; // Returns before async operation
}

//  Solution
async function asyncFunction() {
    console.log('Start');

    await new Promise(resolve => {
        setTimeout(() => {
            console.log('Async operation');
            resolve();
        }, 0);
    });

    return 'Result after async operation';
}

2. Event Loop Starvation

//  Problem - Infinite microtasks
function starveEventLoop() {
    Promise.resolve().then(() => {
        console.log('Microtask');
        starveEventLoop(); // Creates infinite loop of microtasks
    });
}

//  Solution - Use setTimeout to break the loop
function fixedFunction() {
    Promise.resolve().then(() => {
        console.log('Microtask');
        setTimeout(() => fixedFunction(), 0); // Gives other tasks a chance
    });
}

Debugging Event Loop

1. Visualizing Call Stack

function traceStack() {
    console.trace('Current call stack');
}

function a() {
    b();
}

function b() {
    c();
}

function c() {
    traceStack();
}

a(); // Shows full call stack

2. Performance Monitoring

// Measure task timing
function measureTask(taskName, task) {
    const start = performance.now();

    Promise.resolve().then(() => {
        task();
        const end = performance.now();
        console.log(`${taskName} took ${end - start}ms`);
    });
}

measureTask('Heavy calculation', () => {
    for (let i = 0; i < 1000000; i++) {
        Math.random();
    }
});

3. Event Loop Monitoring

// Detect long tasks
let lastTime = performance.now();

function checkEventLoop() {
    const currentTime = performance.now();
    const delay = currentTime - lastTime;

    if (delay > 50) { // Task took longer than 50ms
        console.warn(`Long task detected: ${delay}ms`);
    }

    lastTime = currentTime;
    setTimeout(checkEventLoop, 0);
}

checkEventLoop();

Kết luận

Event Loop là backbone của JavaScript asynchronous programming:

Key Takeaways:

  • Single-threaded nhưng non-blocking nhờ Event Loop
  • Microtasks luôn có độ ưu tiên cao hơn Macrotasks
  • Call Stack phải empty trước khi Event Loop xử lý queues
  • Performance phụ thuộc vào việc quản lý tasks đúng cách

Best Practices:

  • Break long tasks thành chunks nhỏ
  • Use Web Workers cho CPU-intensive operations
  • Understand Promise behavior và microtask queue
  • Avoid blocking main thread
  • Monitor performance và detect long tasks

Nhớ rằng: "Understanding Event Loop is understanding JavaScript" - đây là chìa khóa để viết JavaScript hiệu quả và responsive!