اكتشف WeakRef في JavaScript لتحسين استخدام الذاكرة. تعرّف على المراجع الضعيفة وسجلات التصفية والتطبيقات العملية لبناء تطبيقات ويب فعالة.
WeakRef في JavaScript: المراجع الضعيفة وإدارة الذاكرة الواعية
تعتمد JavaScript، على الرغم من كونها لغة قوية لبناء تطبيقات الويب الديناميكية، على جمع القمامة التلقائي لإدارة الذاكرة. تأتي هذه الراحة بتكلفة: غالبًا ما يكون لدى المطورين سيطرة محدودة على وقت إلغاء تخصيص الكائنات. يمكن أن يؤدي هذا إلى استهلاك ذاكرة غير متوقع واختناقات في الأداء، خاصة في التطبيقات المعقدة التي تتعامل مع مجموعات البيانات الكبيرة أو الكائنات طويلة الأجل. أدخل WeakRef
، وهي آلية تم تقديمها لتوفير تحكم أكثر دقة في دورات حياة الكائنات وتحسين كفاءة الذاكرة.
فهم المراجع القوية والضعيفة
قبل الغوص في WeakRef
، من الضروري فهم مفهوم المراجع القوية و الضعيفة. في JavaScript، تعد المرجع القوي هو الطريقة القياسية التي تتم بها الإشارة إلى الكائنات. عندما يكون للكائن مرجع قوي واحد على الأقل يشير إليه، لن يستعيد جامع القمامة ذاكرته. يعتبر الكائن قابلاً للوصول. على سبيل المثال:
let myObject = { name: "Example" }; // myObject holds a strong reference
let anotherReference = myObject; // anotherReference also holds a strong reference
في هذه الحالة، سيظل الكائن { name: "Example" }
في الذاكرة طالما أن myObject
أو anotherReference
موجود. إذا قمنا بتعيين كليهما على null
:
myObject = null;
anotherReference = null;
يصبح الكائن غير قابل للوصول ومؤهلاً لجمع القمامة.
من ناحية أخرى، يعد المرجع الضعيف مرجعًا لا يمنع الكائن من جمع القمامة. عندما يجد جامع القمامة أن الكائن يحتوي فقط على مراجع ضعيفة تشير إليه، فيمكنه استعادة ذاكرة الكائن. يتيح لك هذا تتبع كائن ما دون منعه من إلغاء تخصيصه عندما لا يتم استخدامه بنشاط.
تقديم JavaScript WeakRef
يتيح لك كائن WeakRef
إنشاء مراجع ضعيفة للكائنات. إنه جزء من مواصفات ECMAScript وهو متاح في بيئات JavaScript الحديثة (Node.js والمتصفحات الحديثة). إليك الطريقة التي يعمل بها:
let myObject = { name: "Important Data" };
let weakRef = new WeakRef(myObject);
console.log(weakRef.deref()); // Access the object (if it hasn't been garbage collected)
دعنا نقسم هذا المثال:
- نقوم بإنشاء كائن
myObject
. - نقوم بإنشاء مثيل
WeakRef
،weakRef
، يشير إلىmyObject
. الأهم من ذلك، أن `weakRef` لا يمنع جمع القمامة لـ `myObject`. - تحاول الطريقة
deref()
الخاصة بـWeakRef
استرداد الكائن المشار إليه. إذا كان الكائن لا يزال في الذاكرة (لم يتم جمع القمامة)، فستعيدderef()
الكائن. إذا تم جمع القمامة للكائن، فستعيدderef()
undefined
.
لماذا تستخدم WeakRef؟
حالة الاستخدام الأساسية لـ WeakRef
هي بناء هياكل بيانات أو ذاكرات تخزين مؤقتة لا تمنع الكائنات من جمع القمامة عندما لم تعد هناك حاجة إليها في مكان آخر في التطبيق. ضع في اعتبارك هذه السيناريوهات:
- التخزين المؤقت: تخيل تطبيقًا كبيرًا يحتاج بشكل متكرر إلى الوصول إلى بيانات مكلفة حسابيًا. يمكن لذاكرة التخزين المؤقت تخزين هذه النتائج لتحسين الأداء. ومع ذلك، إذا كانت ذاكرة التخزين المؤقت تحتفظ بمراجع قوية لهذه الكائنات، فلن يتم جمع القمامة أبدًا، مما قد يؤدي إلى تسرب الذاكرة. يتيح استخدام
WeakRef
في ذاكرة التخزين المؤقت لجامع القمامة استعادة الكائنات المخزنة مؤقتًا عندما لم تعد قيد الاستخدام النشط من قبل التطبيق، مما يؤدي إلى تحرير الذاكرة. - ارتباطات الكائنات: في بعض الأحيان تحتاج إلى ربط بيانات التعريف بكائن ما دون تعديل الكائن الأصلي أو منعه من جمع القمامة. يمكن استخدام
WeakRef
للحفاظ على هذا الارتباط. على سبيل المثال، في محرك ألعاب، قد ترغب في ربط خصائص الفيزياء بكائنات اللعبة دون تعديل فئة كائن اللعبة مباشرةً. - تحسين معالجة DOM: في تطبيقات الويب، يمكن أن تكون معالجة Document Object Model (DOM) مكلفة. يمكن استخدام المراجع الضعيفة لتتبع عناصر DOM دون منع إزالتها من DOM عندما لم تعد هناك حاجة إليها. هذا مفيد بشكل خاص عند التعامل مع المحتوى الديناميكي أو تفاعلات واجهة المستخدم المعقدة.
FinalizationRegistry: معرفة متى يتم جمع الكائنات
في حين أن WeakRef
يسمح لك بإنشاء مراجع ضعيفة، فإنه لا يوفر آلية لتلقي إشعار عندما يتم بالفعل جمع القمامة لكائن ما. هذا هو المكان الذي يأتي فيه FinalizationRegistry
. يوفر FinalizationRegistry
طريقة لتسجيل دالة رد نداء سيتم تنفيذها بعد جمع القمامة لكائن ما.
let registry = new FinalizationRegistry(
(heldValue) => {
console.log("Object with held value " + heldValue + " has been garbage collected.");
}
);
let myObject = { name: "Ephemeral Data" };
registry.register(myObject, "myObjectIdentifier");
myObject = null; // Make the object eligible for garbage collection
//The callback in FinalizationRegistry will be executed sometime after myObject is garbage collected.
في هذا المثال:
- نقوم بإنشاء مثيل
FinalizationRegistry
، ونمرر دالة رد نداء إلى المُنشئ الخاص به. سيتم تنفيذ رد النداء هذا عندما يتم جمع القمامة لكائن مسجل في السجل. - نقوم بتسجيل
myObject
في السجل، جنبًا إلى جنب مع القيمة المحتفظ بها ("myObjectIdentifier"
). سيتم تمرير القيمة المحتفظ بها كحجة لدالة رد النداء عند تنفيذها. - نقوم بتعيين
myObject
علىnull
، مما يجعل الكائن الأصلي مؤهلاً لجمع القمامة. لاحظ أن رد النداء لن يتم تنفيذه على الفور؛ سيحدث ذلك بعد فترة ما بعد قيام جامع القمامة باستعادة ذاكرة الكائن.
الجمع بين WeakRef و FinalizationRegistry
غالبًا ما يتم استخدام WeakRef
و FinalizationRegistry
معًا لبناء استراتيجيات أكثر تعقيدًا لإدارة الذاكرة. على سبيل المثال، يمكنك استخدام WeakRef
لإنشاء ذاكرة تخزين مؤقتة لا تمنع الكائنات من جمع القمامة، ثم استخدام FinalizationRegistry
لتنظيف الموارد المرتبطة بهذه الكائنات عند جمعها.
let registry = new FinalizationRegistry(
(key) => {
console.log("Cleaning up resource for key: " + key);
// Perform cleanup operations here, such as releasing database connections
}
);
class Resource {
constructor(key) {
this.key = key;
// Acquire a resource (e.g., database connection)
console.log("Acquiring resource for key: " + key);
registry.register(this, key);
}
release() {
registry.unregister(this); //Prevent finalization if released manually
console.log("Releasing resource for key: " + this.key + " manually.");
}
}
let resource1 = new Resource("resource1");
//... Later, resource1 is no longer needed
resource1.release();
let resource2 = new Resource("resource2");
resource2 = null; // Make eligible for GC. Cleanup will happen eventually via the FinalizationRegistry
في هذا المثال:
- نقوم بتعريف فئة
Resource
التي تحصل على مورد في مُنشئها وتسجل نفسها فيFinalizationRegistry
. - عند جمع القمامة لكائن
Resource
، سيتم تنفيذ رد النداء فيFinalizationRegistry
، مما يسمح لنا بتحرير المورد الذي تم الحصول عليه. - توفر الطريقة `release()` طريقة لتحرير المورد بشكل صريح وإلغاء تسجيله من السجل، مما يمنع تنفيذ رد نداء التصفية. هذا أمر بالغ الأهمية لإدارة الموارد بشكل محدد.
أمثلة عملية وحالات الاستخدام
1. التخزين المؤقت للصور في تطبيق ويب
ضع في اعتبارك تطبيق ويب يعرض عددًا كبيرًا من الصور. لتحسين الأداء، قد ترغب في تخزين هذه الصور مؤقتًا في الذاكرة. ومع ذلك، إذا كانت ذاكرة التخزين المؤقت تحتفظ بمراجع قوية للصور، فستظل في الذاكرة حتى لو لم تعد معروضة على الشاشة، مما يؤدي إلى الاستخدام المفرط للذاكرة. يمكن استخدام WeakRef
لبناء ذاكرة تخزين مؤقتة للصور فعالة من حيث الذاكرة.
class ImageCache {
constructor() {
this.cache = new Map();
}
getImage(url) {
const weakRef = this.cache.get(url);
if (weakRef) {
const image = weakRef.deref();
if (image) {
console.log("Cache hit for " + url);
return image;
}
console.log("Cache expired for " + url);
this.cache.delete(url); // Remove the expired entry
}
console.log("Cache miss for " + url);
return this.loadImage(url);
}
async loadImage(url) {
// Simulate loading an image from a URL
await new Promise(resolve => setTimeout(resolve, 100));
const image = { url: url, data: "Image data for " + url };
this.cache.set(url, new WeakRef(image));
return image;
}
}
const imageCache = new ImageCache();
async function displayImage(url) {
const image = await imageCache.getImage(url);
console.log("Displaying image: " + image.url);
}
displayImage("image1.jpg");
displayImage("image1.jpg"); //Cache hit
displayImage("image2.jpg");
في هذا المثال، تستخدم الفئة ImageCache
Map
لتخزين مثيلات WeakRef
التي تشير إلى كائنات الصور. عند طلب صورة، تتحقق ذاكرة التخزين المؤقت أولاً مما إذا كانت موجودة في الخريطة. إذا كان الأمر كذلك، فإنها تحاول استرداد الصورة باستخدام deref()
. إذا كانت الصورة لا تزال في الذاكرة، فسيتم إعادتها من ذاكرة التخزين المؤقت. إذا تم جمع القمامة للصورة، تتم إزالة إدخال ذاكرة التخزين المؤقت، ويتم تحميل الصورة من المصدر.
2. تتبع رؤية عنصر DOM
في تطبيق صفحة واحدة (SPA)، قد ترغب في تتبع رؤية عناصر DOM لتنفيذ إجراءات معينة عندما تصبح مرئية أو غير مرئية (على سبيل المثال، الصور التي يتم تحميلها بكسل، أو تشغيل الرسوم المتحركة). يمكن أن يؤدي استخدام مراجع قوية لعناصر DOM إلى منع جمع القمامة حتى لو لم تعد مرفقة بـ DOM. يمكن استخدام WeakRef
لتجنب هذه المشكلة.
class VisibilityTracker {
constructor() {
this.trackedElements = new Map();
}
trackElement(element, callback) {
const weakRef = new WeakRef(element);
this.trackedElements.set(element, { weakRef, callback });
}
observe() {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
this.trackedElements.forEach(({ weakRef, callback }, element) => {
const trackedElement = weakRef.deref();
if (trackedElement === element && entry.target === element) {
callback(entry.isIntersecting);
}
});
});
});
this.trackedElements.forEach((value, key) => {
observer.observe(key);
});
}
}
//Example usage
const visibilityTracker = new VisibilityTracker();
const element1 = document.createElement("div");
element1.textContent = "Element 1";
document.body.appendChild(element1);
const element2 = document.createElement("div");
element2.textContent = "Element 2";
document.body.appendChild(element2);
visibilityTracker.trackElement(element1, (isVisible) => {
console.log("Element 1 is visible: " + isVisible);
});
visibilityTracker.trackElement(element2, (isVisible) => {
console.log("Element 2 is visible: " + isVisible);
});
visibilityTracker.observe();
في هذا المثال، تستخدم الفئة VisibilityTracker
IntersectionObserver
للكشف عن متى تصبح عناصر DOM مرئية أو غير مرئية. تقوم بتخزين مثيلات WeakRef
التي تشير إلى العناصر المتعقبة. عندما يكتشف مراقب التقاطع تغييرًا في الرؤية، فإنه يتكرر على العناصر المتعقبة ويتحقق مما إذا كان العنصر لا يزال موجودًا (لم يتم جمع القمامة له) وما إذا كان العنصر الذي تمت ملاحظته يطابق العنصر المتعقب. إذا تم استيفاء كلا الشرطين، فإنه ينفذ رد النداء المقترن.
3. إدارة الموارد في محرك الألعاب
غالبًا ما تدير محركات الألعاب عددًا كبيرًا من الموارد، مثل القوام والموديلات وملفات الصوت. يمكن أن تستهلك هذه الموارد قدرًا كبيرًا من الذاكرة. يمكن استخدام WeakRef
و FinalizationRegistry
لإدارة هذه الموارد بكفاءة.
class Texture {
constructor(url) {
this.url = url;
// Load the texture data (simulated)
this.data = "Texture data for " + url;
console.log("Texture loaded: " + url);
}
dispose() {
console.log("Texture disposed: " + this.url);
// Release the texture data (e.g., free GPU memory)
this.data = null; // Simulate releasing memory
}
}
class TextureCache {
constructor() {
this.cache = new Map();
this.registry = new FinalizationRegistry((texture) => {
texture.dispose();
});
}
getTexture(url) {
const weakRef = this.cache.get(url);
if (weakRef) {
const texture = weakRef.deref();
if (texture) {
console.log("Texture cache hit: " + url);
return texture;
}
console.log("Texture cache expired: " + url);
this.cache.delete(url);
}
console.log("Texture cache miss: " + url);
const texture = new Texture(url);
this.cache.set(url, new WeakRef(texture));
this.registry.register(texture, texture);
return texture;
}
}
const textureCache = new TextureCache();
const texture1 = textureCache.getTexture("texture1.png");
const texture2 = textureCache.getTexture("texture1.png"); //Cache hit
//... Later, the textures are no longer needed and become eligible for garbage collection.
في هذا المثال، تستخدم الفئة TextureCache
Map
لتخزين مثيلات WeakRef
التي تشير إلى كائنات Texture
. عند طلب نسيج، تتحقق ذاكرة التخزين المؤقت أولاً مما إذا كانت موجودة في الخريطة. إذا كان الأمر كذلك، فإنها تحاول استرداد الملمس باستخدام deref()
. إذا كان الملمس لا يزال في الذاكرة، فسيتم إعادته من ذاكرة التخزين المؤقت. إذا تم جمع القمامة للملمس، تتم إزالة إدخال ذاكرة التخزين المؤقت، ويتم تحميل الملمس من المصدر. يتم استخدام FinalizationRegistry
للتخلص من الملمس عند جمع القمامة له، مما يؤدي إلى تحرير الموارد المرتبطة (على سبيل المثال، ذاكرة GPU).
أفضل الممارسات والاعتبارات
- استخدم باعتدال: يجب استخدام
WeakRef
وFinalizationRegistry
بحكمة. يمكن أن يؤدي الإفراط في استخدامها إلى جعل التعليمات البرمجية الخاصة بك أكثر تعقيدًا ويصعب تصحيحها. - ضع في اعتبارك الآثار المترتبة على الأداء: في حين أن
WeakRef
وFinalizationRegistry
يمكن أن يحسنوا كفاءة الذاكرة، إلا أنهما يمكن أن يقدموا أيضًا عبئًا إضافيًا على الأداء. تأكد من قياس أداء التعليمات البرمجية الخاصة بك قبل وبعد استخدامها. - كن على دراية بدورة جمع القمامة: توقيت جمع القمامة لا يمكن التنبؤ به. يجب ألا تعتمد على جمع القمامة في وقت معين. قد يتم تنفيذ ردود الاتصال المسجلة في
FinalizationRegistry
بعد تأخير كبير. - تعامل مع الأخطاء بأناقة: يمكن لطريقة
deref()
الخاصة بـWeakRef
أن تُرجعundefined
إذا تم جمع القمامة للكائن. يجب عليك التعامل مع هذه الحالة بشكل مناسب في التعليمات البرمجية الخاصة بك. - تجنب التبعيات الدائرية: يمكن أن تؤدي التبعيات الدائرية التي تتضمن
WeakRef
وFinalizationRegistry
إلى سلوك غير متوقع. كن حذرًا عند استخدامها في رسوم بيانية للكائنات المعقدة. - إدارة الموارد: قم بتحرير الموارد بشكل صريح كلما أمكن ذلك. لا تعتمد فقط على جمع القمامة وسجلات التصفية لتنظيف الموارد. توفير آليات للإدارة اليدوية للموارد (مثل طريقة `release()` في مثال المورد أعلاه).
- الاختبار: يمكن أن يكون اختبار التعليمات البرمجية التي تستخدم `WeakRef` و `FinalizationRegistry` أمرًا صعبًا بسبب الطبيعة التي لا يمكن التنبؤ بها لجمع القمامة. ضع في اعتبارك استخدام تقنيات مثل فرض جمع القمامة في بيئات الاختبار (إذا كانت مدعومة) أو استخدام كائنات وهمية لمحاكاة سلوك جمع القمامة.
بدائل WeakRef
قبل استخدام WeakRef
، من المهم مراعاة الأساليب البديلة لإدارة الذاكرة:
- تجمعات الكائنات: يمكن استخدام تجمعات الكائنات لإعادة استخدام الكائنات بدلاً من إنشاء كائنات جديدة، مما يقلل من عدد الكائنات التي تحتاج إلى جمع القمامة لها.
- المذاكرة: المذاكرة هي تقنية لتخزين نتائج استدعاءات الدالة المكلفة. يمكن أن يقلل هذا من الحاجة إلى إنشاء كائنات جديدة.
- هياكل البيانات: اختر بعناية هياكل البيانات التي تقلل من استخدام الذاكرة. على سبيل المثال، يمكن أن يؤدي استخدام المصفوفات المكتوبة بدلاً من المصفوفات العادية إلى تقليل استهلاك الذاكرة عند التعامل مع البيانات الرقمية.
- الإدارة اليدوية للذاكرة (تجنبها إن أمكن): في بعض اللغات منخفضة المستوى، يتمتع المطورون بالتحكم المباشر في تخصيص الذاكرة وإلغاء تخصيصها. ومع ذلك، فإن الإدارة اليدوية للذاكرة عرضة للأخطاء ويمكن أن تؤدي إلى تسرب الذاكرة وغيرها من المشكلات. يتم تثبيطها بشكل عام في JavaScript.
الخلاصة
توفر WeakRef
و FinalizationRegistry
أدوات قوية لبناء تطبيقات JavaScript فعالة من حيث الذاكرة. من خلال فهم كيفية عملها ومتى تستخدمها، يمكنك تحسين أداء وثبات تطبيقاتك. ومع ذلك، من المهم استخدامها بحكمة والنظر في الأساليب البديلة لإدارة الذاكرة قبل اللجوء إلى WeakRef
. مع استمرار تطور JavaScript، من المحتمل أن تصبح هذه الميزات أكثر أهمية لبناء تطبيقات معقدة ومكثفة الموارد.