Hướng dẫn chuyên sâu về JavaScript Runtime
Lời nói đầu
Bạn đã học cú pháp cơ bản của JavaScript, nhưng bạn có từng tự hỏi:
- Code thực sự chạy ở đâu?
- Tại sao cùng một đoạn code lại hoạt động khác nhau trong trình duyệt và Node.js?
- Tại sao đôi khi code bị "treo", nhưng đôi khi lại có thể chạy "song song"?
Bài viết này sẽ đưa bạn đi sâu vào môi trường runtime của JavaScript, bao gồm event loop, call stack, quản lý bộ nhớ, v.v. Sau khi đọc xong, bạn sẽ hiểu được tại sao code lại thực thi theo một thứ tự nhất định, nhanh chóng xác định lỗi liên quan đến bất đồng bộ, tối ưu hiệu suất code và tránh rò rỉ bộ nhớ.
Bài viết này sẽ dạy bạn những gì?
| Chương | Nội dung | Sau khi học có thể làm gì |
|---|---|---|
| Chương 1 | Tổng quan về runtime | Hiểu code JavaScript chạy ở đâu |
| Chương 2 | Runtime trong trình duyệt | Biết trình duyệt cung cấp những Web API nào |
| Chương 3 | Runtime Node.js | Hiểu môi trường JavaScript phía server |
| Chương 4 | Event loop chuyên sâu | Nắm vững thứ tự thực thi của macrotask và microtask |
| Chương 5 | Call stack và bộ nhớ | Hiểu quá trình thực thi code và quản lý bộ nhớ |
| Chương 6 | Kỹ thuật thực chiến | Tối ưu hiệu suất, debug rò rỉ bộ nhớ |
1. Tổng quan về runtime
🤔 Câu hỏi cốt lõi
"Runtime" là gì? JavaScript chỉ là một ngôn ngữ, tại sao cùng một đoạn code lại có hành vi khác nhau trong các môi trường khác nhau?
1.1 Runtime là gì
Runtime = JavaScript Engine + API do môi trường cung cấp
Nếu ví JavaScript như "ngôn ngữ lập trình", thì runtime chính là "hệ điều hành" — nó quyết định code của bạn có thể làm gì và không thể làm gì.
┌─────────────────────────────────────┐
│ Mã JavaScript │
├─────────────────────────────────────┤
│ JavaScript Engine (V8) │ ← Chịu trách nhiệm phân tích và thực thi code
├─────────────────────────────────────┤
│ Môi trường Runtime (Trình duyệt/Node.js) │ ← Cung cấp khả năng bổ sung
└─────────────────────────────────────┘Một phép so sánh: JavaScript là "tiếng phổ thông", runtime là "thành phố"
- Cú pháp JavaScript (tiếng phổ thông) giống nhau ở mọi nơi
- Nhưng các thành phố khác nhau cung cấp cơ sở vật chất khác nhau:
- Trình duyệt = có DOM, window, fetch (giống như thành phố có trung tâm thương mại, thư viện)
- Node.js = có fs, http, path (giống như thành phố có nhà máy, đường cao tốc)
1.2 Hai runtime chính
| Đặc điểm | Trình duyệt | Node.js |
|---|---|---|
| Mục đích chính | Tương tác web, giao diện người dùng | Ứng dụng phía server, công cụ dòng lệnh |
| Đối tượng toàn cục | window | global |
| DOM API | ✅ Hỗ trợ | ❌ Không hỗ trợ |
| Hệ thống tệp | ❌ Bị hạn chế | ✅ Hỗ trợ đầy đủ |
| Hệ thống module | ES Modules | CommonJS + ES Modules |
| Timer | setTimeout, setInterval | setTimeout, setInterval |
| Network request | fetch, XMLHttpRequest | Module http, https |
👇 Thử tương tác: So sánh sự khác biệt môi trường giữa trình duyệt và Node.js
运行时环境对比
浏览器环境
- ✅ 有 DOM 和 BOM API,可以操作网页
- ✅ 有 Web Storage (localStorage, sessionStorage)
- ✅ 有 fetch 和 XMLHttpRequest 进行网络请求
- ❌ 没有文件系统访问权限
- ❌ 不能直接创建 HTTP 服务器
代码演示:不同环境的差异
核心区别:
浏览器运行时专注于用户界面和网页交互,提供 DOM、BOM、fetch 等前端专用 API。
Node.js 运行时专注于服务器端开发,提供文件系统、HTTP 服务器、进程管理等后端专用 API。
同样的 JavaScript 语法,但能用的 API 完全不同——这就是"环境判断"的重要性。
💡 Gợi ý cốt lõi
Runtime quyết định bạn có thể dùng API nào. DOM API dùng được trong trình duyệt thì không dùng được trong Node.js; File API dùng được trong Node.js thì cũng không dùng được trong trình duyệt. Đây chính là lý do tại sao một số code cần "kiểm tra môi trường".
2. Runtime trong trình duyệt
🤔 Câu hỏi cốt lõi
Trình duyệt cung cấp những khả năng gì để JavaScript thao tác với trang web?
2.1 Thành phần của runtime trình duyệt
┌─────────────────────────────────────────────┐
│ JavaScript Engine │
│ (V8 / SpiderMonkey) │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ Web APIs │
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ │
│ │ DOM │ │ BOM │ │ Network │ │
│ │ Thao tác │ │Thao tác │ │Network │ │
│ │trang web │ │trình duyệt│ │ request │ │
│ └─────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ Event Loop │
│ Điều phối thực thi code, xử lý sự kiện, │
│ lập lịch tác vụ │
└─────────────────────────────────────────────┘2.2 Ba loại Web APIs chính
1. DOM API - Thao tác nội dung trang web
// Tìm phần tử
const title = document.querySelector('h1')
// Sửa nội dung
title.textContent = 'Tiêu đề mới'
// Thêm style
title.style.color = 'red'2. BOM API - Thao tác trình duyệt
// Điều hướng trang
window.location.href = 'https://example.com'
// Lưu trữ trình duyệt
localStorage.setItem('key', 'value')
// Lịch sử trình duyệt
history.back()3. Network API - Network request
// Gửi HTTP request
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))2.3 Cơ chế sự kiện đặc trưng của trình duyệt
Một trong những tính năng mạnh mẽ nhất của runtime trình duyệt là "hướng sự kiện" — code không cần chạy liên tục, mà chỉ thực thi khi có thao tác của người dùng.
button.addEventListener('click', () => {
console.log('Nút đã được nhấp')
})Các loại sự kiện phổ biến:
| Loại sự kiện | Thời điểm kích hoạt | Tình huống thực tế |
|---|---|---|
click | Nhấp chuột | Tương tác nút bấm |
input | Nội dung ô nhập thay đổi | Tìm kiếm thời gian thực |
scroll | Cuộn trang | Lazy loading |
load | Tài nguyên tải xong | Khởi tạo dữ liệu |
error | Xảy ra lỗi | Xử lý lỗi |
3. Runtime Node.js
🤔 Câu hỏi cốt lõi
JavaScript có thể chạy ở phía server là nhờ vào đâu?
3.1 Thành phần của Node.js
┌─────────────────────────────────────────────┐
│ JavaScript Engine │
│ (V8) │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ Module tích hợp Node.js │
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ │
│ │ fs │ │ http │ │ path │ │
│ │Thao tác │ │ Server │ │ Xử lý │ │
│ │ tệp │ │ mạng │ │ đường dẫn │ │
│ └─────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ Thư viện event loop libuv │
│ Hỗ trợ I/O bất đồng bộ đa nền tảng │
└─────────────────────────────────────────────┘3.2 Khả năng đặc trưng của Node.js
1. Thao tác hệ thống tệp
const fs = require('fs')
// Đọc tệp
fs.readFile('./data.txt', 'utf8', (err, data) => {
if (err) throw err
console.log(data)
})
// Ghi tệp
fs.writeFile('./output.txt', 'Hello', (err) => {
if (err) throw err
console.log('Ghi thành công')
})2. HTTP Server
const http = require('http')
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' })
res.end('<h1>Hello World</h1>')
})
server.listen(3000)3. Hệ thống module
// CommonJS (Mặc định của Node.js)
const fs = require('fs')
module.exports = { myFunction }
// ES Modules (Cách hiện đại)
import fs from 'fs'
export { myFunction }3.3 So sánh trình duyệt vs Node.js
| Đặc điểm | Trình duyệt | Node.js |
|---|---|---|
| Tệp vào | Tệp HTML | Tệp JavaScript |
| Đối tượng toàn cục | window, document | global, process |
| Tải module | Thẻ <script> | require() / import |
| Bảo mật | Môi trường sandbox, bị hạn chế | Có thể truy cập tài nguyên hệ thống |
| Mục đích | Giao diện người dùng | Dịch vụ backend, công cụ |
4. Event loop chuyên sâu
🤔 Câu hỏi cốt lõi
JavaScript là đơn luồng, tại sao lại có thể "không chặn"?
4.1 Event loop là gì
Event loop = "Trung tâm điều phối tác vụ" của JavaScript
JavaScript là đơn luồng, mỗi lần chỉ làm được một việc. Nhưng event loop khiến nó trông như có thể "đồng thời" làm nhiều việc.
Cơ chế cốt lõi:
- Thực thi code đồng bộ (Call stack)
- Xử lý tác vụ bất đồng bộ (Hàng đợi tác vụ)
- Chờ tác vụ mới (Lặp đi lặp lại)
Call stack Hàng đợi tác vụ
┌─────────┐ ┌──────────┐
│ Tác vụ 1│ │Macrotask 1│
│ Tác vụ 2│ ←──────────── │Macrotask 2│
│ Tác vụ 3│ Thực thi xong│Macrotask 3│
└─────────┘ một thì lấy └──────────┘
↓ cái tiếp theo ↑
└────────────────────────┘
Event loop liên tục kiểm tra4.2 Macrotask vs Microtask
Đây là khái niệm dễ nhầm lẫn nhất trong phỏng vấn và phát triển thực tế!
Macrotask:
setTimeout,setInterval- Thao tác I/O
- UI rendering
Microtask:
Promise.thenMutationObserverqueueMicrotask
Thứ tự thực thi: Code đồng bộ → Microtask → Macrotask
👇 Thử tương tác: Quan sát thứ tự thực thi của macrotask và microtask
任务队列:宏任务 vs 微任务
代码示例
调用栈 (正在执行)
微任务队列 Microtask
宏任务队列 Macrotask
输出日志 (执行顺序)
执行顺序规则
核心要点: 微任务优先级高于宏任务。每次执行完一个宏任务后,都会检查并执行所有微任务,然后再执行下一个宏任务。
4.3 Câu hỏi phỏng vấn kinh điển
console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve().then(() => console.log('3'))
console.log('4')
// Output: 1, 4, 3, 2Tại sao lại là thứ tự này?
- Thực thi code đồng bộ:
console.log('1'),console.log('4')→ output 1, 4 - Kiểm tra hàng đợi microtask:
Promise.then→ output 3 - Kiểm tra hàng đợi macrotask:
setTimeout→ output 2
💡 Kỹ thuật thực chiến
- Nếu muốn code thực thi càng sớm càng tốt, dùng microtask (
Promise.then) - Nếu muốn trì hoãn thực thi, dùng macrotask (
setTimeout) - Đừng bao giờ trộn lẫn quá nhiều thao tác bất đồng bộ, nếu không sẽ rơi vào "callback hell"
5. Call stack và bộ nhớ
🤔 Câu hỏi cốt lõi
Code được thực thi như thế nào? Biến được lưu ở đâu? Khi nào bị thu hồi?
5.1 Call stack: "Dấu chân" thực thi hàm
Call stack = "Sổ ghi chép" ghi lại lời gọi hàm
Mỗi lần gọi một hàm, sẽ thêm một bản ghi mới vào stack; hàm thực thi xong, bản ghi bị xóa.
function a() {
b()
}
function b() {
c()
}
function c() {
console.log('Thực thi xong')
}
a()Sự thay đổi của call stack:
Bước 1: Gọi a()
┌─────────┐
│ a │
└─────────┘
Bước 2: a() gọi b()
┌─────────┐
│ b │
│ a │
└─────────┘
Bước 3: b() gọi c()
┌─────────┐
│ c │
│ b │
│ a │
└─────────┘
Bước 4: c() thực thi xong, lần lượt pop ra
┌─────────┐
│ b │
│ a │
└─────────┘👇 Thử tương tác: Quan sát sự thay đổi của call stack
调用栈:函数执行的足迹
代码
调用栈
当前状态:
调用 main()
输出
调用栈工作原理:
- 每次调用函数,就会在栈上"压入"一个新的"栈帧"
- 栈帧记录了函数的执行状态、局部变量等信息
- 函数执行完毕,栈帧就会从栈上"弹出"
- 栈是"后进先出"(LIFO)的数据结构
- 如果递归太深,会导致"栈溢出"错误
调用栈就像一摞盘子:最后放上去的盘子最先被取走。每个函数就是一个盘子,执行完就取走,然后继续执行下面的函数。
5.2 Quản lý bộ nhớ: Rác đi đâu
JavaScript có cơ chế "tự động thu gom rác" — bạn không cần tự giải phóng bộ nhớ, engine sẽ làm việc đó cho bạn.
Nguyên lý thu gom rác: Thuật toán Mark-and-Sweep
- Giai đoạn đánh dấu: Bắt đầu từ "gốc", tìm tất cả các biến có thể truy cập được
- Giai đoạn quét: Biến không được đánh dấu chính là "rác", sẽ bị thu hồi
// Ví dụ về thu gom rác
let obj1 = { name: 'Đối tượng 1' }
let obj2 = { name: 'Đối tượng 2' }
// obj1 được gán lại, đối tượng ban đầu mất tham chiếu
obj1 = null // { name: 'Đối tượng 1' } ban đầu sẽ bị thu hồi
// obj2 vẫn đang được sử dụng, sẽ không bị thu hồi
console.log(obj2.name)👇 Thử tương tác: Quan sát quá trình thu gom rác
垃圾回收机制
对象引用关系
标记-清除算法 (Mark-and-Sweep)
从根对象(Root)开始,遍历所有可达对象,标记为"活动对象"
遍历整个堆内存,回收所有未被标记的对象
清除所有标记位,为下一次垃圾回收做准备
核心要点
- 根对象(Root): 全局变量、栈上的变量等,总是被认为是可达的
- 可达对象: 从根对象出发,通过引用链能访问到的对象
- 垃圾对象: 无法从根对象访问到的对象,会被回收
- 循环引用: 如果两个对象互相引用但都不可达,仍会被回收
实际应用技巧
对象不再使用时,将其设为 null
使用 const/let 代替 var
组件销毁时移除所有监听器
用 DevTools Memory 面板监控
5.3 Rò rỉ bộ nhớ: Hậu quả của việc quên dọn dẹp
Rò rỉ bộ nhớ = Bộ nhớ đáng lẽ phải được giải phóng nhưng không được giải phóng, tích tụ ngày càng nhiều
Nguyên nhân phổ biến:
1. Quá nhiều biến toàn cục
// ❌ Sai: Biến toàn cục không bị thu hồi
globalCache = []
function addItem(item) {
globalCache.push(item)
}2. Không gỡ bỏ event listener
// ❌ Sai: Listener không được gỡ bỏ
button.addEventListener('click', handleClick)
// ✅ Đúng: Gỡ bỏ listener khi không cần
button.removeEventListener('click', handleClick)3. Closure tham chiếu đến đối tượng lớn
// ❌ Sai: Closure luôn tham chiếu đến đối tượng lớn, không bị thu hồi
function createHandler() {
const bigData = new Array(1000000).fill('data')
return function() {
console.log('Đang xử lý')
}
}
const handler = createHandler() // bigData luôn tồn tại trong bộ nhớ👇 Thử tương tác: Quan sát rò rỉ bộ nhớ xảy ra như thế nào
内存泄漏演示
全局变量泄漏
问题:全局变量不会被垃圾回收,会一直占用内存
示例:不断往全局数组添加数据,从不清理
❌ 错误做法
// 全局变量不会被回收
globalCache = []
function addItem() {
globalCache.push(largeData)
}如何避免内存泄漏
- 避免全局变量: 使用 const/let 代替 var,尽量使用局部变量
- 及时清理监听器: 组件销毁时移除所有事件监听
- 释放闭包引用: 不需要时将闭包变量设为 null
- 使用 WeakMap/WeakSet: 自动清理不再被引用的对象
- 定期检查: 用 DevTools Memory 面板检查内存泄漏
💡 Kỹ thuật thực chiến
- Kiểm tra định kỳ: Mở trình duyệt DevTools → Memory → Take Heap Snapshot, xem mức sử dụng bộ nhớ
- Tránh biến toàn cục: Cố gắng dùng
constvàlet, không dùngvar - Dọn dẹp kịp thời: Event listener, timer dùng xong phải gỡ bỏ
- Tham chiếu yếu: Dùng
WeakMapvàWeakSetđể lưu tham chiếu đối tượng
6. Kỹ thuật thực chiến
🤔 Câu hỏi cốt lõi
Làm thế nào để viết code JavaScript hiệu suất cao? Gặp vấn đề thì debug thế nào?
6.1 Kỹ thuật tối ưu hiệu suất
1. Giảm reflow và repaint
// ❌ Sai: Mỗi lần lặp đều kích hoạt reflow
for (let i = 0; i < 1000; i++) {
element.style.top = i + 'px'
}
// ✅ Đúng: Sửa đổi hàng loạt
element.style.transform = `translateY(${position}px)`2. Sử dụng event delegation
// ❌ Sai: Thêm listener cho từng nút
buttons.forEach(btn => {
btn.addEventListener('click', handleClick)
})
// ✅ Đúng: Chỉ thêm một listener cho phần tử cha
container.addEventListener('click', (e) => {
if (e.target.matches('.button')) {
handleClick(e)
}
})3. Debounce và throttle
// Debounce: Thực thi sau khi người dùng ngừng nhập
function debounce(fn, delay) {
let timer
return function(...args) {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}
// Throttle: Giới hạn tần suất thực thi
function throttle(fn, delay) {
let lastTime = 0
return function(...args) {
const now = Date.now()
if (now - lastTime >= delay) {
fn.apply(this, args)
lastTime = now
}
}
}6.2 Kỹ thuật debug
1. Dùng DevTools xem call stack
function a() {
b()
}
function b() {
c()
}
function c() {
debugger // Tạm dừng ở đây, xem call stack
}
a()2. Dùng console.trace() theo dõi đường dẫn thực thi
function trackExecution() {
console.trace('Đường dẫn thực thi')
// Sẽ xuất ra call stack đầy đủ
}3. Dùng Performance phân tích hiệu suất
performance.mark('start')
// Thực thi một số code
for (let i = 0; i < 10000; i++) {
// ...
}
performance.mark('end')
performance.measure('Hiệu suất vòng lặp', 'start', 'end')
const measure = performance.getEntriesByName('Hiệu suất vòng lặp')[0]
console.log(`Thời gian thực thi: ${measure.duration}ms`)6.3 Tra cứu nhanh vấn đề thường gặp
| Vấn đề | Nguyên nhân có thể | Giải pháp |
|---|---|---|
| Dùng nhiều bộ nhớ | Rò rỉ bộ nhớ, cache quá nhiều | Kiểm tra biến toàn cục, gỡ bỏ listener |
| Trang bị giật lag | Tác vụ dài chặn luồng chính | Chia nhỏ tác vụ, dùng Web Workers |
| Sự kiện không kích hoạt | Listener không được bind, phần tử không tồn tại | Kiểm tra thời điểm tải DOM |
| Thứ tự bất đồng bộ sai | Trộn lẫn macrotask và microtask | Thống nhất dùng Promise hoặc async/await |
| Timer không chính xác | Luồng chính bị chặn | Dùng Web Workers hoặc requestAnimationFrame |
Tổng kết
Bây giờ bạn đã có thể hiểu:
- Runtime = Engine + API môi trường, các runtime khác nhau cung cấp khả năng khác nhau
- Event loop chịu trách nhiệm điều phối thứ tự thực thi của code đồng bộ, microtask, macrotask
- Call stack ghi lại quá trình thực thi hàm, stack overflow là do đệ quy quá sâu
- Thu gom rác tự động dọn dẹp biến không dùng, nhưng cần chú ý rò rỉ bộ nhớ
- Chìa khóa của tối ưu hiệu suất là giảm reflow/repaint, sử dụng bất đồng bộ hợp lý
💡 Khi gặp vấn đề hãy nói với AI như thế này
- "Hàm này chạy quá chậm, giúp tôi xem cách tối ưu hiệu suất"
- "Bộ nhớ liên tục tăng, có thể là rò rỉ bộ nhớ, giúp tôi kiểm tra"
- "Thứ tự thao tác bất đồng bộ không đúng, lẽ ra phải A trước rồi B, hiện tại A và B gần như bắt đầu cùng lúc"
- "Event listener không kích hoạt, kiểm tra xem phần tử đã được tải vào DOM chưa"