دليل متعمق لوقت تشغيل JavaScript
مقدمة
لقد تعلمت القواعد الأساسية للغة JavaScript، لكن هل تساءلت يومًا:
- أين يتم تشغيل الكود بالضبط؟
- لماذا يتصرف نفس الكود بشكل مختلف في المتصفح وNode.js؟
- لماذا "يعلق" الكود أحيانًا، بينما يمكنه أحيانًا التنفيذ "بالتوازي"؟
ستأخذك هذه المقالة في رحلة عميقة لفهم بيئة وقت تشغيل JavaScript، بما في ذلك حلقة الأحداث (Event Loop)، ومكدس الاستدعاء (Call Stack)، وإدارة الذاكرة. بعد قراءة هذا الدليل، ستفهم لماذا يتم تنفيذ الكود بترتيب معين، وستتمكن من تحديد الأخطاء المتعلقة بالعمليات غير المتزامنة بسرعة، وتحسين أداء الكود وتجنب تسرب الذاكرة.
ماذا ستتعلم من هذه المقالة؟
| الفصل | المحتوى | ماذا ستتمكن من فعله بعد التعلم |
|---|---|---|
| الفصل 1 | نظرة عامة على وقت التشغيل | فهم أين يتم تشغيل كود JavaScript |
| الفصل 2 | وقت تشغيل المتصفح | معرفة واجهات Web API التي يوفرها المتصفح |
| الفصل 3 | وقت تشغيل Node.js | فهم بيئة JavaScript على جانب الخادم |
| الفصل 4 | التعمق في حلقة الأحداث | إتقان ترتيب تنفيذ المهام الكبيرة (Macrotask) والمهام الدقيقة (Microtask) |
| الفصل 5 | مكدس الاستدعاء والذاكرة | فهم عملية تنفيذ الكود وإدارة الذاكرة |
| الفصل 6 | مهارات عملية | تحسين الأداء وتصحيح تسرب الذاكرة |
1. نظرة عامة على وقت التشغيل
🤔 السؤال الأساسي
ما هو "وقت التشغيل"؟ JavaScript مجرد لغة، فلماذا يتصرف نفس الكود بشكل مختلف في بيئات مختلفة؟
1.1 ما هو وقت التشغيل
وقت التشغيل = محرك JavaScript + واجهات API التي توفرها البيئة
إذا شبهنا JavaScript بـ"لغة البرمجة"، فإن وقت التشغيل هو "نظام التشغيل" — إنه يحدد ما يمكن لكودك فعله وما لا يمكنه فعله.
┌─────────────────────────────────────┐
│ كود JavaScript │
├─────────────────────────────────────┤
│ محرك JavaScript (V8) │ ← مسؤول عن تحليل وتنفيذ الكود
├─────────────────────────────────────┤
│ بيئة وقت التشغيل (المتصفح/Node.js) │ ← توفر قدرات إضافية
└─────────────────────────────────────┘تشبيه: JavaScript هي "اللغة العربية الفصحى"، ووقت التشغيل هو "المدينة"
- قواعد JavaScript (الفصحى) هي نفسها في كل مكان
- لكن المرافق التي توفرها المدن المختلفة تختلف:
- المتصفح = يحتوي على DOM وwindow وfetch (مثل مدينة بها مراكز تسوق ومكتبات)
- Node.js = يحتوي على fs وhttp وpath (مثل مدينة بها مصانع وطرق سريعة)
1.2 وقتا التشغيل الرئيسيان
| الخاصية | المتصفح | Node.js |
|---|---|---|
| الاستخدام الرئيسي | تفاعل صفحات الويب، واجهة المستخدم | تطبيقات جانب الخادم، أدوات سطر الأوامر |
| الكائن العام | window | global |
| DOM API | ✅ مدعوم | ❌ غير مدعوم |
| نظام الملفات | ❌ محدود | ✅ دعم كامل |
| نظام الوحدات | ES Modules | CommonJS + ES Modules |
| المؤقتات | setTimeout, setInterval | setTimeout, setInterval |
| طلبات الشبكة | fetch, XMLHttpRequest | وحدات http, https |
👇 جرب بنفسك: قارن بين اختلافات البيئة في المتصفح وNode.js
运行时环境对比
浏览器环境
- ✅ 有 DOM 和 BOM API,可以操作网页
- ✅ 有 Web Storage (localStorage, sessionStorage)
- ✅ 有 fetch 和 XMLHttpRequest 进行网络请求
- ❌ 没有文件系统访问权限
- ❌ 不能直接创建 HTTP 服务器
代码演示:不同环境的差异
核心区别:
浏览器运行时专注于用户界面和网页交互,提供 DOM、BOM、fetch 等前端专用 API。
Node.js 运行时专注于服务器端开发,提供文件系统、HTTP 服务器、进程管理等后端专用 API。
同样的 JavaScript 语法,但能用的 API 完全不同——这就是"环境判断"的重要性。
💡 الفكرة الأساسية
وقت التشغيل يحدد واجهات API التي يمكنك استخدامها. واجهات DOM API التي يمكن استخدامها في المتصفح لا يمكن استخدامها في Node.js؛ وواجهات الملفات التي يمكن استخدامها في Node.js لا يمكن استخدامها في المتصفح. لهذا السبب يحتاج بعض الكود إلى "التحقق من البيئة".
2. وقت تشغيل المتصفح
🤔 السؤال الأساسي
ما القدرات التي يوفرها المتصفح لتمكين JavaScript من التعامل مع صفحات الويب؟
2.1 مكونات وقت تشغيل المتصفح
┌─────────────────────────────────────────────┐
│ محرك JavaScript │
│ (V8 / SpiderMonkey) │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ Web APIs │
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ │
│ │ DOM │ │ BOM │ │ Network │ │
│ │ معالجة │ │ معالجة │ │ طلبات │ │
│ │ الصفحة │ │ المتصفح │ │ الشبكة │ │
│ └─────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ حلقة الأحداث (Event Loop) │
│ مسؤولة عن تنسيق تنفيذ الكود ومعالجة │
│ الأحداث وجدولة المهام │
└─────────────────────────────────────────────┘2.2 الفئات الثلاث الرئيسية لواجهات Web APIs
1. DOM API - معالجة محتوى صفحة الويب
// البحث عن عنصر
const title = document.querySelector('h1')
// تعديل المحتوى
title.textContent = 'عنوان جديد'
// إضافة نمط
title.style.color = 'red'2. BOM API - معالجة المتصفح
// الانتقال إلى صفحة أخرى
window.location.href = 'https://example.com'
// تخزين المتصفح
localStorage.setItem('key', 'value')
// سجل المتصفح
history.back()3. Network API - طلبات الشبكة
// إرسال طلب HTTP
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))2.3 آلية الأحداث الخاصة بالمتصفح
إحدى أقوى ميزات وقت تشغيل المتصفح هي "البرمجة المدفوعة بالأحداث" — لا يحتاج الكود إلى التشغيل المستمر، بل ينتظر حتى يقوم المستخدم بإجراء ما ليتم تنفيذه.
button.addEventListener('click', () => {
console.log('تم النقر على الزر')
})أنواع الأحداث الشائعة:
| نوع الحدث | وقت التشغيل | سيناريو عملي |
|---|---|---|
click | النقر بالماوس | تفاعل الأزرار |
input | تغير محتوى حقل الإدخال | البحث الفوري |
scroll | تمرير الصفحة | التحميل الكسول |
load | اكتمال تحميل الموارد | تهيئة البيانات |
error | حدوث خطأ | معالجة الأخطاء |
3. وقت تشغيل Node.js
🤔 السؤال الأساسي
ما الذي يمكّن JavaScript من العمل على جانب الخادم؟
3.1 مكونات Node.js
┌─────────────────────────────────────────────┐
│ محرك JavaScript │
│ (V8) │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ وحدات Node.js المضمنة │
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ │
│ │ fs │ │ http │ │ path │ │
│ │ عمليات │ │ خادم │ │ معالجة │ │
│ │ الملفات │ │ الشبكة │ │ المسارات │ │
│ └─────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ مكتبة libuv لحلقة الأحداث │
│ دعم الإدخال/الإخراج غير المتزامن │
│ عبر المنصات المختلفة │
└─────────────────────────────────────────────┘3.2 القدرات الفريدة لـ Node.js
1. عمليات نظام الملفات
const fs = require('fs')
// قراءة ملف
fs.readFile('./data.txt', 'utf8', (err, data) => {
if (err) throw err
console.log(data)
})
// كتابة ملف
fs.writeFile('./output.txt', 'Hello', (err) => {
if (err) throw err
console.log('تمت الكتابة بنجاح')
})2. خادم HTTP
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. نظام الوحدات
// CommonJS (الافتراضي في Node.js)
const fs = require('fs')
module.exports = { myFunction }
// ES Modules (الطريقة الحديثة)
import fs from 'fs'
export { myFunction }3.3 مقارنة بين المتصفح وNode.js
| الخاصية | المتصفح | Node.js |
|---|---|---|
| ملف الدخول | ملف HTML | ملف JavaScript |
| الكائن العام | window, document | global, process |
| تحميل الوحدات | وسم <script> | require() / import |
| الأمان | بيئة معزولة (sandbox)، مقيدة | يمكن الوصول إلى موارد النظام |
| الاستخدام | واجهة المستخدم | خدمات الخلفية، الأدوات |
4. التعمق في حلقة الأحداث
🤔 السؤال الأساسي
JavaScript أحادية الخيط، فكيف يمكنها أن تكون "غير محظورة"؟
4.1 ما هي حلقة الأحداث
حلقة الأحداث = "مركز جدولة المهام" في JavaScript
JavaScript أحادية الخيط، مما يعني أنها تستطيع فعل شيء واحد فقط في كل مرة. لكن حلقة الأحداث تجعلها تبدو وكأنها تفعل أشياء كثيرة "في نفس الوقت".
الآلية الأساسية:
- تنفيذ الكود المتزامن (مكدس الاستدعاء)
- معالجة المهام غير المتزامنة (طابور المهام)
- انتظار مهام جديدة (التكرار المستمر)
مكدس الاستدعاء طابور المهام
┌─────────┐ ┌──────────┐
│ مهمة 1 │ │مهمة كبيرة 1│
│ مهمة 2 │ ←──────────── │مهمة كبيرة 2│
│ مهمة 3 │ بعد انتهاء │مهمة كبيرة 3│
└─────────┘ واحدة تؤخذ └──────────┘
↓ التالية ↑
└────────────────────────┘
حلقة الأحداث تتحقق باستمرار4.2 المهام الكبيرة (Macrotask) مقابل المهام الدقيقة (Microtask)
هذا أكثر مفهوم يسبب الارتباك في المقابلات والتطوير الفعلي!
المهام الكبيرة (Macrotask):
setTimeout,setInterval- عمليات الإدخال/الإخراج (I/O)
- عرض واجهة المستخدم (UI Rendering)
المهام الدقيقة (Microtask):
Promise.thenMutationObserverqueueMicrotask
ترتيب التنفيذ: الكود المتزامن → المهام الدقيقة → المهام الكبيرة
👇 جرب بنفسك: لاحظ ترتيب تنفيذ المهام الكبيرة والمهام الدقيقة
任务队列:宏任务 vs 微任务
代码示例
调用栈 (正在执行)
微任务队列 Microtask
宏任务队列 Macrotask
输出日志 (执行顺序)
执行顺序规则
核心要点: 微任务优先级高于宏任务。每次执行完一个宏任务后,都会检查并执行所有微任务,然后再执行下一个宏任务。
4.3 سؤال مقابلة كلاسيكي
console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve().then(() => console.log('3'))
console.log('4')
// الناتج: 1, 4, 3, 2لماذا هذا الترتيب؟
- تنفيذ الكود المتزامن:
console.log('1'),console.log('4')→ الناتج 1, 4 - التحقق من طابور المهام الدقيقة:
Promise.then→ الناتج 3 - التحقق من طابور المهام الكبيرة:
setTimeout→ الناتج 2
💡 نصائح عملية
- إذا أردت تنفيذ الكود بأسرع وقت، استخدم المهام الدقيقة (
Promise.then) - إذا أردت تأخير التنفيذ، استخدم المهام الكبيرة (
setTimeout) - لا تخلط الكثير من العمليات غير المتزامنة أبدًا، وإلا ستقع في "جحيم ردود النداء"
5. مكدس الاستدعاء والذاكرة
🤔 السؤال الأساسي
كيف يتم تنفيذ الكود؟ أين تُخزن المتغيرات؟ متى يتم تحريرها؟
5.1 مكدس الاستدعاء: "آثار أقدام" تنفيذ الدوال
مكدس الاستدعاء = "دفتر ملاحظات" يسجل استدعاءات الدوال
في كل مرة يتم استدعاء دالة، تُضاف سجل جديد إلى المكدس؛ وعندما تنتهي الدالة من التنفيذ، يُزال السجل.
function a() {
b()
}
function b() {
c()
}
function c() {
console.log('اكتمل التنفيذ')
}
a()تغيرات مكدس الاستدعاء:
الخطوة 1: استدعاء ()a
┌─────────┐
│ a │
└─────────┘
الخطوة 2: ()a تستدعي ()b
┌─────────┐
│ b │
│ a │
└─────────┘
الخطوة 3: ()b تستدعي ()c
┌─────────┐
│ c │
│ b │
│ a │
└─────────┘
الخطوة 4: ()c تنتهي، ويتم إخراجها بالتتابع
┌─────────┐
│ b │
│ a │
└─────────┘👇 جرب بنفسك: لاحظ تغيرات مكدس الاستدعاء
调用栈:函数执行的足迹
代码
调用栈
当前状态:
调用 main()
输出
调用栈工作原理:
- 每次调用函数,就会在栈上"压入"一个新的"栈帧"
- 栈帧记录了函数的执行状态、局部变量等信息
- 函数执行完毕,栈帧就会从栈上"弹出"
- 栈是"后进先出"(LIFO)的数据结构
- 如果递归太深,会导致"栈溢出"错误
调用栈就像一摞盘子:最后放上去的盘子最先被取走。每个函数就是一个盘子,执行完就取走,然后继续执行下面的函数。
5.2 إدارة الذاكرة: أين تذهب القمامة؟
JavaScript لديها آلية "جمع القمامة تلقائيًا" — لا تحتاج إلى تحرير الذاكرة يدويًا، فالمحرك يقوم بذلك نيابة عنك.
مبدأ جمع القمامة: خوارزمية الوسم والإزالة (Mark-and-Sweep)
- مرحلة الوسم: البدء من "الجذر"، والعثور على جميع المتغيرات التي يمكن الوصول إليها
- مرحلة الإزالة: المتغيرات التي لم يتم وسمها تعتبر "قمامة" وسيتم جمعها
// مثال على جمع القمامة
let obj1 = { name: 'كائن 1' }
let obj2 = { name: 'كائن 2' }
// تمت إعادة تعيين obj1، الكائن الأصلي فقد مرجعه
obj1 = null // الكائن الأصلي { name: 'كائن 1' } سيتم جمعه
// obj2 لا يزال قيد الاستخدام، ولن يتم جمعه
console.log(obj2.name)👇 جرب بنفسك: لاحظ عملية جمع القمامة
垃圾回收机制
对象引用关系
标记-清除算法 (Mark-and-Sweep)
从根对象(Root)开始,遍历所有可达对象,标记为"活动对象"
遍历整个堆内存,回收所有未被标记的对象
清除所有标记位,为下一次垃圾回收做准备
核心要点
- 根对象(Root): 全局变量、栈上的变量等,总是被认为是可达的
- 可达对象: 从根对象出发,通过引用链能访问到的对象
- 垃圾对象: 无法从根对象访问到的对象,会被回收
- 循环引用: 如果两个对象互相引用但都不可达,仍会被回收
实际应用技巧
对象不再使用时,将其设为 null
使用 const/let 代替 var
组件销毁时移除所有监听器
用 DevTools Memory 面板监控
5.3 تسرب الذاكرة: عواقب نسيان التنظيف
تسرب الذاكرة = ذاكرة كان يجب تحريرها لكنها لم تُحرر، وتتراكم أكثر فأكثر
الأسباب الشائعة:
1. كثرة المتغيرات العامة
// ❌ خطأ: المتغيرات العامة لا يتم جمعها
globalCache = []
function addItem(item) {
globalCache.push(item)
}2. عدم إزالة مستمعي الأحداث
// ❌ خطأ: لم يتم إزالة المستمع
button.addEventListener('click', handleClick)
// ✅ صحيح: إزالة المستمع عند عدم الحاجة
button.removeEventListener('click', handleClick)3. إغلاقات (Closures) تشير إلى كائنات كبيرة
// ❌ خطأ: الإغلاق يشير دائمًا إلى كائن كبير، ولن يتم جمعه
function createHandler() {
const bigData = new Array(1000000).fill('data')
return function() {
console.log('قيد المعالجة')
}
}
const handler = createHandler() // bigData موجود دائمًا في الذاكرة👇 جرب بنفسك: لاحظ كيف يحدث تسرب الذاكرة
内存泄漏演示
全局变量泄漏
问题:全局变量不会被垃圾回收,会一直占用内存
示例:不断往全局数组添加数据,从不清理
❌ 错误做法
// 全局变量不会被回收
globalCache = []
function addItem() {
globalCache.push(largeData)
}如何避免内存泄漏
- 避免全局变量: 使用 const/let 代替 var,尽量使用局部变量
- 及时清理监听器: 组件销毁时移除所有事件监听
- 释放闭包引用: 不需要时将闭包变量设为 null
- 使用 WeakMap/WeakSet: 自动清理不再被引用的对象
- 定期检查: 用 DevTools Memory 面板检查内存泄漏
💡 نصائح عملية
- الفحص الدوري: افتح DevTools في المتصفح → Memory → Take Heap Snapshot، للتحقق من استهلاك الذاكرة
- تجنب المتغيرات العامة: استخدم
constوletقدر الإمكان، ولا تستخدمvar - التنظيف في الوقت المناسب: أزل مستمعي الأحداث والمؤقتات عند الانتهاء من استخدامها
- المراجع الضعيفة: استخدم
WeakMapوWeakSetلتخزين مراجع الكائنات
6. مهارات عملية
🤔 السؤال الأساسي
كيف تكتب كود JavaScript عالي الأداء؟ كيف تصحح الأخطاء عند مواجهة المشاكل؟
6.1 نصائح تحسين الأداء
1. تقليل إعادة التخطيط وإعادة الرسم (Reflow & Repaint)
// ❌ خطأ: كل دورة تشغل إعادة تخطيط
for (let i = 0; i < 1000; i++) {
element.style.top = i + 'px'
}
// ✅ صحيح: تعديل دفعة واحدة
element.style.transform = `translateY(${position}px)`2. استخدام تفويض الأحداث (Event Delegation)
// ❌ خطأ: إضافة مستمع لكل زر
buttons.forEach(btn => {
btn.addEventListener('click', handleClick)
})
// ✅ صحيح: إضافة مستمع واحد فقط للعنصر الأب
container.addEventListener('click', (e) => {
if (e.target.matches('.button')) {
handleClick(e)
}
})3. مانع الاهتزاز (Debounce) والاختناق (Throttle)
// مانع الاهتزاز: التنفيذ بعد توقف المستخدم عن الإدخال
function debounce(fn, delay) {
let timer
return function(...args) {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}
// الاختناق: تقييد تكرار التنفيذ
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 نصائح التصحيح
1. استخدام DevTools لعرض مكدس الاستدعاء
function a() {
b()
}
function b() {
c()
}
function c() {
debugger // توقف هنا لعرض مكدس الاستدعاء
}
a()2. استخدام console.trace() لتتبع مسار التنفيذ
function trackExecution() {
console.trace('مسار التنفيذ')
// سيُخرج مكدس الاستدعاء الكامل
}3. استخدام Performance لتحليل الأداء
performance.mark('start')
// تنفيذ بعض الكود
for (let i = 0; i < 10000; i++) {
// ...
}
performance.mark('end')
performance.measure('أداء الحلقة', 'start', 'end')
const measure = performance.getEntriesByName('أداء الحلقة')[0]
console.log(`زمن التنفيذ: ${measure.duration}ms`)6.3 دليل سريع للمشاكل الشائعة
| المشكلة | السبب المحتمل | الحل |
|---|---|---|
| استهلاك عالٍ للذاكرة | تسرب الذاكرة، تخزين مؤقت كثير جدًا | تحقق من المتغيرات العامة، أزل المستمعات |
| تجميد الصفحة | مهام طويلة تحجب الخيط الرئيسي | قسم المهام، استخدم Web Workers |
| عدم تشغيل الأحداث | المستمع غير مربوط، العنصر غير موجود | تحقق من توقيت تحميل DOM |
| ترتيب غير صحيح للعمليات غير المتزامنة | خلط المهام الكبيرة والدقيقة | استخدم Promise أو async/await بشكل موحد |
| عدم دقة المؤقتات | حجب الخيط الرئيسي | استخدم Web Workers أو requestAnimationFrame |
الخلاصة
يجب أن تكون الآن قادرًا على فهم:
- وقت التشغيل = المحرك + واجهات API البيئية، وأوقات التشغيل المختلفة توفر قدرات مختلفة
- حلقة الأحداث مسؤولة عن تنسيق ترتيب تنفيذ الكود المتزامن والمهام الدقيقة والمهام الكبيرة
- مكدس الاستدعاء يسجل عملية تنفيذ الدوال، وتجاوز سعة المكدس يحدث بسبب التعمق الزائد في الاستدعاء الذاتي (recursion)
- جمع القمامة ينظف المتغيرات غير المستخدمة تلقائيًا، لكن يجب الانتباه إلى تسرب الذاكرة
- مفتاح تحسين الأداء هو تقليل إعادة التخطيط وإعادة الرسم، واستخدام العمليات غير المتزامنة بشكل معقول
💡 عند مواجهة مشكلة، تحدث إلى الذكاء الاصطناعي بهذه الطريقة
- "هذه الدالة تنفذ ببطء شديد، ساعدني في معرفة كيفية تحسين الأداء"
- "استهلاك الذاكرة في ارتفاع مستمر، قد يكون هناك تسرب في الذاكرة، ساعدني في التحقق"
- "ترتيب العمليات غير المتزامنة غير صحيح، يجب أن يكون A أولاً ثم B، لكن حاليًا A وB يبدآن في نفس الوقت تقريبًا"
- "مستمع الحدث لا يعمل، تحقق مما إذا كان العنصر قد تم تحميله في DOM"