WeakMap بالعربي: اربط بيانات بالكائنات من غير Memory Leak
هتعرف هنا إمتى تستخدم WeakMap بدل Map عشان تربط بيانات مؤقتة بكائنات JavaScript من غير ما تعمل memory leak صامت يكبر مع الوقت.
المشكلة باختصار
الطريقة الشائعة إنك تعمل Map وتخزن فيها بيانات إضافية لكل object. مثال: آخر وقت request اتعمل، صلاحيات user داخل request، أو قياسات مؤقتة لكل DOM node. الطريقة دي بتفشل لما الكائن الأصلي يخلص شغله، لكن الـ Map تفضل ماسكة reference عليه. النتيجة: الـ garbage collector مش هيقدر يشيله من الذاكرة.
سيناريو واقعي: لو عندك لوحة تحكم بتفتح وتقفل 20,000 row يوميًا، وكل row مربوط ببيانات UI داخل Map، ممكن تفضل البيانات القديمة عايشة لساعات. لو كل row ماسك 2KB metadata، فأنت بتتكلم عن حوالي 40MB زيادة في اليوم من غير سبب واضح.
الفكرة بمثال بسيط
ركز في المثال ده. عندك موظف داخل شركة، ومعاه كارت دخول مؤقت. لما الموظف يسيب الشركة، المفروض الكارت يتلغي تلقائيًا. Map هنا عاملة زي مكتب أمن بيحتفظ بصورة الموظف والكارت حتى بعد ما الموظف مشي. WeakMap أقرب لنظام بيقول: طالما الموظف نفسه مش موجود في أي مكان تاني، امسح بيانات الكارت معاه.
علميًا، WeakMap لا تعمل strong reference على المفاتيح. المفتاح لازم يكون object أو non-registered symbol. لو الكائن لم يعد reachable من باقي البرنامج، وجوده كمفتاح داخل WeakMap لا يمنع جمعه بواسطة الـ garbage collector. ده موثق في MDN، وده سبب إن WeakMap لا تسمح بعمل loop على المفاتيح: لأن حالة المفاتيح مرتبطة بالـ garbage collection، وهي عملية غير حتمية.
مثال تنفيذي يقيس الفرق
الافتراض إنك بتشغل Node.js محليًا، وعايز تشوف الفرق بشكل تقريبي. القياس مش benchmark نهائي، لأن الـ garbage collector توقيته مش مضمون. لكنه كفاية يوضح السلوك.
// weakmap-demo.js
// شغله كده:
// node --expose-gc weakmap-demo.js map
// node --expose-gc weakmap-demo.js weak
const mode = process.argv[2] || "map";
const store = mode === "weak" ? new WeakMap() : new Map();
function mb() {
return Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
}
if (!global.gc) {
throw new Error("Run with: node --expose-gc weakmap-demo.js map|weak");
}
global.gc();
console.log("before", mb(), "MB");
for (let i = 0; i < 50_000; i++) {
const request = { id: i };
store.set(request, {
startedAt: Date.now(),
payload: "x".repeat(2048)
});
}
global.gc();
console.log("after", mb(), "MB");
console.log("mode", mode, "size", store.size ?? "not enumerable");